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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -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' },
},
}
+396
View File
@@ -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' },
},
}