Skip to content

Commit efdd643

Browse files
committed
fix(microsoft-dataverse): align integration with live API docs, add table metadata tool
- trim environment URL/entity/record IDs across all tools via shared getDataverseBaseUrl - encodeURIComponent OData $select/$filter/$orderby/$expand values in list_records/get_record - fix $top being silently ignored when Prefer: odata.maxpagesize is also sent - add microsoft_dataverse_get_entity_metadata tool for table/column lookup (covered by existing user_impersonation scope, no new scopes)
1 parent a48ecd2 commit efdd643

23 files changed

Lines changed: 358 additions & 61 deletions

apps/sim/app/api/tools/microsoft-dataverse/upload-file/route.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
1111
import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
1212
import { docNotReadyResponse } from '@/lib/uploads/utils/servable-file-response'
1313
import { assertToolFileAccess } from '@/app/api/files/authorization'
14+
import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils'
1415

1516
export const dynamic = 'force-dynamic'
1617

@@ -93,8 +94,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
9394
)
9495
}
9596

96-
const baseUrl = validatedData.environmentUrl.replace(/\/$/, '')
97-
const uploadUrl = `${baseUrl}/api/data/v9.2/${validatedData.entitySetName}(${validatedData.recordId})/${validatedData.fileColumn}`
97+
const baseUrl = getDataverseBaseUrl(validatedData.environmentUrl)
98+
const uploadUrl = `${baseUrl}/api/data/v9.2/${validatedData.entitySetName.trim()}(${validatedData.recordId.trim()})/${validatedData.fileColumn.trim()}`
9899

99100
const response = await secureFetchWithValidation(
100101
uploadUrl,

apps/sim/blocks/blocks/microsoft_dataverse.ts

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const MicrosoftDataverseBlock: BlockConfig<DataverseResponse> = {
1111
description: 'Manage records in Microsoft Dataverse tables',
1212
authMode: AuthMode.OAuth,
1313
longDescription:
14-
'Integrate Microsoft Dataverse into your workflow. Create, read, update, delete, upsert, associate, query, search, and execute actions and functions against Dataverse tables using the Web API. Supports bulk operations, FetchXML, file uploads, and relevance search. Works with Dynamics 365, Power Platform, and custom Dataverse environments.',
14+
'Integrate Microsoft Dataverse into your workflow. Create, read, update, delete, upsert, associate, query, search, and execute actions and functions against Dataverse tables using the Web API. Supports bulk operations, FetchXML, file uploads, relevance search, and table metadata lookup. Works with Dynamics 365, Power Platform, and custom Dataverse environments.',
1515
docsLink: 'https://docs.sim.ai/integrations/microsoft_dataverse',
1616
category: 'tools',
1717
integrationType: IntegrationType.Databases,
@@ -39,6 +39,7 @@ export const MicrosoftDataverseBlock: BlockConfig<DataverseResponse> = {
3939
{ label: 'Download File', id: 'download_file' },
4040
{ label: 'Associate Records', id: 'associate' },
4141
{ label: 'Disassociate Records', id: 'disassociate' },
42+
{ label: 'Get Table Metadata', id: 'get_entity_metadata' },
4243
{ label: 'WhoAmI', id: 'whoami' },
4344
],
4445
value: () => 'list_records',
@@ -66,12 +67,12 @@ export const MicrosoftDataverseBlock: BlockConfig<DataverseResponse> = {
6667
placeholder: 'Plural table name (e.g., accounts, contacts)',
6768
condition: {
6869
field: 'operation',
69-
value: ['whoami', 'search'],
70+
value: ['whoami', 'search', 'get_entity_metadata'],
7071
not: true,
7172
},
7273
required: {
7374
field: 'operation',
74-
value: ['whoami', 'search', 'execute_action', 'execute_function'],
75+
value: ['whoami', 'search', 'execute_action', 'execute_function', 'get_entity_metadata'],
7576
not: true,
7677
},
7778
},
@@ -256,8 +257,25 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`,
256257
title: 'Table Logical Name',
257258
type: 'short-input',
258259
placeholder: 'Singular table name (e.g., account, contact)',
259-
condition: { field: 'operation', value: ['create_multiple', 'update_multiple'] },
260-
required: { field: 'operation', value: ['create_multiple', 'update_multiple'] },
260+
condition: {
261+
field: 'operation',
262+
value: ['create_multiple', 'update_multiple', 'get_entity_metadata'],
263+
},
264+
required: {
265+
field: 'operation',
266+
value: ['create_multiple', 'update_multiple', 'get_entity_metadata'],
267+
},
268+
},
269+
{
270+
id: 'includeAttributes',
271+
title: 'Include Column Definitions',
272+
type: 'dropdown',
273+
options: [
274+
{ label: 'No', id: 'false' },
275+
{ label: 'Yes', id: 'true' },
276+
],
277+
value: () => 'false',
278+
condition: { field: 'operation', value: 'get_entity_metadata' },
261279
},
262280
{
263281
id: 'records',
@@ -319,6 +337,16 @@ Return ONLY a valid JSON array - no explanations, no markdown code blocks.`,
319337
mode: 'advanced',
320338
required: { field: 'operation', value: 'upload_file' },
321339
},
340+
// Table metadata
341+
{
342+
id: 'metadataSelect',
343+
title: 'Select Metadata Properties',
344+
type: 'short-input',
345+
placeholder:
346+
'Comma-separated metadata properties (e.g., LogicalName,DisplayName,EntitySetName)',
347+
condition: { field: 'operation', value: 'get_entity_metadata' },
348+
mode: 'advanced',
349+
},
322350
// OData query options (list_records)
323351
{
324352
id: 'select',
@@ -463,6 +491,7 @@ Return ONLY the expand expression - no $expand= prefix, no explanations.`,
463491
'microsoft_dataverse_execute_action',
464492
'microsoft_dataverse_execute_function',
465493
'microsoft_dataverse_fetchxml_query',
494+
'microsoft_dataverse_get_entity_metadata',
466495
'microsoft_dataverse_get_record',
467496
'microsoft_dataverse_list_records',
468497
'microsoft_dataverse_search',
@@ -498,9 +527,14 @@ Return ONLY the expand expression - no $expand= prefix, no explanations.`,
498527
// Prevent stale action parameters from overwriting mapped function parameters
499528
rest.parameters = undefined
500529
}
530+
if (operation === 'get_entity_metadata' && rest.metadataSelect) {
531+
cleanParams.select = rest.metadataSelect
532+
rest.metadataSelect = undefined
533+
}
501534
// Always clean up mapped subBlock IDs so they don't leak through the loop below
502535
rest.searchEntities = undefined
503536
rest.functionParameters = undefined
537+
rest.metadataSelect = undefined
504538

505539
Object.entries(rest).forEach(([key, value]) => {
506540
if (value !== undefined && value !== null && value !== '') {
@@ -547,11 +581,22 @@ Return ONLY the expand expression - no $expand= prefix, no explanations.`,
547581
description: 'Function parameters as URL-encoded string',
548582
},
549583
parameters: { type: 'json', description: 'Action parameters as JSON object' },
550-
entityLogicalName: { type: 'string', description: 'Table logical name for @odata.type' },
584+
entityLogicalName: {
585+
type: 'string',
586+
description: 'Table logical name for @odata.type, or to look up table metadata',
587+
},
551588
records: { type: 'json', description: 'Array of record objects for bulk operations' },
552589
fileColumn: { type: 'string', description: 'File or image column logical name' },
553590
fileName: { type: 'string', description: 'Name of the file to upload' },
554591
file: { type: 'json', description: 'File to upload (canonical param)' },
592+
metadataSelect: {
593+
type: 'string',
594+
description: 'Comma-separated table metadata properties to return',
595+
},
596+
includeAttributes: {
597+
type: 'string',
598+
description: 'Set to "true" to also return column (attribute) definitions',
599+
},
555600
},
556601
outputs: {
557602
records: { type: 'json', description: 'Array of records (list/fetchxml/search)' },
@@ -569,7 +614,8 @@ Return ONLY the expand expression - no $expand= prefix, no explanations.`,
569614
organizationId: { type: 'string', description: 'Organization ID (WhoAmI)' },
570615
entitySetName: {
571616
type: 'string',
572-
description: 'Source entity set name (associate/disassociate)',
617+
description:
618+
'Source entity set name (associate/disassociate), or the looked-up entity set name (get table metadata)',
573619
},
574620
navigationProperty: {
575621
type: 'string',
@@ -589,6 +635,30 @@ Return ONLY the expand expression - no $expand= prefix, no explanations.`,
589635
fileSize: { type: 'number', description: 'File size in bytes' },
590636
mimeType: { type: 'string', description: 'File MIME type' },
591637
fileColumn: { type: 'string', description: 'File column name' },
638+
logicalName: {
639+
type: 'string',
640+
description: 'Singular table logical name (get table metadata)',
641+
},
642+
displayName: {
643+
type: 'string',
644+
description: 'Localized table display name (get table metadata)',
645+
},
646+
primaryIdAttribute: {
647+
type: 'string',
648+
description: 'Primary key column logical name (get table metadata)',
649+
},
650+
primaryNameAttribute: {
651+
type: 'string',
652+
description: 'Primary name column logical name (get table metadata)',
653+
},
654+
attributes: {
655+
type: 'json',
656+
description: 'Column definitions for the table (get table metadata)',
657+
},
658+
metadata: {
659+
type: 'json',
660+
description: 'Full raw table metadata response (get table metadata)',
661+
},
592662
},
593663
}
594664

@@ -690,5 +760,11 @@ export const MicrosoftDataverseBlockMeta = {
690760
content:
691761
'# Bulk Write Records\n\nWrite many Dataverse rows efficiently in a single call.\n\n## Steps\n1. Assemble the array of records, mapping each to the table column names.\n2. Use Create Multiple to insert new rows, or Update Multiple to change existing rows by ID.\n3. Verify the operation succeeded and capture any per-record errors.\n\n## Output\nThe count of records written, their IDs, and any rows that failed with their error.',
692762
},
763+
{
764+
name: 'discover-table-schema',
765+
description: 'Look up a table entity set name and column logical names before writing data.',
766+
content:
767+
'# Discover Table Schema\n\nAvoid guessing at column and entity set names when a workflow targets an unfamiliar Dataverse table.\n\n## Steps\n1. Take the singular table logical name (e.g., account, contact, or a custom table).\n2. Use Get Table Metadata, optionally including column definitions.\n3. Use the returned entity set name and column logical names to build the record data and entity set name for other operations.\n\n## Output\nThe entity set name, primary key and primary name columns, and (optionally) the full list of column definitions.',
768+
},
693769
],
694770
} as const satisfies BlockMeta

apps/sim/tools/microsoft_dataverse/associate.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
DataverseAssociateParams,
44
DataverseAssociateResponse,
55
} from '@/tools/microsoft_dataverse/types'
6+
import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils'
67
import type { ToolConfig } from '@/tools/types'
78

89
const logger = createLogger('DataverseAssociate')
@@ -75,8 +76,8 @@ export const dataverseAssociateTool: ToolConfig<
7576

7677
request: {
7778
url: (params) => {
78-
const baseUrl = params.environmentUrl.replace(/\/$/, '')
79-
return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})/${params.navigationProperty}/$ref`
79+
const baseUrl = getDataverseBaseUrl(params.environmentUrl)
80+
return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}(${params.recordId.trim()})/${params.navigationProperty.trim()}/$ref`
8081
},
8182
method: (params) => (params.navigationType === 'single' ? 'PUT' : 'POST'),
8283
headers: (params) => ({
@@ -87,9 +88,9 @@ export const dataverseAssociateTool: ToolConfig<
8788
Accept: 'application/json',
8889
}),
8990
body: (params) => {
90-
const baseUrl = params.environmentUrl.replace(/\/$/, '')
91+
const baseUrl = getDataverseBaseUrl(params.environmentUrl)
9192
return {
92-
'@odata.id': `${baseUrl}/api/data/v9.2/${params.targetEntitySetName}(${params.targetRecordId})`,
93+
'@odata.id': `${baseUrl}/api/data/v9.2/${params.targetEntitySetName.trim()}(${params.targetRecordId.trim()})`,
9394
}
9495
},
9596
},

apps/sim/tools/microsoft_dataverse/create_multiple.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
DataverseCreateMultipleParams,
44
DataverseCreateMultipleResponse,
55
} from '@/tools/microsoft_dataverse/types'
6+
import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils'
67
import type { ToolConfig } from '@/tools/types'
78

89
const logger = createLogger('DataverseCreateMultiple')
@@ -57,8 +58,8 @@ export const dataverseCreateMultipleTool: ToolConfig<
5758

5859
request: {
5960
url: (params) => {
60-
const baseUrl = params.environmentUrl.replace(/\/$/, '')
61-
return `${baseUrl}/api/data/v9.2/${params.entitySetName}/Microsoft.Dynamics.CRM.CreateMultiple`
61+
const baseUrl = getDataverseBaseUrl(params.environmentUrl)
62+
return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}/Microsoft.Dynamics.CRM.CreateMultiple`
6263
},
6364
method: 'POST',
6465
headers: (params) => ({
@@ -80,9 +81,10 @@ export const dataverseCreateMultipleTool: ToolConfig<
8081
if (!Array.isArray(records)) {
8182
throw new Error('Records must be an array of objects')
8283
}
84+
const entityLogicalName = params.entityLogicalName.trim()
8385
const targets = records.map((record: Record<string, unknown>) => ({
8486
...record,
85-
'@odata.type': `Microsoft.Dynamics.CRM.${params.entityLogicalName}`,
87+
'@odata.type': `Microsoft.Dynamics.CRM.${entityLogicalName}`,
8688
}))
8789
return { Targets: targets }
8890
},

apps/sim/tools/microsoft_dataverse/create_record.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
DataverseCreateRecordResponse,
55
} from '@/tools/microsoft_dataverse/types'
66
import { DATAVERSE_RECORD_OUTPUT } from '@/tools/microsoft_dataverse/types'
7+
import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils'
78
import type { ToolConfig } from '@/tools/types'
89

910
const logger = createLogger('DataverseCreateRecord')
@@ -50,8 +51,8 @@ export const dataverseCreateRecordTool: ToolConfig<
5051

5152
request: {
5253
url: (params) => {
53-
const baseUrl = params.environmentUrl.replace(/\/$/, '')
54-
return `${baseUrl}/api/data/v9.2/${params.entitySetName}`
54+
const baseUrl = getDataverseBaseUrl(params.environmentUrl)
55+
return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}`
5556
},
5657
method: 'POST',
5758
headers: (params) => ({

apps/sim/tools/microsoft_dataverse/delete_record.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
DataverseDeleteRecordParams,
44
DataverseDeleteRecordResponse,
55
} from '@/tools/microsoft_dataverse/types'
6+
import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils'
67
import type { ToolConfig } from '@/tools/types'
78

89
const logger = createLogger('DataverseDeleteRecord')
@@ -48,8 +49,8 @@ export const dataverseDeleteRecordTool: ToolConfig<
4849

4950
request: {
5051
url: (params) => {
51-
const baseUrl = params.environmentUrl.replace(/\/$/, '')
52-
return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})`
52+
const baseUrl = getDataverseBaseUrl(params.environmentUrl)
53+
return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}(${params.recordId.trim()})`
5354
},
5455
method: 'DELETE',
5556
headers: (params) => ({

apps/sim/tools/microsoft_dataverse/disassociate.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
DataverseDisassociateParams,
44
DataverseDisassociateResponse,
55
} from '@/tools/microsoft_dataverse/types'
6+
import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils'
67
import type { ToolConfig } from '@/tools/types'
78

89
const logger = createLogger('DataverseDisassociate')
@@ -63,11 +64,14 @@ export const dataverseDisassociateTool: ToolConfig<
6364

6465
request: {
6566
url: (params) => {
66-
const baseUrl = params.environmentUrl.replace(/\/$/, '')
67+
const baseUrl = getDataverseBaseUrl(params.environmentUrl)
68+
const entitySetName = params.entitySetName.trim()
69+
const recordId = params.recordId.trim()
70+
const navigationProperty = params.navigationProperty.trim()
6771
if (params.targetRecordId) {
68-
return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})/${params.navigationProperty}(${params.targetRecordId})/$ref`
72+
return `${baseUrl}/api/data/v9.2/${entitySetName}(${recordId})/${navigationProperty}(${params.targetRecordId.trim()})/$ref`
6973
}
70-
return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})/${params.navigationProperty}/$ref`
74+
return `${baseUrl}/api/data/v9.2/${entitySetName}(${recordId})/${navigationProperty}/$ref`
7175
},
7276
method: 'DELETE',
7377
headers: (params) => ({

apps/sim/tools/microsoft_dataverse/download_file.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
DataverseDownloadFileParams,
44
DataverseDownloadFileResponse,
55
} from '@/tools/microsoft_dataverse/types'
6+
import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils'
67
import type { ToolConfig } from '@/tools/types'
78

89
const logger = createLogger('DataverseDownloadFile')
@@ -55,8 +56,8 @@ export const dataverseDownloadFileTool: ToolConfig<
5556

5657
request: {
5758
url: (params) => {
58-
const baseUrl = params.environmentUrl.replace(/\/$/, '')
59-
return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})/${params.fileColumn}/$value`
59+
const baseUrl = getDataverseBaseUrl(params.environmentUrl)
60+
return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}(${params.recordId.trim()})/${params.fileColumn.trim()}/$value`
6061
},
6162
method: 'GET',
6263
headers: (params) => ({

apps/sim/tools/microsoft_dataverse/execute_action.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
DataverseExecuteActionParams,
44
DataverseExecuteActionResponse,
55
} from '@/tools/microsoft_dataverse/types'
6+
import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils'
67
import type { ToolConfig } from '@/tools/types'
78

89
const logger = createLogger('DataverseExecuteAction')
@@ -65,14 +66,16 @@ export const dataverseExecuteActionTool: ToolConfig<
6566

6667
request: {
6768
url: (params) => {
68-
const baseUrl = params.environmentUrl.replace(/\/$/, '')
69+
const baseUrl = getDataverseBaseUrl(params.environmentUrl)
70+
const actionName = params.actionName.trim()
6971
if (params.entitySetName) {
72+
const entitySetName = params.entitySetName.trim()
7073
if (params.recordId) {
71-
return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})/Microsoft.Dynamics.CRM.${params.actionName}`
74+
return `${baseUrl}/api/data/v9.2/${entitySetName}(${params.recordId.trim()})/Microsoft.Dynamics.CRM.${actionName}`
7275
}
73-
return `${baseUrl}/api/data/v9.2/${params.entitySetName}/Microsoft.Dynamics.CRM.${params.actionName}`
76+
return `${baseUrl}/api/data/v9.2/${entitySetName}/Microsoft.Dynamics.CRM.${actionName}`
7477
}
75-
return `${baseUrl}/api/data/v9.2/${params.actionName}`
78+
return `${baseUrl}/api/data/v9.2/${actionName}`
7679
},
7780
method: 'POST',
7881
headers: (params) => ({

apps/sim/tools/microsoft_dataverse/execute_function.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
DataverseExecuteFunctionParams,
44
DataverseExecuteFunctionResponse,
55
} from '@/tools/microsoft_dataverse/types'
6+
import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils'
67
import type { ToolConfig } from '@/tools/types'
78

89
const logger = createLogger('DataverseExecuteFunction')
@@ -64,15 +65,17 @@ export const dataverseExecuteFunctionTool: ToolConfig<
6465

6566
request: {
6667
url: (params) => {
67-
const baseUrl = params.environmentUrl.replace(/\/$/, '')
68+
const baseUrl = getDataverseBaseUrl(params.environmentUrl)
69+
const functionName = params.functionName.trim()
6870
const paramStr = params.parameters ? `(${params.parameters})` : '()'
6971
if (params.entitySetName) {
72+
const entitySetName = params.entitySetName.trim()
7073
if (params.recordId) {
71-
return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})/Microsoft.Dynamics.CRM.${params.functionName}${paramStr}`
74+
return `${baseUrl}/api/data/v9.2/${entitySetName}(${params.recordId.trim()})/Microsoft.Dynamics.CRM.${functionName}${paramStr}`
7275
}
73-
return `${baseUrl}/api/data/v9.2/${params.entitySetName}/Microsoft.Dynamics.CRM.${params.functionName}${paramStr}`
76+
return `${baseUrl}/api/data/v9.2/${entitySetName}/Microsoft.Dynamics.CRM.${functionName}${paramStr}`
7477
}
75-
return `${baseUrl}/api/data/v9.2/${params.functionName}${paramStr}`
78+
return `${baseUrl}/api/data/v9.2/${functionName}${paramStr}`
7679
},
7780
method: 'GET',
7881
headers: (params) => ({

0 commit comments

Comments
 (0)