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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
+132
View File
@@ -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',
},
},
}
+106
View File
@@ -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',
},
},
}
+84
View File
@@ -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',
},
},
}
+83
View File
@@ -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,
},
},
}
+14
View File
@@ -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'
+99
View File
@@ -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,
},
},
}
+89
View File
@@ -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',
},
},
}
+106
View File
@@ -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',
},
},
}
+87
View 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,
},
},
},
},
},
}
+115
View File
@@ -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',
},
},
}
+89
View File
@@ -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',
},
},
}
+173
View File
@@ -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
}
}
+91
View File
@@ -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',
},
},
}
+158
View File
@@ -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()
})
})
+132
View File
@@ -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 }
+162
View File
@@ -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'
}
}