chore: import upstream snapshot with attribution
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) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
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) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseAssociateParams,
|
||||
DataverseAssociateResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseAssociate')
|
||||
|
||||
export const dataverseAssociateTool: ToolConfig<
|
||||
DataverseAssociateParams,
|
||||
DataverseAssociateResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_associate',
|
||||
name: 'Associate Microsoft Dataverse Records',
|
||||
description:
|
||||
'Associate two records in Microsoft Dataverse via a navigation property. Creates a relationship between a source record and a target record. Supports both collection-valued (POST) and single-valued (PUT) navigation properties.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Source entity set name (e.g., accounts)',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Source record GUID',
|
||||
},
|
||||
navigationProperty: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Navigation property name (e.g., contact_customer_accounts for collection-valued, or parentcustomerid_account for single-valued)',
|
||||
},
|
||||
targetEntitySetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Target entity set name (e.g., contacts)',
|
||||
},
|
||||
targetRecordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Target record GUID to associate',
|
||||
},
|
||||
navigationType: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Type of navigation property: "collection" (default, uses POST) or "single" (uses PUT for lookup fields)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = getDataverseBaseUrl(params.environmentUrl)
|
||||
return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}(${params.recordId.trim()})/${params.navigationProperty.trim()}/$ref`
|
||||
},
|
||||
method: (params) => (params.navigationType === 'single' ? 'PUT' : 'POST'),
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const baseUrl = getDataverseBaseUrl(params.environmentUrl)
|
||||
return {
|
||||
'@odata.id': `${baseUrl}/api/data/v9.2/${params.targetEntitySetName.trim()}(${params.targetRecordId.trim()})`,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: DataverseAssociateParams) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse associate failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
entitySetName: params?.entitySetName ?? '',
|
||||
recordId: params?.recordId ?? '',
|
||||
navigationProperty: params?.navigationProperty ?? '',
|
||||
targetEntitySetName: params?.targetEntitySetName ?? '',
|
||||
targetRecordId: params?.targetRecordId ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether the association was created successfully' },
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
description: 'Source entity set name used in the association',
|
||||
},
|
||||
recordId: { type: 'string', description: 'Source record GUID that was associated' },
|
||||
navigationProperty: {
|
||||
type: 'string',
|
||||
description: 'Navigation property used for the association',
|
||||
},
|
||||
targetEntitySetName: {
|
||||
type: 'string',
|
||||
description: 'Target entity set name used in the association',
|
||||
},
|
||||
targetRecordId: { type: 'string', description: 'Target record GUID that was associated' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseCreateMultipleParams,
|
||||
DataverseCreateMultipleResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseCreateMultiple')
|
||||
|
||||
export const dataverseCreateMultipleTool: ToolConfig<
|
||||
DataverseCreateMultipleParams,
|
||||
DataverseCreateMultipleResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_create_multiple',
|
||||
name: 'Create Multiple Microsoft Dataverse Records',
|
||||
description:
|
||||
'Create multiple records of the same table type in a single request. Each record in the Targets array must include an @odata.type annotation. Recommended batch size: 100-1000 records for standard tables.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Entity set name (plural table name, e.g., accounts, contacts)',
|
||||
},
|
||||
entityLogicalName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Table logical name for @odata.type annotation (e.g., account, contact). Used to set Microsoft.Dynamics.CRM.{entityLogicalName} on each record.',
|
||||
},
|
||||
records: {
|
||||
type: 'object',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Array of record objects to create. Each record should contain column logical names as keys. The @odata.type annotation is added automatically.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = getDataverseBaseUrl(params.environmentUrl)
|
||||
return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}/Microsoft.Dynamics.CRM.CreateMultiple`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
let records = params.records
|
||||
if (typeof records === 'string') {
|
||||
try {
|
||||
records = JSON.parse(records)
|
||||
} catch {
|
||||
throw new Error('Invalid JSON format for records array')
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(records)) {
|
||||
throw new Error('Records must be an array of objects')
|
||||
}
|
||||
const entityLogicalName = params.entityLogicalName.trim()
|
||||
const targets = records.map((record: Record<string, unknown>) => ({
|
||||
...record,
|
||||
'@odata.type': `Microsoft.Dynamics.CRM.${entityLogicalName}`,
|
||||
}))
|
||||
return { Targets: targets }
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse create multiple failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const data = await response.json().catch(() => null)
|
||||
const ids = data?.Ids ?? []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ids,
|
||||
count: ids.length,
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
ids: {
|
||||
type: 'array',
|
||||
description: 'Array of GUIDs for the created records',
|
||||
items: {
|
||||
type: 'string',
|
||||
description: 'GUID of a created record',
|
||||
},
|
||||
},
|
||||
count: { type: 'number', description: 'Number of records created' },
|
||||
success: { type: 'boolean', description: 'Whether all records were created successfully' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseCreateRecordParams,
|
||||
DataverseCreateRecordResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import { DATAVERSE_RECORD_OUTPUT } from '@/tools/microsoft_dataverse/types'
|
||||
import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseCreateRecord')
|
||||
|
||||
export const dataverseCreateRecordTool: ToolConfig<
|
||||
DataverseCreateRecordParams,
|
||||
DataverseCreateRecordResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_create_record',
|
||||
name: 'Create Microsoft Dataverse Record',
|
||||
description:
|
||||
'Create a new record in a Microsoft Dataverse table. Requires the entity set name (plural table name) and record data as a JSON object.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Entity set name (plural table name, e.g., accounts, contacts)',
|
||||
},
|
||||
data: {
|
||||
type: 'object',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Record data as a JSON object with column names as keys',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = getDataverseBaseUrl(params.environmentUrl)
|
||||
return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
Prefer: 'return=representation',
|
||||
}),
|
||||
body: (params) => {
|
||||
let data = params.data
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
data = JSON.parse(data)
|
||||
} catch {
|
||||
throw new Error('Invalid JSON format for record data')
|
||||
}
|
||||
}
|
||||
return data
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse create record failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const data = await response.json().catch(() => null)
|
||||
|
||||
let recordId = ''
|
||||
if (data) {
|
||||
const idKey = Object.keys(data).find((k) => k.endsWith('id') && !k.startsWith('@'))
|
||||
recordId = idKey ? String(data[idKey]) : ''
|
||||
}
|
||||
|
||||
if (!recordId) {
|
||||
const entityIdHeader = response.headers.get('OData-EntityId')
|
||||
if (entityIdHeader) {
|
||||
const match = entityIdHeader.match(/\(([^)]+)\)/)
|
||||
if (match) {
|
||||
recordId = match[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
recordId,
|
||||
record: data ?? {},
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
recordId: { type: 'string', description: 'The ID of the created record', optional: true },
|
||||
record: { ...DATAVERSE_RECORD_OUTPUT, optional: true },
|
||||
success: { type: 'boolean', description: 'Whether the record was created successfully' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseDeleteRecordParams,
|
||||
DataverseDeleteRecordResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseDeleteRecord')
|
||||
|
||||
export const dataverseDeleteRecordTool: ToolConfig<
|
||||
DataverseDeleteRecordParams,
|
||||
DataverseDeleteRecordResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_delete_record',
|
||||
name: 'Delete Microsoft Dataverse Record',
|
||||
description: 'Delete a record from a Microsoft Dataverse table by its ID.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Entity set name (plural table name, e.g., accounts, contacts)',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The unique identifier (GUID) of the record to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = getDataverseBaseUrl(params.environmentUrl)
|
||||
return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}(${params.recordId.trim()})`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: DataverseDeleteRecordParams) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse delete record failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
recordId: params?.recordId ?? '',
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
recordId: { type: 'string', description: 'The ID of the deleted record' },
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseDisassociateParams,
|
||||
DataverseDisassociateResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseDisassociate')
|
||||
|
||||
export const dataverseDisassociateTool: ToolConfig<
|
||||
DataverseDisassociateParams,
|
||||
DataverseDisassociateResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_disassociate',
|
||||
name: 'Disassociate Microsoft Dataverse Records',
|
||||
description:
|
||||
'Remove an association between two records in Microsoft Dataverse. For collection-valued navigation properties, provide the target record ID. For single-valued navigation properties, only the navigation property name is needed.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Source entity set name (e.g., accounts)',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Source record GUID',
|
||||
},
|
||||
navigationProperty: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Navigation property name (e.g., contact_customer_accounts for collection-valued, or parentcustomerid_account for single-valued)',
|
||||
},
|
||||
targetRecordId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Target record GUID (required for collection-valued navigation properties, omit for single-valued)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = getDataverseBaseUrl(params.environmentUrl)
|
||||
const entitySetName = params.entitySetName.trim()
|
||||
const recordId = params.recordId.trim()
|
||||
const navigationProperty = params.navigationProperty.trim()
|
||||
if (params.targetRecordId) {
|
||||
return `${baseUrl}/api/data/v9.2/${entitySetName}(${recordId})/${navigationProperty}(${params.targetRecordId.trim()})/$ref`
|
||||
}
|
||||
return `${baseUrl}/api/data/v9.2/${entitySetName}(${recordId})/${navigationProperty}/$ref`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: DataverseDisassociateParams) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse disassociate failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
entitySetName: params?.entitySetName ?? '',
|
||||
recordId: params?.recordId ?? '',
|
||||
navigationProperty: params?.navigationProperty ?? '',
|
||||
...(params?.targetRecordId ? { targetRecordId: params.targetRecordId } : {}),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the disassociation was completed successfully',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
description: 'Source entity set name used in the disassociation',
|
||||
},
|
||||
recordId: { type: 'string', description: 'Source record GUID that was disassociated' },
|
||||
navigationProperty: {
|
||||
type: 'string',
|
||||
description: 'Navigation property used for the disassociation',
|
||||
},
|
||||
targetRecordId: {
|
||||
type: 'string',
|
||||
description: 'Target record GUID that was disassociated',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseDownloadFileParams,
|
||||
DataverseDownloadFileResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseDownloadFile')
|
||||
|
||||
export const dataverseDownloadFileTool: ToolConfig<
|
||||
DataverseDownloadFileParams,
|
||||
DataverseDownloadFileResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_download_file',
|
||||
name: 'Download File from Microsoft Dataverse',
|
||||
description:
|
||||
'Download a file from a file or image column on a Dataverse record. Stores the file in execution storage and returns a file reference, plus the base64 content and metadata directly.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Entity set name (plural table name, e.g., accounts, contacts)',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Record GUID to download the file from',
|
||||
},
|
||||
fileColumn: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'File or image column logical name (e.g., entityimage, cr_document)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = getDataverseBaseUrl(params.environmentUrl)
|
||||
return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}(${params.recordId.trim()})/${params.fileColumn.trim()}/$value`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: DataverseDownloadFileParams) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse download file failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const fileName = response.headers.get('x-ms-file-name') || 'download'
|
||||
const fileSize = response.headers.get('x-ms-file-size') ?? ''
|
||||
const mimeType =
|
||||
response.headers.get('mimetype') ??
|
||||
response.headers.get('content-type') ??
|
||||
'application/octet-stream'
|
||||
|
||||
const buffer = await response.arrayBuffer()
|
||||
const base64Content = Buffer.from(buffer).toString('base64')
|
||||
const resolvedSize = fileSize ? Number.parseInt(fileSize, 10) : buffer.byteLength
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
file: {
|
||||
name: fileName,
|
||||
mimeType,
|
||||
data: base64Content,
|
||||
size: resolvedSize,
|
||||
},
|
||||
fileContent: base64Content,
|
||||
fileName,
|
||||
fileSize: resolvedSize,
|
||||
mimeType,
|
||||
fileColumn: params?.fileColumn ?? '',
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
file: { type: 'file', description: 'Downloaded file stored in execution files' },
|
||||
fileContent: { type: 'string', description: 'Base64-encoded file content' },
|
||||
fileName: { type: 'string', description: 'Name of the downloaded file', optional: true },
|
||||
fileSize: { type: 'number', description: 'File size in bytes' },
|
||||
mimeType: { type: 'string', description: 'MIME type of the file', optional: true },
|
||||
fileColumn: { type: 'string', description: 'File column the file was downloaded from' },
|
||||
success: { type: 'boolean', description: 'Whether the file was downloaded successfully' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseExecuteActionParams,
|
||||
DataverseExecuteActionResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseExecuteAction')
|
||||
|
||||
export const dataverseExecuteActionTool: ToolConfig<
|
||||
DataverseExecuteActionParams,
|
||||
DataverseExecuteActionResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_execute_action',
|
||||
name: 'Execute Microsoft Dataverse Action',
|
||||
description:
|
||||
'Execute a bound or unbound Dataverse action. Actions perform operations with side effects (e.g., Merge, GrantAccess, SendEmail, QualifyLead). For bound actions, provide the entity set name and record ID.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
actionName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Action name (e.g., Merge, GrantAccess, SendEmail). Do not include the Microsoft.Dynamics.CRM. namespace prefix for unbound actions.',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Entity set name for bound actions (e.g., accounts). Leave empty for unbound actions.',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Record GUID for bound actions. Leave empty for unbound or collection-bound actions.',
|
||||
},
|
||||
parameters: {
|
||||
type: 'object',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Action parameters as a JSON object. For entity references, include @odata.type annotation (e.g., {"Target": {"@odata.type": "Microsoft.Dynamics.CRM.account", "accountid": "..."}})',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = getDataverseBaseUrl(params.environmentUrl)
|
||||
const actionName = params.actionName.trim()
|
||||
if (params.entitySetName) {
|
||||
const entitySetName = params.entitySetName.trim()
|
||||
if (params.recordId) {
|
||||
return `${baseUrl}/api/data/v9.2/${entitySetName}(${params.recordId.trim()})/Microsoft.Dynamics.CRM.${actionName}`
|
||||
}
|
||||
return `${baseUrl}/api/data/v9.2/${entitySetName}/Microsoft.Dynamics.CRM.${actionName}`
|
||||
}
|
||||
return `${baseUrl}/api/data/v9.2/${actionName}`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
if (!params.parameters) return {}
|
||||
let data = params.parameters
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
data = JSON.parse(data)
|
||||
} catch {
|
||||
throw new Error('Invalid JSON format for action parameters')
|
||||
}
|
||||
}
|
||||
return data
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse execute action failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const data = response.status === 204 ? null : await response.json().catch(() => null)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
result: data,
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
result: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Action response data. Structure varies by action. Null for actions that return 204 No Content.',
|
||||
optional: true,
|
||||
},
|
||||
success: { type: 'boolean', description: 'Whether the action executed successfully' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseExecuteFunctionParams,
|
||||
DataverseExecuteFunctionResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseExecuteFunction')
|
||||
|
||||
export const dataverseExecuteFunctionTool: ToolConfig<
|
||||
DataverseExecuteFunctionParams,
|
||||
DataverseExecuteFunctionResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_execute_function',
|
||||
name: 'Execute Microsoft Dataverse Function',
|
||||
description:
|
||||
'Execute a bound or unbound Dataverse function. Functions are read-only operations (e.g., RetrievePrincipalAccess, RetrieveTotalRecordCount, InitializeFrom). For bound functions, provide the entity set name and record ID.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
functionName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Function name (e.g., RetrievePrincipalAccess, RetrieveTotalRecordCount). Do not include the Microsoft.Dynamics.CRM. namespace prefix for unbound functions.',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Entity set name for bound functions (e.g., systemusers). Leave empty for unbound functions.',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Record GUID for bound functions. Leave empty for unbound functions.',
|
||||
},
|
||||
parameters: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Function parameters for the URL. Simple values can be inlined (e.g., "LocalizedStandardName=\'Pacific Standard Time\',LocaleId=1033"), but values with reserved characters (/ < > * % & : \\ ? +) must use parameter aliases: put the alias assignment in parentheses and the alias-to-value bindings after a "?", e.g. "LocalizedStandardName=@p1,LocaleId=@p2?@p1=\'Pacific Standard Time\'&@p2=1033". Do not include the enclosing parentheses yourself.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = getDataverseBaseUrl(params.environmentUrl)
|
||||
const functionName = params.functionName.trim()
|
||||
const rawParams = params.parameters?.trim() ?? ''
|
||||
const separatorIndex = rawParams.indexOf('?')
|
||||
const inlineParams = separatorIndex === -1 ? rawParams : rawParams.slice(0, separatorIndex)
|
||||
const aliasQuery = separatorIndex === -1 ? '' : rawParams.slice(separatorIndex + 1)
|
||||
const paramStr = inlineParams ? `(${inlineParams})` : '()'
|
||||
const querySuffix = aliasQuery ? `?${aliasQuery}` : ''
|
||||
if (params.entitySetName) {
|
||||
const entitySetName = params.entitySetName.trim()
|
||||
if (params.recordId) {
|
||||
return `${baseUrl}/api/data/v9.2/${entitySetName}(${params.recordId.trim()})/Microsoft.Dynamics.CRM.${functionName}${paramStr}${querySuffix}`
|
||||
}
|
||||
return `${baseUrl}/api/data/v9.2/${entitySetName}/Microsoft.Dynamics.CRM.${functionName}${paramStr}${querySuffix}`
|
||||
}
|
||||
return `${baseUrl}/api/data/v9.2/${functionName}${paramStr}${querySuffix}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse execute function failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const data = await response.json().catch(() => null)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
result: data,
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
result: {
|
||||
type: 'object',
|
||||
description: 'Function response data. Structure varies by function.',
|
||||
optional: true,
|
||||
},
|
||||
success: { type: 'boolean', description: 'Whether the function executed successfully' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseFetchXmlQueryParams,
|
||||
DataverseFetchXmlQueryResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import { DATAVERSE_RECORDS_ARRAY_OUTPUT } from '@/tools/microsoft_dataverse/types'
|
||||
import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseFetchXmlQuery')
|
||||
|
||||
export const dataverseFetchXmlQueryTool: ToolConfig<
|
||||
DataverseFetchXmlQueryParams,
|
||||
DataverseFetchXmlQueryResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_fetchxml_query',
|
||||
name: 'FetchXML Query Microsoft Dataverse',
|
||||
description:
|
||||
'Execute a FetchXML query against a Microsoft Dataverse table. FetchXML supports aggregation, grouping, linked-entity joins, and complex filtering beyond OData capabilities.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Entity set name (plural table name, e.g., accounts, contacts)',
|
||||
},
|
||||
fetchXml: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'FetchXML query string. Must include <fetch> root element and <entity> child element matching the table logical name.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = getDataverseBaseUrl(params.environmentUrl)
|
||||
const encodedFetchXml = encodeURIComponent(params.fetchXml)
|
||||
return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}?fetchXml=${encodedFetchXml}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
Prefer: 'odata.include-annotations="*"',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse FetchXML query failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const records = data.value ?? []
|
||||
const fetchXmlPagingCookie = data['@Microsoft.Dynamics.CRM.fetchxmlpagingcookie'] ?? null
|
||||
const moreRecords = data['@Microsoft.Dynamics.CRM.morerecords'] ?? false
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
records,
|
||||
count: records.length,
|
||||
fetchXmlPagingCookie,
|
||||
moreRecords,
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
records: DATAVERSE_RECORDS_ARRAY_OUTPUT,
|
||||
count: { type: 'number', description: 'Number of records returned in the current page' },
|
||||
fetchXmlPagingCookie: {
|
||||
type: 'string',
|
||||
description: 'Paging cookie for retrieving the next page of results',
|
||||
optional: true,
|
||||
},
|
||||
moreRecords: {
|
||||
type: 'boolean',
|
||||
description: 'Whether more records are available beyond the current page',
|
||||
},
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseGetEntityMetadataParams,
|
||||
DataverseGetEntityMetadataResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseGetEntityMetadata')
|
||||
|
||||
const DEFAULT_ATTRIBUTE_SELECT =
|
||||
'LogicalName,DisplayName,AttributeType,RequiredLevel,IsPrimaryId,IsPrimaryName'
|
||||
|
||||
export const dataverseGetEntityMetadataTool: ToolConfig<
|
||||
DataverseGetEntityMetadataParams,
|
||||
DataverseGetEntityMetadataResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_get_entity_metadata',
|
||||
name: 'Get Microsoft Dataverse Table Metadata',
|
||||
description:
|
||||
'Retrieve table (entity) and column (attribute) definitions for a Microsoft Dataverse table by its singular logical name. Use this to look up the correct entity set name and column logical names before building record data for other operations.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
entityLogicalName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Singular table logical name to look up (e.g., account, contact)',
|
||||
},
|
||||
select: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Comma-separated table metadata properties to return (OData $select, e.g., LogicalName,DisplayName,EntitySetName,PrimaryIdAttribute)',
|
||||
},
|
||||
includeAttributes: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Set to "true" to also return the column (attribute) definitions for the table',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = getDataverseBaseUrl(params.environmentUrl)
|
||||
// OData string literals escape embedded single quotes by doubling them - URL-encoding the
|
||||
// quote alone isn't sufficient since Dataverse URL-decodes the request before parsing the
|
||||
// OData key predicate, so a percent-encoded quote would still land as a literal delimiter.
|
||||
const entityLogicalName = params.entityLogicalName.trim().replace(/'/g, "''")
|
||||
const queryParts: string[] = []
|
||||
if (params.select) queryParts.push(`$select=${encodeURIComponent(params.select)}`)
|
||||
if (params.includeAttributes === 'true') {
|
||||
queryParts.push(
|
||||
`$expand=${encodeURIComponent(`Attributes($select=${DEFAULT_ATTRIBUTE_SELECT})`)}`
|
||||
)
|
||||
}
|
||||
const query = queryParts.length > 0 ? `?${queryParts.join('&')}` : ''
|
||||
return `${baseUrl}/api/data/v9.2/EntityDefinitions(LogicalName='${entityLogicalName}')${query}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse get entity metadata failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const displayName = data?.DisplayName?.UserLocalizedLabel?.Label ?? null
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
entitySetName: data?.EntitySetName ?? null,
|
||||
logicalName: data?.LogicalName ?? null,
|
||||
displayName,
|
||||
primaryIdAttribute: data?.PrimaryIdAttribute ?? null,
|
||||
primaryNameAttribute: data?.PrimaryNameAttribute ?? null,
|
||||
attributes: data?.Attributes ?? [],
|
||||
metadata: data ?? {},
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
description: 'The entity set name (plural, used in Web API URLs) for this table',
|
||||
optional: true,
|
||||
},
|
||||
logicalName: {
|
||||
type: 'string',
|
||||
description: 'The singular logical name of the table',
|
||||
optional: true,
|
||||
},
|
||||
displayName: {
|
||||
type: 'string',
|
||||
description: 'The localized display name of the table',
|
||||
optional: true,
|
||||
},
|
||||
primaryIdAttribute: {
|
||||
type: 'string',
|
||||
description: 'The logical name of the primary key column',
|
||||
optional: true,
|
||||
},
|
||||
primaryNameAttribute: {
|
||||
type: 'string',
|
||||
description: 'The logical name of the primary name (title) column',
|
||||
optional: true,
|
||||
},
|
||||
attributes: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Column (attribute) definitions for the table (only populated when includeAttributes is "true")',
|
||||
items: {
|
||||
type: 'object',
|
||||
description:
|
||||
'A single column definition (logical name, display name, type, requirement level)',
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'The full raw entity metadata response from Dataverse',
|
||||
},
|
||||
success: { type: 'boolean', description: 'Whether the metadata was retrieved successfully' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseGetRecordParams,
|
||||
DataverseGetRecordResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import { DATAVERSE_RECORD_OUTPUT } from '@/tools/microsoft_dataverse/types'
|
||||
import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseGetRecord')
|
||||
|
||||
export const dataverseGetRecordTool: ToolConfig<
|
||||
DataverseGetRecordParams,
|
||||
DataverseGetRecordResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_get_record',
|
||||
name: 'Get Microsoft Dataverse Record',
|
||||
description:
|
||||
'Retrieve a single record from a Microsoft Dataverse table by its ID. Supports $select and $expand OData query options.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Entity set name (plural table name, e.g., accounts, contacts)',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The unique identifier (GUID) of the record to retrieve',
|
||||
},
|
||||
select: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of columns to return (OData $select)',
|
||||
},
|
||||
expand: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Navigation properties to expand (OData $expand)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = getDataverseBaseUrl(params.environmentUrl)
|
||||
const queryParts: string[] = []
|
||||
if (params.select) queryParts.push(`$select=${encodeURIComponent(params.select)}`)
|
||||
if (params.expand) queryParts.push(`$expand=${encodeURIComponent(params.expand)}`)
|
||||
const query = queryParts.length > 0 ? `?${queryParts.join('&')}` : ''
|
||||
return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}(${params.recordId.trim()})${query}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
Prefer: 'odata.include-annotations="*"',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse get record failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const idKey = Object.keys(data).find((k) => k.endsWith('id') && !k.startsWith('@'))
|
||||
const recordId = idKey ? String(data[idKey]) : ''
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
record: data,
|
||||
recordId,
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
record: DATAVERSE_RECORD_OUTPUT,
|
||||
recordId: {
|
||||
type: 'string',
|
||||
description: 'The record primary key ID (auto-detected from response)',
|
||||
optional: true,
|
||||
},
|
||||
success: { type: 'boolean', description: 'Whether the record was retrieved successfully' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export { dataverseAssociateTool } from './associate'
|
||||
export { dataverseCreateMultipleTool } from './create_multiple'
|
||||
export { dataverseCreateRecordTool } from './create_record'
|
||||
export { dataverseDeleteRecordTool } from './delete_record'
|
||||
export { dataverseDisassociateTool } from './disassociate'
|
||||
export { dataverseDownloadFileTool } from './download_file'
|
||||
export { dataverseExecuteActionTool } from './execute_action'
|
||||
export { dataverseExecuteFunctionTool } from './execute_function'
|
||||
export { dataverseFetchXmlQueryTool } from './fetchxml_query'
|
||||
export { dataverseGetEntityMetadataTool } from './get_entity_metadata'
|
||||
export { dataverseGetRecordTool } from './get_record'
|
||||
export { dataverseListRecordsTool } from './list_records'
|
||||
export { dataverseSearchTool } from './search'
|
||||
export { dataverseUpdateMultipleTool } from './update_multiple'
|
||||
export { dataverseUpdateRecordTool } from './update_record'
|
||||
export { dataverseUploadFileTool } from './upload_file'
|
||||
export { dataverseUpsertRecordTool } from './upsert_record'
|
||||
export { dataverseWhoAmITool } from './whoami'
|
||||
@@ -0,0 +1,155 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseListRecordsParams,
|
||||
DataverseListRecordsResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import { DATAVERSE_RECORDS_ARRAY_OUTPUT } from '@/tools/microsoft_dataverse/types'
|
||||
import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseListRecords')
|
||||
|
||||
export const dataverseListRecordsTool: ToolConfig<
|
||||
DataverseListRecordsParams,
|
||||
DataverseListRecordsResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_list_records',
|
||||
name: 'List Microsoft Dataverse Records',
|
||||
description:
|
||||
'Query and list records from a Microsoft Dataverse table. Supports OData query options for filtering, selecting columns, ordering, and pagination.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Entity set name (plural table name, e.g., accounts, contacts)',
|
||||
},
|
||||
select: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of columns to return (OData $select)',
|
||||
},
|
||||
filter: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'OData $filter expression (e.g., statecode eq 0)',
|
||||
},
|
||||
orderBy: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'OData $orderby expression (e.g., name asc, createdon desc)',
|
||||
},
|
||||
top: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of records to return (OData $top)',
|
||||
},
|
||||
expand: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Navigation properties to expand (OData $expand)',
|
||||
},
|
||||
count: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Set to "true" to include total record count in response (OData $count)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = getDataverseBaseUrl(params.environmentUrl)
|
||||
const queryParts: string[] = []
|
||||
if (params.select) queryParts.push(`$select=${encodeURIComponent(params.select)}`)
|
||||
if (params.filter) queryParts.push(`$filter=${encodeURIComponent(params.filter)}`)
|
||||
if (params.orderBy) queryParts.push(`$orderby=${encodeURIComponent(params.orderBy)}`)
|
||||
if (params.top) queryParts.push(`$top=${params.top}`)
|
||||
if (params.expand) queryParts.push(`$expand=${encodeURIComponent(params.expand)}`)
|
||||
if (params.count) queryParts.push(`$count=${params.count}`)
|
||||
const query = queryParts.length > 0 ? `?${queryParts.join('&')}` : ''
|
||||
return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}${query}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => {
|
||||
// Dataverse ignores $top entirely when Prefer: odata.maxpagesize is also sent, so the
|
||||
// page-size preference is only applied when the caller hasn't requested an explicit $top.
|
||||
const preferParts = ['odata.include-annotations="*"']
|
||||
if (!params.top) {
|
||||
preferParts.push('odata.maxpagesize=100')
|
||||
}
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
Prefer: preferParts.join(','),
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse list records failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const records = data.value ?? []
|
||||
const nextLink = data['@odata.nextLink'] ?? null
|
||||
const totalCount = data['@odata.count'] ?? null
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
records,
|
||||
count: records.length,
|
||||
totalCount,
|
||||
nextLink,
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
records: DATAVERSE_RECORDS_ARRAY_OUTPUT,
|
||||
count: { type: 'number', description: 'Number of records returned in the current page' },
|
||||
totalCount: {
|
||||
type: 'number',
|
||||
description: 'Total number of matching records server-side (requires $count=true)',
|
||||
optional: true,
|
||||
},
|
||||
nextLink: {
|
||||
type: 'string',
|
||||
description: 'URL for the next page of results',
|
||||
optional: true,
|
||||
},
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseSearchParams,
|
||||
DataverseSearchResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseSearch')
|
||||
|
||||
export const dataverseSearchTool: ToolConfig<DataverseSearchParams, DataverseSearchResponse> = {
|
||||
id: 'microsoft_dataverse_search',
|
||||
name: 'Search Microsoft Dataverse',
|
||||
description:
|
||||
'Perform a full-text relevance search across Microsoft Dataverse tables. Requires Dataverse Search to be enabled on the environment. Supports simple and Lucene query syntax.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
searchTerm: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Search text (1-100 chars). Supports simple syntax: + (AND), | (OR), - (NOT), * (wildcard), "exact phrase"',
|
||||
},
|
||||
entities: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'JSON array of search entity configs. Each object: {"Name":"account","SelectColumns":["name"],"SearchColumns":["name"],"Filter":"statecode eq 0"}',
|
||||
},
|
||||
filter: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Global OData filter applied across all entities (e.g., "createdon gt 2024-01-01")',
|
||||
},
|
||||
facets: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'JSON array of facet specifications (e.g., ["entityname,count:100","ownerid,count:100"])',
|
||||
},
|
||||
top: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of results (default: 50, max: 100)',
|
||||
},
|
||||
skip: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results to skip for pagination',
|
||||
},
|
||||
orderBy: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'JSON array of sort expressions (e.g., ["createdon desc"])',
|
||||
},
|
||||
searchMode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Search mode: "any" (default, match any term) or "all" (match all terms)',
|
||||
},
|
||||
searchType: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Query type: "simple" (default) or "lucene" (enables regex, fuzzy, proximity, boosting)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = getDataverseBaseUrl(params.environmentUrl)
|
||||
return `${baseUrl}/api/data/v9.2/searchquery`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {
|
||||
search: params.searchTerm,
|
||||
count: true,
|
||||
}
|
||||
if (params.entities) body.entities = params.entities
|
||||
if (params.filter) body.filter = params.filter
|
||||
if (params.facets) body.facets = params.facets
|
||||
if (params.top) body.top = params.top
|
||||
if (params.skip) body.skip = params.skip
|
||||
if (params.orderBy) body.orderby = params.orderBy
|
||||
|
||||
const options: Record<string, string> = {}
|
||||
if (params.searchMode) options.searchmode = params.searchMode
|
||||
if (params.searchType) options.querytype = params.searchType
|
||||
if (Object.keys(options).length > 0) {
|
||||
body.options = JSON.stringify(options).replace(/"/g, "'")
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse search failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
let parsedResponse = data.response
|
||||
if (typeof parsedResponse === 'string') {
|
||||
try {
|
||||
parsedResponse = JSON.parse(parsedResponse)
|
||||
} catch {
|
||||
parsedResponse = {}
|
||||
}
|
||||
}
|
||||
|
||||
const results = parsedResponse?.Value ?? []
|
||||
const totalCount = parsedResponse?.Count ?? 0
|
||||
const facets = parsedResponse?.Facets ?? null
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
results,
|
||||
totalCount,
|
||||
count: results.length,
|
||||
facets,
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: {
|
||||
type: 'array',
|
||||
description: 'Array of search result objects',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
Id: { type: 'string', description: 'Record GUID' },
|
||||
EntityName: {
|
||||
type: 'string',
|
||||
description: 'Table logical name (e.g., account, contact)',
|
||||
},
|
||||
ObjectTypeCode: { type: 'number', description: 'Entity type code' },
|
||||
Attributes: {
|
||||
type: 'object',
|
||||
description: 'Record attributes matching the search. Keys are column logical names.',
|
||||
},
|
||||
Highlights: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Highlighted search matches. Keys are column names, values are arrays of strings with {crmhit}/{/crmhit} markers.',
|
||||
optional: true,
|
||||
},
|
||||
Score: { type: 'number', description: 'Relevance score for this result' },
|
||||
},
|
||||
},
|
||||
},
|
||||
totalCount: {
|
||||
type: 'number',
|
||||
description: 'Total number of matching records across all tables',
|
||||
},
|
||||
count: { type: 'number', description: 'Number of results returned in this page' },
|
||||
facets: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Facet results when facets were requested. Keys are facet names, values are arrays of facet value objects with count and value properties.',
|
||||
optional: true,
|
||||
},
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
import type { OutputProperty, ToolResponse } from '@/tools/types'
|
||||
|
||||
/**
|
||||
* Microsoft Dataverse Web API types.
|
||||
* @see https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/overview
|
||||
*/
|
||||
|
||||
/**
|
||||
* Dataverse record output definition.
|
||||
* Dataverse records are dynamic (user-defined tables), so columns vary by table.
|
||||
* Every record includes OData metadata fields such as `@odata.etag`.
|
||||
* @see https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/retrieve-entity-using-web-api
|
||||
*/
|
||||
export const DATAVERSE_RECORD_OUTPUT: OutputProperty = {
|
||||
type: 'object',
|
||||
description:
|
||||
'Dataverse record object. Contains dynamic columns based on the queried table, plus OData metadata fields.',
|
||||
properties: {
|
||||
'@odata.context': {
|
||||
type: 'string',
|
||||
description: 'OData context URL describing the entity type and properties returned',
|
||||
optional: true,
|
||||
},
|
||||
'@odata.etag': {
|
||||
type: 'string',
|
||||
description: 'OData entity tag for concurrency control (e.g., W/"12345")',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Array of Dataverse records output definition for list endpoints.
|
||||
* Each item mirrors `DATAVERSE_RECORD_OUTPUT`.
|
||||
* @see https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/query-data-web-api
|
||||
*/
|
||||
export const DATAVERSE_RECORDS_ARRAY_OUTPUT: OutputProperty = {
|
||||
type: 'array',
|
||||
description:
|
||||
'Array of Dataverse records. Each record has dynamic columns based on the table schema.',
|
||||
items: {
|
||||
type: 'object',
|
||||
description: 'A single Dataverse record with dynamic columns based on the table schema',
|
||||
properties: {
|
||||
'@odata.etag': {
|
||||
type: 'string',
|
||||
description: 'OData entity tag for concurrency control (e.g., W/"12345")',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export interface DataverseCreateRecordParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
entitySetName: string
|
||||
data: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface DataverseGetRecordParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
entitySetName: string
|
||||
recordId: string
|
||||
select?: string
|
||||
expand?: string
|
||||
}
|
||||
|
||||
export interface DataverseUpdateRecordParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
entitySetName: string
|
||||
recordId: string
|
||||
data: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface DataverseDeleteRecordParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
entitySetName: string
|
||||
recordId: string
|
||||
}
|
||||
|
||||
export interface DataverseListRecordsParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
entitySetName: string
|
||||
select?: string
|
||||
filter?: string
|
||||
orderBy?: string
|
||||
top?: number
|
||||
expand?: string
|
||||
count?: string
|
||||
}
|
||||
|
||||
export interface DataverseCreateRecordResponse extends ToolResponse {
|
||||
output: {
|
||||
recordId: string
|
||||
record: Record<string, unknown>
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseGetRecordResponse extends ToolResponse {
|
||||
output: {
|
||||
record: Record<string, unknown>
|
||||
recordId: string
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseUpdateRecordResponse extends ToolResponse {
|
||||
output: {
|
||||
recordId: string
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseDeleteRecordResponse extends ToolResponse {
|
||||
output: {
|
||||
recordId: string
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseListRecordsResponse extends ToolResponse {
|
||||
output: {
|
||||
records: Record<string, unknown>[]
|
||||
count: number
|
||||
totalCount: number | null
|
||||
nextLink: string | null
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseUpsertRecordParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
entitySetName: string
|
||||
recordId: string
|
||||
data: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface DataverseUpsertRecordResponse extends ToolResponse {
|
||||
output: {
|
||||
recordId: string
|
||||
created: boolean
|
||||
record: Record<string, unknown> | null
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseWhoAmIParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
}
|
||||
|
||||
export interface DataverseWhoAmIResponse extends ToolResponse {
|
||||
output: {
|
||||
userId: string
|
||||
businessUnitId: string
|
||||
organizationId: string
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseAssociateParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
entitySetName: string
|
||||
recordId: string
|
||||
navigationProperty: string
|
||||
targetEntitySetName: string
|
||||
targetRecordId: string
|
||||
navigationType?: 'collection' | 'single'
|
||||
}
|
||||
|
||||
export interface DataverseAssociateResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
entitySetName: string
|
||||
recordId: string
|
||||
navigationProperty: string
|
||||
targetEntitySetName: string
|
||||
targetRecordId: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseDisassociateParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
entitySetName: string
|
||||
recordId: string
|
||||
navigationProperty: string
|
||||
targetRecordId?: string
|
||||
}
|
||||
|
||||
export interface DataverseDisassociateResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
entitySetName: string
|
||||
recordId: string
|
||||
navigationProperty: string
|
||||
targetRecordId?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseFetchXmlQueryParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
entitySetName: string
|
||||
fetchXml: string
|
||||
}
|
||||
|
||||
export interface DataverseFetchXmlQueryResponse extends ToolResponse {
|
||||
output: {
|
||||
records: Record<string, unknown>[]
|
||||
count: number
|
||||
fetchXmlPagingCookie: string | null
|
||||
moreRecords: boolean
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseExecuteActionParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
actionName: string
|
||||
entitySetName?: string
|
||||
recordId?: string
|
||||
parameters?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface DataverseExecuteActionResponse extends ToolResponse {
|
||||
output: {
|
||||
result: Record<string, unknown> | null
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseExecuteFunctionParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
functionName: string
|
||||
entitySetName?: string
|
||||
recordId?: string
|
||||
parameters?: string
|
||||
}
|
||||
|
||||
export interface DataverseExecuteFunctionResponse extends ToolResponse {
|
||||
output: {
|
||||
result: Record<string, unknown> | null
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseCreateMultipleParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
entitySetName: string
|
||||
entityLogicalName: string
|
||||
records: Record<string, unknown>[]
|
||||
}
|
||||
|
||||
export interface DataverseCreateMultipleResponse extends ToolResponse {
|
||||
output: {
|
||||
ids: string[]
|
||||
count: number
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseUpdateMultipleParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
entitySetName: string
|
||||
entityLogicalName: string
|
||||
records: Record<string, unknown>[]
|
||||
}
|
||||
|
||||
export interface DataverseUpdateMultipleResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseUploadFileParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
entitySetName: string
|
||||
recordId: string
|
||||
fileColumn: string
|
||||
fileName: string
|
||||
file?: unknown
|
||||
fileContent?: string
|
||||
}
|
||||
|
||||
export interface DataverseUploadFileResponse extends ToolResponse {
|
||||
output: {
|
||||
recordId: string
|
||||
fileColumn: string
|
||||
fileName: string
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseDownloadFileParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
entitySetName: string
|
||||
recordId: string
|
||||
fileColumn: string
|
||||
}
|
||||
|
||||
export interface DataverseDownloadFileResponse extends ToolResponse {
|
||||
output: {
|
||||
file: {
|
||||
name: string
|
||||
mimeType: string
|
||||
data: Buffer | string // Buffer for direct use, string for base64-encoded data
|
||||
size: number
|
||||
}
|
||||
fileContent: string
|
||||
fileName: string
|
||||
fileSize: number
|
||||
mimeType: string
|
||||
fileColumn: string
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseSearchParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
searchTerm: string
|
||||
entities?: string
|
||||
filter?: string
|
||||
facets?: string
|
||||
top?: number
|
||||
skip?: number
|
||||
orderBy?: string
|
||||
searchMode?: string
|
||||
searchType?: string
|
||||
}
|
||||
|
||||
export interface DataverseSearchResponse extends ToolResponse {
|
||||
output: {
|
||||
results: Record<string, unknown>[]
|
||||
totalCount: number
|
||||
count: number
|
||||
facets: Record<string, unknown> | null
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseGetEntityMetadataParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
entityLogicalName: string
|
||||
select?: string
|
||||
includeAttributes?: string
|
||||
}
|
||||
|
||||
export interface DataverseGetEntityMetadataResponse extends ToolResponse {
|
||||
output: {
|
||||
entitySetName: string | null
|
||||
logicalName: string | null
|
||||
displayName: string | null
|
||||
primaryIdAttribute: string | null
|
||||
primaryNameAttribute: string | null
|
||||
attributes: Record<string, unknown>[]
|
||||
metadata: Record<string, unknown>
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export type DataverseResponse =
|
||||
| DataverseCreateRecordResponse
|
||||
| DataverseGetRecordResponse
|
||||
| DataverseUpdateRecordResponse
|
||||
| DataverseDeleteRecordResponse
|
||||
| DataverseListRecordsResponse
|
||||
| DataverseUpsertRecordResponse
|
||||
| DataverseWhoAmIResponse
|
||||
| DataverseAssociateResponse
|
||||
| DataverseDisassociateResponse
|
||||
| DataverseFetchXmlQueryResponse
|
||||
| DataverseExecuteActionResponse
|
||||
| DataverseExecuteFunctionResponse
|
||||
| DataverseCreateMultipleResponse
|
||||
| DataverseUpdateMultipleResponse
|
||||
| DataverseUploadFileResponse
|
||||
| DataverseDownloadFileResponse
|
||||
| DataverseSearchResponse
|
||||
| DataverseGetEntityMetadataResponse
|
||||
@@ -0,0 +1,114 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseUpdateMultipleParams,
|
||||
DataverseUpdateMultipleResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseUpdateMultiple')
|
||||
|
||||
export const dataverseUpdateMultipleTool: ToolConfig<
|
||||
DataverseUpdateMultipleParams,
|
||||
DataverseUpdateMultipleResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_update_multiple',
|
||||
name: 'Update Multiple Microsoft Dataverse Records',
|
||||
description:
|
||||
'Update multiple records of the same table type in a single request. Each record must include its primary key. Only include columns that need to be changed. Recommended batch size: 100-1000 records.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Entity set name (plural table name, e.g., accounts, contacts)',
|
||||
},
|
||||
entityLogicalName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Table logical name for @odata.type annotation (e.g., account, contact). Used to set Microsoft.Dynamics.CRM.{entityLogicalName} on each record.',
|
||||
},
|
||||
records: {
|
||||
type: 'object',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Array of record objects to update. Each record must include its primary key (e.g., accountid) and only the columns being changed. The @odata.type annotation is added automatically.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = getDataverseBaseUrl(params.environmentUrl)
|
||||
return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}/Microsoft.Dynamics.CRM.UpdateMultiple`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
let records = params.records
|
||||
if (typeof records === 'string') {
|
||||
try {
|
||||
records = JSON.parse(records)
|
||||
} catch {
|
||||
throw new Error('Invalid JSON format for records array')
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(records)) {
|
||||
throw new Error('Records must be an array of objects')
|
||||
}
|
||||
const entityLogicalName = params.entityLogicalName.trim()
|
||||
const targets = records.map((record: Record<string, unknown>) => ({
|
||||
...record,
|
||||
'@odata.type': `Microsoft.Dynamics.CRM.${entityLogicalName}`,
|
||||
}))
|
||||
return { Targets: targets }
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse update multiple failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether all records were updated successfully' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseUpdateRecordParams,
|
||||
DataverseUpdateRecordResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseUpdateRecord')
|
||||
|
||||
export const dataverseUpdateRecordTool: ToolConfig<
|
||||
DataverseUpdateRecordParams,
|
||||
DataverseUpdateRecordResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_update_record',
|
||||
name: 'Update Microsoft Dataverse Record',
|
||||
description:
|
||||
'Update an existing record in a Microsoft Dataverse table. Only send the columns you want to change.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Entity set name (plural table name, e.g., accounts, contacts)',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The unique identifier (GUID) of the record to update',
|
||||
},
|
||||
data: {
|
||||
type: 'object',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Record data to update as a JSON object with column names as keys',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = getDataverseBaseUrl(params.environmentUrl)
|
||||
return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}(${params.recordId.trim()})`
|
||||
},
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
'If-Match': '*',
|
||||
}),
|
||||
body: (params) => {
|
||||
let data = params.data
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
data = JSON.parse(data)
|
||||
} catch {
|
||||
throw new Error('Invalid JSON format for record data')
|
||||
}
|
||||
}
|
||||
return data
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: DataverseUpdateRecordParams) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse update record failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
recordId: params?.recordId ?? '',
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
recordId: { type: 'string', description: 'The ID of the updated record' },
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import type {
|
||||
DataverseUploadFileParams,
|
||||
DataverseUploadFileResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const dataverseUploadFileTool: ToolConfig<
|
||||
DataverseUploadFileParams,
|
||||
DataverseUploadFileResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_upload_file',
|
||||
name: 'Upload File to Microsoft Dataverse',
|
||||
description:
|
||||
'Upload a file to a file or image column on a Dataverse record. Supports single-request upload for files up to 128 MB. The file content must be provided as a base64-encoded string.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Entity set name (plural table name, e.g., accounts, contacts)',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Record GUID to upload the file to',
|
||||
},
|
||||
fileColumn: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'File or image column logical name (e.g., entityimage, cr_document)',
|
||||
},
|
||||
fileName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Name of the file being uploaded (e.g., document.pdf)',
|
||||
},
|
||||
file: {
|
||||
type: 'file',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'File to upload (UserFile object)',
|
||||
},
|
||||
fileContent: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'Base64-encoded file content (legacy)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/microsoft-dataverse/upload-file',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
accessToken: params.accessToken,
|
||||
environmentUrl: params.environmentUrl,
|
||||
entitySetName: params.entitySetName,
|
||||
recordId: params.recordId,
|
||||
fileColumn: params.fileColumn,
|
||||
fileName: params.fileName,
|
||||
file: params.file,
|
||||
fileContent: params.fileContent,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'Dataverse upload file failed')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: data.output,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
recordId: { type: 'string', description: 'Record GUID the file was uploaded to' },
|
||||
fileColumn: { type: 'string', description: 'File column the file was uploaded to' },
|
||||
fileName: { type: 'string', description: 'Name of the uploaded file' },
|
||||
success: { type: 'boolean', description: 'Whether the file was uploaded successfully' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseUpsertRecordParams,
|
||||
DataverseUpsertRecordResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import { DATAVERSE_RECORD_OUTPUT } from '@/tools/microsoft_dataverse/types'
|
||||
import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseUpsertRecord')
|
||||
|
||||
export const dataverseUpsertRecordTool: ToolConfig<
|
||||
DataverseUpsertRecordParams,
|
||||
DataverseUpsertRecordResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_upsert_record',
|
||||
name: 'Upsert Microsoft Dataverse Record',
|
||||
description:
|
||||
'Create or update a record in a Microsoft Dataverse table. If a record with the given ID exists, it is updated; otherwise, a new record is created.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Entity set name (plural table name, e.g., accounts, contacts)',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The unique identifier (GUID) of the record to upsert',
|
||||
},
|
||||
data: {
|
||||
type: 'object',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Record data as a JSON object with column names as keys',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = getDataverseBaseUrl(params.environmentUrl)
|
||||
return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}(${params.recordId.trim()})`
|
||||
},
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
Prefer: 'return=representation',
|
||||
}),
|
||||
body: (params) => {
|
||||
let data = params.data
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
data = JSON.parse(data)
|
||||
} catch {
|
||||
throw new Error('Invalid JSON format for record data')
|
||||
}
|
||||
}
|
||||
return data
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: DataverseUpsertRecordParams) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse upsert record failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const created = response.status === 201
|
||||
const data = await response.json().catch(() => null)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
recordId: params?.recordId ?? '',
|
||||
created,
|
||||
record: data,
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
recordId: { type: 'string', description: 'The ID of the upserted record' },
|
||||
created: { type: 'boolean', description: 'True if the record was created, false if updated' },
|
||||
record: { ...DATAVERSE_RECORD_OUTPUT, optional: true },
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Normalizes a Dataverse environment URL into a base URL suitable for building Web API request
|
||||
* paths: trims incidental whitespace (common when pasted from a browser address bar) and strips
|
||||
* a trailing slash so callers can safely append `/api/data/v9.2/...`.
|
||||
*/
|
||||
export function getDataverseBaseUrl(environmentUrl: string): string {
|
||||
return environmentUrl.trim().replace(/\/$/, '')
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseWhoAmIParams,
|
||||
DataverseWhoAmIResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseWhoAmI')
|
||||
|
||||
export const dataverseWhoAmITool: ToolConfig<DataverseWhoAmIParams, DataverseWhoAmIResponse> = {
|
||||
id: 'microsoft_dataverse_whoami',
|
||||
name: 'Microsoft Dataverse WhoAmI',
|
||||
description:
|
||||
'Retrieve the current authenticated user information from Microsoft Dataverse. Useful for testing connectivity and getting the user ID, business unit ID, and organization ID.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = getDataverseBaseUrl(params.environmentUrl)
|
||||
return `${baseUrl}/api/data/v9.2/WhoAmI()`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse WhoAmI failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
userId: data.UserId ?? '',
|
||||
businessUnitId: data.BusinessUnitId ?? '',
|
||||
organizationId: data.OrganizationId ?? '',
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
userId: { type: 'string', description: 'The authenticated user ID' },
|
||||
businessUnitId: { type: 'string', description: 'The business unit ID' },
|
||||
organizationId: { type: 'string', description: 'The organization ID' },
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user