chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import type { AgiloftAttachFileParams, AgiloftAttachFileResponse } from '@/tools/agiloft/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agiloftAttachFileTool: ToolConfig<AgiloftAttachFileParams, AgiloftAttachFileResponse> =
|
||||
{
|
||||
id: 'agiloft_attach_file',
|
||||
name: 'Agiloft Attach File',
|
||||
description: 'Attach a file to a field in an Agiloft record.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
|
||||
},
|
||||
knowledgeBase: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Knowledge base name',
|
||||
},
|
||||
login: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft password',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table name (e.g., "contracts")',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the record to attach the file to',
|
||||
},
|
||||
fieldName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Name of the attachment field',
|
||||
},
|
||||
file: {
|
||||
type: 'file',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'File to attach',
|
||||
},
|
||||
fileName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Name to assign to the file (defaults to original file name)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/agiloft/attach',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
instanceUrl: params.instanceUrl,
|
||||
knowledgeBase: params.knowledgeBase,
|
||||
login: params.login,
|
||||
password: params.password,
|
||||
table: params.table,
|
||||
recordId: params.recordId,
|
||||
fieldName: params.fieldName,
|
||||
file: params.file,
|
||||
fileName: params.fileName,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
recordId: '',
|
||||
fieldName: '',
|
||||
fileName: '',
|
||||
totalAttachments: 0,
|
||||
},
|
||||
error: data.error || 'Failed to attach file',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
recordId: data.output?.recordId ?? '',
|
||||
fieldName: data.output?.fieldName ?? '',
|
||||
fileName: data.output?.fileName ?? '',
|
||||
totalAttachments: data.output?.totalAttachments ?? 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
recordId: {
|
||||
type: 'string',
|
||||
description: 'ID of the record the file was attached to',
|
||||
},
|
||||
fieldName: {
|
||||
type: 'string',
|
||||
description: 'Name of the field the file was attached to',
|
||||
},
|
||||
fileName: {
|
||||
type: 'string',
|
||||
description: 'Name of the attached file',
|
||||
},
|
||||
totalAttachments: {
|
||||
type: 'number',
|
||||
description: 'Total number of files attached in the field after the operation',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import type {
|
||||
AgiloftAttachmentInfoParams,
|
||||
AgiloftAttachmentInfoResponse,
|
||||
} from '@/tools/agiloft/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agiloftAttachmentInfoTool: ToolConfig<
|
||||
AgiloftAttachmentInfoParams,
|
||||
AgiloftAttachmentInfoResponse
|
||||
> = {
|
||||
id: 'agiloft_attachment_info',
|
||||
name: 'Agiloft Attachment Info',
|
||||
description: 'Get information about file attachments on a record field.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
|
||||
},
|
||||
knowledgeBase: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Knowledge base name',
|
||||
},
|
||||
login: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft password',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table name (e.g., "contracts")',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the record to check attachments on',
|
||||
},
|
||||
fieldName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Name of the attachment field to inspect',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => '/api/tools/agiloft/attachment_info',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => ({
|
||||
instanceUrl: params.instanceUrl,
|
||||
knowledgeBase: params.knowledgeBase,
|
||||
login: params.login,
|
||||
password: params.password,
|
||||
table: params.table,
|
||||
recordId: params.recordId,
|
||||
fieldName: params.fieldName,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: data.success ?? true,
|
||||
output: data.output,
|
||||
...(data.error ? { error: data.error } : {}),
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
attachments: {
|
||||
type: 'array',
|
||||
description: 'List of attachments with position, name, and size',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
position: {
|
||||
type: 'number',
|
||||
description: 'Position index of the attachment in the field',
|
||||
},
|
||||
name: { type: 'string', description: 'File name of the attachment' },
|
||||
size: { type: 'number', description: 'File size in bytes' },
|
||||
},
|
||||
},
|
||||
},
|
||||
totalCount: {
|
||||
type: 'number',
|
||||
description: 'Total number of attachments in the field',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import type { AgiloftCreateRecordParams, AgiloftRecordResponse } from '@/tools/agiloft/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agiloftCreateRecordTool: ToolConfig<AgiloftCreateRecordParams, AgiloftRecordResponse> =
|
||||
{
|
||||
id: 'agiloft_create_record',
|
||||
name: 'Agiloft Create Record',
|
||||
description: 'Create a new record in an Agiloft table.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
|
||||
},
|
||||
knowledgeBase: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Knowledge base name',
|
||||
},
|
||||
login: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft password',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table name (e.g., "contracts", "contacts.employees")',
|
||||
},
|
||||
data: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Record field values as a JSON object (e.g., {"first_name": "John", "status": "Active"})',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => '/api/tools/agiloft/create_record',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => ({
|
||||
instanceUrl: params.instanceUrl,
|
||||
knowledgeBase: params.knowledgeBase,
|
||||
login: params.login,
|
||||
password: params.password,
|
||||
table: params.table,
|
||||
data: params.data,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: data.success ?? true,
|
||||
output: data.output,
|
||||
...(data.error ? { error: data.error } : {}),
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'ID of the created record',
|
||||
},
|
||||
fields: {
|
||||
type: 'json',
|
||||
description: 'Field values of the created record',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { AgiloftDeleteRecordParams, AgiloftDeleteResponse } from '@/tools/agiloft/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agiloftDeleteRecordTool: ToolConfig<AgiloftDeleteRecordParams, AgiloftDeleteResponse> =
|
||||
{
|
||||
id: 'agiloft_delete_record',
|
||||
name: 'Agiloft Delete Record',
|
||||
description: 'Delete a record from an Agiloft table.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
|
||||
},
|
||||
knowledgeBase: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Knowledge base name',
|
||||
},
|
||||
login: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft password',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table name (e.g., "contracts", "contacts.employees")',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the record to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => '/api/tools/agiloft/delete_record',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => ({
|
||||
instanceUrl: params.instanceUrl,
|
||||
knowledgeBase: params.knowledgeBase,
|
||||
login: params.login,
|
||||
password: params.password,
|
||||
table: params.table,
|
||||
recordId: params.recordId,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: data.success ?? true,
|
||||
output: data.output,
|
||||
...(data.error ? { error: data.error } : {}),
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'ID of the deleted record',
|
||||
},
|
||||
deleted: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the record was successfully deleted',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import type {
|
||||
AgiloftGetChoiceLineIdParams,
|
||||
AgiloftGetChoiceLineIdResponse,
|
||||
} from '@/tools/agiloft/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agiloftGetChoiceLineIdTool: ToolConfig<
|
||||
AgiloftGetChoiceLineIdParams,
|
||||
AgiloftGetChoiceLineIdResponse
|
||||
> = {
|
||||
id: 'agiloft_get_choice_line_id',
|
||||
name: 'Agiloft Get Choice Line ID',
|
||||
description:
|
||||
'Resolve the internal numeric ID of a choice-list value, for use in EWSelect WHERE clauses against choice fields.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
|
||||
},
|
||||
knowledgeBase: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Knowledge base name',
|
||||
},
|
||||
login: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft password',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table name (e.g., "case", "contracts")',
|
||||
},
|
||||
fieldName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Choice field name (e.g., "priority", "status")',
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Choice display value to resolve (e.g., "High", "Active")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => '/api/tools/agiloft/get_choice_line_id',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => ({
|
||||
instanceUrl: params.instanceUrl,
|
||||
knowledgeBase: params.knowledgeBase,
|
||||
login: params.login,
|
||||
password: params.password,
|
||||
table: params.table,
|
||||
fieldName: params.fieldName,
|
||||
value: params.value,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: data.success ?? true,
|
||||
output: data.output,
|
||||
...(data.error ? { error: data.error } : {}),
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
choiceLineId: {
|
||||
type: 'number',
|
||||
description: 'Internal numeric line ID of the choice value',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export { agiloftAttachFileTool } from '@/tools/agiloft/attach_file'
|
||||
export { agiloftAttachmentInfoTool } from '@/tools/agiloft/attachment_info'
|
||||
export { agiloftCreateRecordTool } from '@/tools/agiloft/create_record'
|
||||
export { agiloftDeleteRecordTool } from '@/tools/agiloft/delete_record'
|
||||
export { agiloftGetChoiceLineIdTool } from '@/tools/agiloft/get_choice_line_id'
|
||||
export { agiloftLockRecordTool } from '@/tools/agiloft/lock_record'
|
||||
export { agiloftReadRecordTool } from '@/tools/agiloft/read_record'
|
||||
export { agiloftRemoveAttachmentTool } from '@/tools/agiloft/remove_attachment'
|
||||
export { agiloftRetrieveAttachmentTool } from '@/tools/agiloft/retrieve_attachment'
|
||||
export { agiloftSavedSearchTool } from '@/tools/agiloft/saved_search'
|
||||
export { agiloftSearchRecordsTool } from '@/tools/agiloft/search_records'
|
||||
export { agiloftSelectRecordsTool } from '@/tools/agiloft/select_records'
|
||||
export * from '@/tools/agiloft/types'
|
||||
export { agiloftUpdateRecordTool } from '@/tools/agiloft/update_record'
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { AgiloftLockRecordParams, AgiloftLockResponse } from '@/tools/agiloft/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agiloftLockRecordTool: ToolConfig<AgiloftLockRecordParams, AgiloftLockResponse> = {
|
||||
id: 'agiloft_lock_record',
|
||||
name: 'Agiloft Lock Record',
|
||||
description: 'Lock, unlock, or check the lock status of an Agiloft record.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
|
||||
},
|
||||
knowledgeBase: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Knowledge base name',
|
||||
},
|
||||
login: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft password',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table name (e.g., "contracts")',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the record to lock, unlock, or check',
|
||||
},
|
||||
lockAction: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Action to perform: "lock", "unlock", or "check"',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => '/api/tools/agiloft/lock_record',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => ({
|
||||
instanceUrl: params.instanceUrl,
|
||||
knowledgeBase: params.knowledgeBase,
|
||||
login: params.login,
|
||||
password: params.password,
|
||||
table: params.table,
|
||||
recordId: params.recordId,
|
||||
lockAction: params.lockAction,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: data.success ?? true,
|
||||
output: data.output,
|
||||
...(data.error ? { error: data.error } : {}),
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'Record ID',
|
||||
},
|
||||
lockStatus: {
|
||||
type: 'string',
|
||||
description: 'Lock status (e.g., "LOCKED", "UNLOCKED")',
|
||||
},
|
||||
lockedBy: {
|
||||
type: 'string',
|
||||
description: 'Username of the user who locked the record',
|
||||
optional: true,
|
||||
},
|
||||
lockExpiresInMinutes: {
|
||||
type: 'number',
|
||||
description: 'Minutes until the lock expires',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { AgiloftReadRecordParams, AgiloftRecordResponse } from '@/tools/agiloft/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agiloftReadRecordTool: ToolConfig<AgiloftReadRecordParams, AgiloftRecordResponse> = {
|
||||
id: 'agiloft_read_record',
|
||||
name: 'Agiloft Read Record',
|
||||
description: 'Read a record by ID from an Agiloft table.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
|
||||
},
|
||||
knowledgeBase: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Knowledge base name',
|
||||
},
|
||||
login: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft password',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table name (e.g., "contracts", "contacts.employees")',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the record to read',
|
||||
},
|
||||
fields: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of field names to include in the response',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => '/api/tools/agiloft/read_record',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => ({
|
||||
instanceUrl: params.instanceUrl,
|
||||
knowledgeBase: params.knowledgeBase,
|
||||
login: params.login,
|
||||
password: params.password,
|
||||
table: params.table,
|
||||
recordId: params.recordId,
|
||||
fields: params.fields,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: data.success ?? true,
|
||||
output: data.output,
|
||||
...(data.error ? { error: data.error } : {}),
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'ID of the record',
|
||||
},
|
||||
fields: {
|
||||
type: 'json',
|
||||
description: 'Field values of the record',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import type {
|
||||
AgiloftRemoveAttachmentParams,
|
||||
AgiloftRemoveAttachmentResponse,
|
||||
} from '@/tools/agiloft/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agiloftRemoveAttachmentTool: ToolConfig<
|
||||
AgiloftRemoveAttachmentParams,
|
||||
AgiloftRemoveAttachmentResponse
|
||||
> = {
|
||||
id: 'agiloft_remove_attachment',
|
||||
name: 'Agiloft Remove Attachment',
|
||||
description: 'Remove an attached file from a field in an Agiloft record.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
|
||||
},
|
||||
knowledgeBase: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Knowledge base name',
|
||||
},
|
||||
login: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft password',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table name (e.g., "contracts")',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the record containing the attachment',
|
||||
},
|
||||
fieldName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Name of the attachment field',
|
||||
},
|
||||
position: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Position index of the file to remove (starting from 0)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => '/api/tools/agiloft/remove_attachment',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => ({
|
||||
instanceUrl: params.instanceUrl,
|
||||
knowledgeBase: params.knowledgeBase,
|
||||
login: params.login,
|
||||
password: params.password,
|
||||
table: params.table,
|
||||
recordId: params.recordId,
|
||||
fieldName: params.fieldName,
|
||||
position: params.position,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: data.success ?? true,
|
||||
output: data.output,
|
||||
...(data.error ? { error: data.error } : {}),
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
recordId: {
|
||||
type: 'string',
|
||||
description: 'ID of the record',
|
||||
},
|
||||
fieldName: {
|
||||
type: 'string',
|
||||
description: 'Name of the attachment field',
|
||||
},
|
||||
remainingAttachments: {
|
||||
type: 'number',
|
||||
description: 'Number of attachments remaining in the field after removal',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import type {
|
||||
AgiloftRetrieveAttachmentParams,
|
||||
AgiloftRetrieveAttachmentResponse,
|
||||
} from '@/tools/agiloft/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agiloftRetrieveAttachmentTool: ToolConfig<
|
||||
AgiloftRetrieveAttachmentParams,
|
||||
AgiloftRetrieveAttachmentResponse
|
||||
> = {
|
||||
id: 'agiloft_retrieve_attachment',
|
||||
name: 'Agiloft Retrieve Attachment',
|
||||
description: 'Download an attached file from an Agiloft record field.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
|
||||
},
|
||||
knowledgeBase: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Knowledge base name',
|
||||
},
|
||||
login: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft password',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table name (e.g., "contracts")',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the record containing the attachment',
|
||||
},
|
||||
fieldName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Name of the attachment field',
|
||||
},
|
||||
position: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Position index of the file in the field (starting from 0)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/agiloft/retrieve',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
instanceUrl: params.instanceUrl,
|
||||
knowledgeBase: params.knowledgeBase,
|
||||
login: params.login,
|
||||
password: params.password,
|
||||
table: params.table,
|
||||
recordId: params.recordId,
|
||||
fieldName: params.fieldName,
|
||||
position: params.position,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
file: { name: '', mimeType: '', data: '', size: 0 },
|
||||
},
|
||||
error: data.error || 'Failed to retrieve attachment',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
file: {
|
||||
name: data.output.file.name,
|
||||
mimeType: data.output.file.mimeType,
|
||||
data: data.output.file.data,
|
||||
size: data.output.file.size,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
file: {
|
||||
type: 'file',
|
||||
description: 'Downloaded attachment file',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import type { AgiloftSavedSearchParams, AgiloftSavedSearchResponse } from '@/tools/agiloft/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agiloftSavedSearchTool: ToolConfig<
|
||||
AgiloftSavedSearchParams,
|
||||
AgiloftSavedSearchResponse
|
||||
> = {
|
||||
id: 'agiloft_saved_search',
|
||||
name: 'Agiloft Saved Search',
|
||||
description: 'List saved searches defined for an Agiloft table.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
|
||||
},
|
||||
knowledgeBase: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Knowledge base name',
|
||||
},
|
||||
login: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft password',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table name to list saved searches for (e.g., "contracts")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => '/api/tools/agiloft/saved_search',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => ({
|
||||
instanceUrl: params.instanceUrl,
|
||||
knowledgeBase: params.knowledgeBase,
|
||||
login: params.login,
|
||||
password: params.password,
|
||||
table: params.table,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: data.success ?? true,
|
||||
output: data.output,
|
||||
...(data.error ? { error: data.error } : {}),
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
searches: {
|
||||
type: 'array',
|
||||
description: 'List of saved searches for the table',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', description: 'Saved search name' },
|
||||
label: { type: 'string', description: 'Saved search display label' },
|
||||
id: { type: 'number', description: 'Saved search database identifier' },
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Saved search description',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import type { AgiloftSearchRecordsParams, AgiloftSearchResponse } from '@/tools/agiloft/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agiloftSearchRecordsTool: ToolConfig<
|
||||
AgiloftSearchRecordsParams,
|
||||
AgiloftSearchResponse
|
||||
> = {
|
||||
id: 'agiloft_search_records',
|
||||
name: 'Agiloft Search Records',
|
||||
description: 'Search for records in an Agiloft table using a query.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
|
||||
},
|
||||
knowledgeBase: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Knowledge base name',
|
||||
},
|
||||
login: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft password',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table name to search in (e.g., "contracts", "contacts.employees")',
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Search query using Agiloft query syntax (e.g., "status=\'Active\'" or "company_name~=\'Acme\'")',
|
||||
},
|
||||
fields: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of field names to include in the results',
|
||||
},
|
||||
page: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Page number for paginated results (starting from 0)',
|
||||
},
|
||||
limit: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of records to return per page',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => '/api/tools/agiloft/search_records',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => ({
|
||||
instanceUrl: params.instanceUrl,
|
||||
knowledgeBase: params.knowledgeBase,
|
||||
login: params.login,
|
||||
password: params.password,
|
||||
table: params.table,
|
||||
query: params.query,
|
||||
fields: params.fields,
|
||||
page: params.page,
|
||||
limit: params.limit,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: data.success ?? true,
|
||||
output: data.output,
|
||||
...(data.error ? { error: data.error } : {}),
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
records: {
|
||||
type: 'json',
|
||||
description: 'Array of matching records with their field values',
|
||||
},
|
||||
totalCount: {
|
||||
type: 'number',
|
||||
description: 'Total number of matching records',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
description: 'Current page number',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Records per page',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { AgiloftSelectRecordsParams, AgiloftSelectResponse } from '@/tools/agiloft/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agiloftSelectRecordsTool: ToolConfig<
|
||||
AgiloftSelectRecordsParams,
|
||||
AgiloftSelectResponse
|
||||
> = {
|
||||
id: 'agiloft_select_records',
|
||||
name: 'Agiloft Select Records',
|
||||
description: 'Select record IDs matching a SQL WHERE clause from an Agiloft table.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
|
||||
},
|
||||
knowledgeBase: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Knowledge base name',
|
||||
},
|
||||
login: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft password',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table name (e.g., "contracts", "contacts.employees")',
|
||||
},
|
||||
where: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'SQL WHERE clause using database column names (e.g., "summary like \'%new%\'" or "assigned_person=\'John Doe\'")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => '/api/tools/agiloft/select_records',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => ({
|
||||
instanceUrl: params.instanceUrl,
|
||||
knowledgeBase: params.knowledgeBase,
|
||||
login: params.login,
|
||||
password: params.password,
|
||||
table: params.table,
|
||||
where: params.where,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: data.success ?? true,
|
||||
output: data.output,
|
||||
...(data.error ? { error: data.error } : {}),
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
recordIds: {
|
||||
type: 'array',
|
||||
description: 'Array of record IDs matching the query',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
totalCount: {
|
||||
type: 'number',
|
||||
description: 'Total number of matching records',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
/**
|
||||
* Base parameters shared by all Agiloft tools.
|
||||
* Agiloft authenticates via instance URL, KB name, and user credentials.
|
||||
*/
|
||||
export interface AgiloftBaseParams {
|
||||
instanceUrl: string
|
||||
knowledgeBase: string
|
||||
login: string
|
||||
password: string
|
||||
table: string
|
||||
}
|
||||
|
||||
export interface AgiloftCreateRecordParams extends AgiloftBaseParams {
|
||||
data: string
|
||||
}
|
||||
|
||||
export interface AgiloftReadRecordParams extends AgiloftBaseParams {
|
||||
recordId: string
|
||||
fields?: string
|
||||
}
|
||||
|
||||
export interface AgiloftUpdateRecordParams extends AgiloftBaseParams {
|
||||
recordId: string
|
||||
data: string
|
||||
}
|
||||
|
||||
export interface AgiloftDeleteRecordParams extends AgiloftBaseParams {
|
||||
recordId: string
|
||||
}
|
||||
|
||||
export interface AgiloftSearchRecordsParams extends AgiloftBaseParams {
|
||||
query: string
|
||||
fields?: string
|
||||
page?: string
|
||||
limit?: string
|
||||
}
|
||||
|
||||
export interface AgiloftSelectRecordsParams extends AgiloftBaseParams {
|
||||
where: string
|
||||
}
|
||||
|
||||
export type AgiloftSavedSearchParams = AgiloftBaseParams
|
||||
|
||||
export interface AgiloftAttachmentInfoParams extends AgiloftBaseParams {
|
||||
recordId: string
|
||||
fieldName: string
|
||||
}
|
||||
|
||||
export interface AgiloftLockRecordParams extends AgiloftBaseParams {
|
||||
recordId: string
|
||||
lockAction: 'lock' | 'unlock' | 'check'
|
||||
}
|
||||
|
||||
export interface AgiloftRecordResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string | null
|
||||
fields: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgiloftDeleteResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
deleted: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgiloftSearchResponse extends ToolResponse {
|
||||
output: {
|
||||
records: Record<string, unknown>[]
|
||||
totalCount: number
|
||||
page: number
|
||||
limit: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgiloftSelectResponse extends ToolResponse {
|
||||
output: {
|
||||
recordIds: string[]
|
||||
totalCount: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgiloftSavedSearchResponse extends ToolResponse {
|
||||
output: {
|
||||
searches: Array<{
|
||||
name: string
|
||||
label: string
|
||||
id: string | number
|
||||
description: string | null
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgiloftAttachmentInfoResponse extends ToolResponse {
|
||||
output: {
|
||||
attachments: Array<{
|
||||
position: number
|
||||
name: string
|
||||
size: number
|
||||
}>
|
||||
totalCount: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgiloftLockResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
lockStatus: string
|
||||
lockedBy: string | null
|
||||
lockExpiresInMinutes: number | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgiloftAttachFileParams extends AgiloftBaseParams {
|
||||
recordId: string
|
||||
fieldName: string
|
||||
file?: unknown
|
||||
fileName?: string
|
||||
}
|
||||
|
||||
export interface AgiloftAttachFileResponse extends ToolResponse {
|
||||
output: {
|
||||
recordId: string
|
||||
fieldName: string
|
||||
fileName: string
|
||||
totalAttachments: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgiloftRetrieveAttachmentParams extends AgiloftBaseParams {
|
||||
recordId: string
|
||||
fieldName: string
|
||||
position: string
|
||||
}
|
||||
|
||||
export interface AgiloftRetrieveAttachmentResponse extends ToolResponse {
|
||||
output: {
|
||||
file: {
|
||||
name: string
|
||||
mimeType: string
|
||||
data: string
|
||||
size: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgiloftRemoveAttachmentParams extends AgiloftBaseParams {
|
||||
recordId: string
|
||||
fieldName: string
|
||||
position: string
|
||||
}
|
||||
|
||||
export interface AgiloftRemoveAttachmentResponse extends ToolResponse {
|
||||
output: {
|
||||
recordId: string
|
||||
fieldName: string
|
||||
remainingAttachments: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface AgiloftGetChoiceLineIdParams extends AgiloftBaseParams {
|
||||
fieldName: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface AgiloftGetChoiceLineIdResponse extends ToolResponse {
|
||||
output: {
|
||||
choiceLineId: number | null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import type { AgiloftRecordResponse, AgiloftUpdateRecordParams } from '@/tools/agiloft/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const agiloftUpdateRecordTool: ToolConfig<AgiloftUpdateRecordParams, AgiloftRecordResponse> =
|
||||
{
|
||||
id: 'agiloft_update_record',
|
||||
name: 'Agiloft Update Record',
|
||||
description: 'Update an existing record in an Agiloft table.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
|
||||
},
|
||||
knowledgeBase: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Knowledge base name',
|
||||
},
|
||||
login: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Agiloft password',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table name (e.g., "contracts", "contacts.employees")',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the record to update',
|
||||
},
|
||||
data: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Updated field values as a JSON object (e.g., {"status": "Active", "priority": "High"})',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => '/api/tools/agiloft/update_record',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => ({
|
||||
instanceUrl: params.instanceUrl,
|
||||
knowledgeBase: params.knowledgeBase,
|
||||
login: params.login,
|
||||
password: params.password,
|
||||
table: params.table,
|
||||
recordId: params.recordId,
|
||||
data: params.data,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: data.success ?? true,
|
||||
output: data.output,
|
||||
...(data.error ? { error: data.error } : {}),
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'ID of the updated record',
|
||||
},
|
||||
fields: {
|
||||
type: 'json',
|
||||
description: 'Updated field values of the record',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { mockValidateUrlWithDNS, mockSecureFetch } = vi.hoisted(() => ({
|
||||
mockValidateUrlWithDNS: vi.fn(),
|
||||
mockSecureFetch: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/core/security/input-validation.server', () => ({
|
||||
validateUrlWithDNS: mockValidateUrlWithDNS,
|
||||
secureFetchWithPinnedIP: mockSecureFetch,
|
||||
}))
|
||||
|
||||
import { executeAgiloftRequest } from '@/tools/agiloft/utils.server'
|
||||
|
||||
const baseParams = {
|
||||
instanceUrl: 'https://example.agiloft.com',
|
||||
knowledgeBase: 'demo',
|
||||
login: 'admin',
|
||||
password: 'secret',
|
||||
table: 'contracts',
|
||||
}
|
||||
|
||||
function mockResponse(body: { ok?: boolean; status?: number; json?: unknown; text?: string }) {
|
||||
return {
|
||||
ok: body.ok ?? true,
|
||||
status: body.status ?? 200,
|
||||
statusText: '',
|
||||
headers: { get: () => null, getSetCookie: () => [], toRecord: () => ({}) },
|
||||
body: null,
|
||||
text: async () => body.text ?? '',
|
||||
json: async () => body.json ?? {},
|
||||
arrayBuffer: async () => new ArrayBuffer(0),
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockValidateUrlWithDNS.mockReset()
|
||||
mockSecureFetch.mockReset()
|
||||
mockValidateUrlWithDNS.mockResolvedValue({ isValid: true, resolvedIP: '203.0.113.10' })
|
||||
})
|
||||
|
||||
describe('executeAgiloftRequest', () => {
|
||||
it('resolves DNS once, logs in, runs the operation with the bearer token, then logs out — all pinned', async () => {
|
||||
mockSecureFetch
|
||||
.mockResolvedValueOnce(mockResponse({ json: { access_token: 'tok-1' } }))
|
||||
.mockResolvedValueOnce(mockResponse({ json: { id: 42, fields: { name: 'foo' } } }))
|
||||
.mockResolvedValueOnce(mockResponse({}))
|
||||
|
||||
const result = await executeAgiloftRequest(
|
||||
baseParams,
|
||||
(base) => ({
|
||||
url: `${base}/ewws/REST/demo/contracts/42`,
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' },
|
||||
}),
|
||||
async (response) => {
|
||||
const data = (await response.json()) as { id: number; fields: Record<string, unknown> }
|
||||
return {
|
||||
success: response.ok,
|
||||
output: { id: String(data.id), fields: data.fields },
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
expect(result).toEqual({ success: true, output: { id: '42', fields: { name: 'foo' } } })
|
||||
|
||||
expect(mockValidateUrlWithDNS).toHaveBeenCalledWith(
|
||||
'https://example.agiloft.com',
|
||||
'instanceUrl'
|
||||
)
|
||||
|
||||
const calls = mockSecureFetch.mock.calls
|
||||
expect(calls).toHaveLength(3)
|
||||
expect(calls[0][0]).toBe(
|
||||
'https://example.agiloft.com/ewws/EWLogin?$KB=demo&$login=admin&$password=secret'
|
||||
)
|
||||
expect(calls[1][0]).toBe('https://example.agiloft.com/ewws/REST/demo/contracts/42')
|
||||
expect(calls[2][0]).toBe('https://example.agiloft.com/ewws/EWLogout?$KB=demo')
|
||||
|
||||
for (const call of calls) {
|
||||
expect(call[1]).toBe('203.0.113.10')
|
||||
}
|
||||
expect(calls[1][2]).toMatchObject({
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json', Authorization: 'Bearer tok-1' },
|
||||
})
|
||||
})
|
||||
|
||||
it('still logs out when the operation throws', async () => {
|
||||
mockSecureFetch
|
||||
.mockResolvedValueOnce(mockResponse({ json: { access_token: 'tok-2' } }))
|
||||
.mockResolvedValueOnce(mockResponse({ ok: false, status: 500 }))
|
||||
.mockResolvedValueOnce(mockResponse({}))
|
||||
|
||||
await expect(
|
||||
executeAgiloftRequest(
|
||||
baseParams,
|
||||
(base) => ({ url: `${base}/ewws/REST/demo/contracts/42`, method: 'GET' }),
|
||||
async (response) => {
|
||||
if (!response.ok) throw new Error('operation failed')
|
||||
return { success: true, output: {} }
|
||||
}
|
||||
)
|
||||
).rejects.toThrow('operation failed')
|
||||
|
||||
expect(mockSecureFetch).toHaveBeenCalledTimes(3)
|
||||
expect(mockSecureFetch.mock.calls[2][0]).toContain('/ewws/EWLogout')
|
||||
})
|
||||
|
||||
it('swallows logout failures (best-effort)', async () => {
|
||||
mockSecureFetch
|
||||
.mockResolvedValueOnce(mockResponse({ json: { access_token: 'tok-3' } }))
|
||||
.mockResolvedValueOnce(mockResponse({ json: { ok: true } }))
|
||||
.mockRejectedValueOnce(new Error('logout network error'))
|
||||
|
||||
const result = await executeAgiloftRequest(
|
||||
baseParams,
|
||||
(base) => ({ url: `${base}/ewws/REST/demo/contracts/42`, method: 'GET' }),
|
||||
async () => ({ success: true, output: {} })
|
||||
)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('throws when login does not return an access token', async () => {
|
||||
mockSecureFetch.mockResolvedValueOnce(mockResponse({ json: {} }))
|
||||
|
||||
await expect(
|
||||
executeAgiloftRequest(
|
||||
baseParams,
|
||||
(base) => ({ url: `${base}/ewws/REST/demo/contracts/42`, method: 'GET' }),
|
||||
async () => ({ success: true, output: {} })
|
||||
)
|
||||
).rejects.toThrow('Agiloft login did not return an access token')
|
||||
|
||||
expect(mockSecureFetch).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('rejects an instance URL that resolves to a blocked IP without issuing any request', async () => {
|
||||
mockValidateUrlWithDNS.mockResolvedValue({
|
||||
isValid: false,
|
||||
error: 'instanceUrl resolves to a blocked IP address',
|
||||
})
|
||||
|
||||
await expect(
|
||||
executeAgiloftRequest(
|
||||
{ ...baseParams, instanceUrl: 'https://internal.attacker.com' },
|
||||
(base) => ({ url: `${base}/ewws/REST/demo/contracts/42`, method: 'GET' }),
|
||||
async () => ({ success: true, output: {} })
|
||||
)
|
||||
).rejects.toThrow(/blocked IP address/)
|
||||
|
||||
expect(mockSecureFetch).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,132 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import {
|
||||
type SecureFetchResponse,
|
||||
secureFetchWithPinnedIP,
|
||||
validateUrlWithDNS,
|
||||
} from '@/lib/core/security/input-validation.server'
|
||||
import type { AgiloftBaseParams } from '@/tools/agiloft/types'
|
||||
import type { HttpMethod, ToolResponse } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('AgiloftAuthServer')
|
||||
|
||||
interface AgiloftRequestConfig {
|
||||
url: string
|
||||
method: HttpMethod
|
||||
headers?: Record<string, string>
|
||||
body?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the Agiloft instance URL and resolves its DNS once, returning the
|
||||
* resolved IP so subsequent requests can pin to it. This prevents DNS-rebinding
|
||||
* (TOCTOU) SSRF where the hostname could resolve to a private IP on a later
|
||||
* lookup. Server-only — uses node:dns/promises.
|
||||
*/
|
||||
export async function resolveAgiloftInstance(instanceUrl: string): Promise<string> {
|
||||
const validation = await validateUrlWithDNS(instanceUrl, 'instanceUrl')
|
||||
if (!validation.isValid || !validation.resolvedIP) {
|
||||
throw new Error(validation.error || 'Invalid Agiloft instance URL')
|
||||
}
|
||||
return validation.resolvedIP
|
||||
}
|
||||
|
||||
/**
|
||||
* DNS-pinned variant of agiloftLogin. Requires a pre-resolved IP so the
|
||||
* connection cannot be steered to a different host between validation and
|
||||
* the actual TCP connection.
|
||||
*/
|
||||
export async function agiloftLoginPinned(
|
||||
params: AgiloftBaseParams,
|
||||
resolvedIP: string
|
||||
): Promise<string> {
|
||||
const base = params.instanceUrl.replace(/\/$/, '')
|
||||
const kb = encodeURIComponent(params.knowledgeBase)
|
||||
const login = encodeURIComponent(params.login)
|
||||
const password = encodeURIComponent(params.password)
|
||||
|
||||
const url = `${base}/ewws/EWLogin?$KB=${kb}&$login=${login}&$password=${password}`
|
||||
const response = await secureFetchWithPinnedIP(url, resolvedIP, { method: 'POST' })
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`Agiloft login failed: ${response.status} - ${errorText}`)
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { access_token?: string }
|
||||
const token = data.access_token
|
||||
|
||||
if (!token) {
|
||||
throw new Error('Agiloft login did not return an access token')
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
/**
|
||||
* DNS-pinned variant of agiloftLogout. Best-effort — failures are logged but
|
||||
* not thrown.
|
||||
*/
|
||||
export async function agiloftLogoutPinned(
|
||||
instanceUrl: string,
|
||||
knowledgeBase: string,
|
||||
token: string,
|
||||
resolvedIP: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
const base = instanceUrl.replace(/\/$/, '')
|
||||
const kb = encodeURIComponent(knowledgeBase)
|
||||
await secureFetchWithPinnedIP(`${base}/ewws/EWLogout?$KB=${kb}`, resolvedIP, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
} catch (error) {
|
||||
logger.warn('Agiloft logout failed (best-effort)', { error })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared wrapper that handles the full Agiloft auth lifecycle behind the
|
||||
* codebase's SSRF-safe fetch path. The instance URL is validated and resolved
|
||||
* to a concrete IP once via `validateUrlWithDNS` (which rejects hostnames that
|
||||
* resolve to private/reserved addresses), and every hop — login, the operation
|
||||
* request, and logout — is issued through `secureFetchWithPinnedIP` so the
|
||||
* connection is pinned to that validated IP. This defeats DNS-rebinding (TOCTOU)
|
||||
* SSRF where a hostname could resolve to an internal address on a later lookup.
|
||||
*
|
||||
* 1. Validate + resolve the instance URL once.
|
||||
* 2. Login to obtain a Bearer token.
|
||||
* 3. Execute the operation request with the token.
|
||||
* 4. Logout to clean up the session (best-effort).
|
||||
*
|
||||
* The `buildRequest` callback receives the base URL and returns the request
|
||||
* config. The `transformResponse` callback converts the raw response into the
|
||||
* tool's output format.
|
||||
*
|
||||
* Server-only — uses node:dns/promises and node:http(s) via the pinned fetch.
|
||||
*/
|
||||
export async function executeAgiloftRequest<R extends ToolResponse>(
|
||||
params: AgiloftBaseParams,
|
||||
buildRequest: (base: string) => AgiloftRequestConfig,
|
||||
transformResponse: (response: SecureFetchResponse) => Promise<R>
|
||||
): Promise<R> {
|
||||
const resolvedIP = await resolveAgiloftInstance(params.instanceUrl)
|
||||
const token = await agiloftLoginPinned(params, resolvedIP)
|
||||
const base = params.instanceUrl.replace(/\/$/, '')
|
||||
|
||||
try {
|
||||
const req = buildRequest(base)
|
||||
const response = await secureFetchWithPinnedIP(req.url, resolvedIP, {
|
||||
method: req.method,
|
||||
headers: {
|
||||
...req.headers,
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: req.body,
|
||||
})
|
||||
return await transformResponse(response)
|
||||
} finally {
|
||||
await agiloftLogoutPinned(params.instanceUrl, params.knowledgeBase, token, resolvedIP)
|
||||
}
|
||||
}
|
||||
|
||||
export type { SecureFetchResponse }
|
||||
@@ -0,0 +1,162 @@
|
||||
import type {
|
||||
AgiloftAttachmentInfoParams,
|
||||
AgiloftBaseParams,
|
||||
AgiloftDeleteRecordParams,
|
||||
AgiloftGetChoiceLineIdParams,
|
||||
AgiloftLockRecordParams,
|
||||
AgiloftReadRecordParams,
|
||||
AgiloftRemoveAttachmentParams,
|
||||
AgiloftRetrieveAttachmentParams,
|
||||
AgiloftSavedSearchParams,
|
||||
AgiloftSearchRecordsParams,
|
||||
AgiloftSelectRecordsParams,
|
||||
} from '@/tools/agiloft/types'
|
||||
import type { HttpMethod } from '@/tools/types'
|
||||
|
||||
/** URL builders (credential-free -- auth is via Bearer token header) */
|
||||
|
||||
function encodeTable(params: AgiloftBaseParams) {
|
||||
return {
|
||||
kb: encodeURIComponent(params.knowledgeBase),
|
||||
table: encodeURIComponent(params.table),
|
||||
}
|
||||
}
|
||||
|
||||
export function buildCreateRecordUrl(base: string, params: AgiloftBaseParams): string {
|
||||
const { kb, table } = encodeTable(params)
|
||||
return `${base}/ewws/REST/${kb}/${table}?$lang=en`
|
||||
}
|
||||
|
||||
export function buildReadRecordUrl(base: string, params: AgiloftReadRecordParams): string {
|
||||
const { kb, table } = encodeTable(params)
|
||||
const id = encodeURIComponent(params.recordId.trim())
|
||||
let url = `${base}/ewws/REST/${kb}/${table}/${id}?$lang=en`
|
||||
|
||||
if (params.fields) {
|
||||
const fieldList = params.fields
|
||||
.split(',')
|
||||
.map((f) => f.trim())
|
||||
.filter(Boolean)
|
||||
for (const field of fieldList) {
|
||||
url += `&$fields=${encodeURIComponent(field)}`
|
||||
}
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
export function buildUpdateRecordUrl(
|
||||
base: string,
|
||||
params: AgiloftBaseParams & { recordId: string }
|
||||
): string {
|
||||
const { kb, table } = encodeTable(params)
|
||||
const id = encodeURIComponent(params.recordId.trim())
|
||||
return `${base}/ewws/REST/${kb}/${table}/${id}?$lang=en`
|
||||
}
|
||||
|
||||
export function buildDeleteRecordUrl(base: string, params: AgiloftDeleteRecordParams): string {
|
||||
const { kb, table } = encodeTable(params)
|
||||
const id = encodeURIComponent(params.recordId.trim())
|
||||
return `${base}/ewws/REST/${kb}/${table}/${id}?$lang=en`
|
||||
}
|
||||
|
||||
function buildEwBaseQuery(params: AgiloftBaseParams): string {
|
||||
const { kb, table } = encodeTable(params)
|
||||
return `$KB=${kb}&$table=${table}&$lang=en`
|
||||
}
|
||||
|
||||
export function buildSearchRecordsUrl(base: string, params: AgiloftSearchRecordsParams): string {
|
||||
const query = encodeURIComponent(params.query)
|
||||
let url = `${base}/ewws/EWSearch/.json?${buildEwBaseQuery(params)}&query=${query}`
|
||||
|
||||
if (params.fields) {
|
||||
const fieldList = params.fields
|
||||
.split(',')
|
||||
.map((f) => f.trim())
|
||||
.filter(Boolean)
|
||||
for (const field of fieldList) {
|
||||
url += `&field=${encodeURIComponent(field)}`
|
||||
}
|
||||
}
|
||||
|
||||
if (params.page) {
|
||||
url += `&page=${encodeURIComponent(params.page)}`
|
||||
}
|
||||
if (params.limit) {
|
||||
url += `&limit=${encodeURIComponent(params.limit)}`
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
export function buildSelectRecordsUrl(base: string, params: AgiloftSelectRecordsParams): string {
|
||||
const where = encodeURIComponent(params.where)
|
||||
return `${base}/ewws/EWSelect/.json?${buildEwBaseQuery(params)}&where=${where}`
|
||||
}
|
||||
|
||||
export function buildSavedSearchUrl(base: string, params: AgiloftSavedSearchParams): string {
|
||||
return `${base}/ewws/EWSavedSearch/.json?${buildEwBaseQuery(params)}`
|
||||
}
|
||||
|
||||
export function buildRetrieveAttachmentUrl(
|
||||
base: string,
|
||||
params: AgiloftRetrieveAttachmentParams
|
||||
): string {
|
||||
const id = encodeURIComponent(params.recordId.trim())
|
||||
const field = encodeURIComponent(params.fieldName.trim())
|
||||
const position = encodeURIComponent(params.position)
|
||||
return `${base}/ewws/EWRetrieve?${buildEwBaseQuery(params)}&id=${id}&field=${field}&filePosition=${position}`
|
||||
}
|
||||
|
||||
export function buildRemoveAttachmentUrl(
|
||||
base: string,
|
||||
params: AgiloftRemoveAttachmentParams
|
||||
): string {
|
||||
const id = encodeURIComponent(params.recordId.trim())
|
||||
const field = encodeURIComponent(params.fieldName.trim())
|
||||
const position = encodeURIComponent(params.position)
|
||||
return `${base}/ewws/EWRemoveAttachment?${buildEwBaseQuery(params)}&id=${id}&field=${field}&filePosition=${position}`
|
||||
}
|
||||
|
||||
export function buildAttachmentInfoUrl(base: string, params: AgiloftAttachmentInfoParams): string {
|
||||
const id = encodeURIComponent(params.recordId.trim())
|
||||
const fieldName = encodeURIComponent(params.fieldName.trim())
|
||||
return `${base}/ewws/EWAttachInfo/.json?${buildEwBaseQuery(params)}&id=${id}&field=${fieldName}`
|
||||
}
|
||||
|
||||
export function buildLockRecordUrl(base: string, params: AgiloftLockRecordParams): string {
|
||||
const id = encodeURIComponent(params.recordId.trim())
|
||||
return `${base}/ewws/EWLock/.json?${buildEwBaseQuery(params)}&id=${id}`
|
||||
}
|
||||
|
||||
export function buildAttachFileUrl(
|
||||
base: string,
|
||||
params: AgiloftBaseParams & { recordId: string; fieldName: string },
|
||||
fileName: string
|
||||
): string {
|
||||
const { kb, table } = encodeTable(params)
|
||||
const recordId = encodeURIComponent(params.recordId.trim())
|
||||
const fieldName = encodeURIComponent(params.fieldName.trim())
|
||||
const encodedFileName = encodeURIComponent(fileName)
|
||||
return `${base}/ewws/EWAttach?$KB=${kb}&$table=${table}&$lang=en&id=${recordId}&field=${fieldName}&fileName=${encodedFileName}`
|
||||
}
|
||||
|
||||
export function buildGetChoiceLineIdUrl(
|
||||
base: string,
|
||||
params: AgiloftGetChoiceLineIdParams
|
||||
): string {
|
||||
const field = encodeURIComponent(params.fieldName.trim())
|
||||
const value = encodeURIComponent(params.value.trim())
|
||||
return `${base}/ewws/EWGetChoiceLineId/.json?${buildEwBaseQuery(params)}&field=${field}&value=${value}`
|
||||
}
|
||||
|
||||
export function getLockHttpMethod(lockAction: string): HttpMethod {
|
||||
switch (lockAction) {
|
||||
case 'lock':
|
||||
return 'PUT'
|
||||
case 'unlock':
|
||||
return 'DELETE'
|
||||
default:
|
||||
return 'GET'
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user