diff --git a/apps/sim/app/api/tools/microsoft-dataverse/upload-file/route.ts b/apps/sim/app/api/tools/microsoft-dataverse/upload-file/route.ts index 21a767f4a3e..56438618aa6 100644 --- a/apps/sim/app/api/tools/microsoft-dataverse/upload-file/route.ts +++ b/apps/sim/app/api/tools/microsoft-dataverse/upload-file/route.ts @@ -11,11 +11,15 @@ import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils' import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server' import { docNotReadyResponse } from '@/lib/uploads/utils/servable-file-response' import { assertToolFileAccess } from '@/app/api/files/authorization' +import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils' export const dynamic = 'force-dynamic' const logger = createLogger('DataverseUploadFileAPI') +/** Dataverse Web API's absolute ceiling for a single-request (non-chunked) file column upload. */ +const DATAVERSE_SINGLE_REQUEST_UPLOAD_MAX_BYTES = 128 * 1024 * 1024 + export const POST = withRouteHandler(async (request: NextRequest) => { const requestId = generateRequestId() @@ -93,8 +97,20 @@ export const POST = withRouteHandler(async (request: NextRequest) => { ) } - const baseUrl = validatedData.environmentUrl.replace(/\/$/, '') - const uploadUrl = `${baseUrl}/api/data/v9.2/${validatedData.entitySetName}(${validatedData.recordId})/${validatedData.fileColumn}` + if (fileBuffer.length > DATAVERSE_SINGLE_REQUEST_UPLOAD_MAX_BYTES) { + const sizeMB = (fileBuffer.length / (1024 * 1024)).toFixed(2) + logger.warn(`[${requestId}] File too large for single-request upload: ${sizeMB}MB`) + return NextResponse.json( + { + success: false, + error: `File size (${sizeMB}MB) exceeds Dataverse's 128MB limit for single-request file column uploads. Split the file and use chunked upload instead.`, + }, + { status: 400 } + ) + } + + const baseUrl = getDataverseBaseUrl(validatedData.environmentUrl) + const uploadUrl = `${baseUrl}/api/data/v9.2/${validatedData.entitySetName.trim()}(${validatedData.recordId.trim()})/${validatedData.fileColumn.trim()}` const response = await secureFetchWithValidation( uploadUrl, diff --git a/apps/sim/blocks/blocks/microsoft_dataverse.ts b/apps/sim/blocks/blocks/microsoft_dataverse.ts index 34781e98366..a0942cbf354 100644 --- a/apps/sim/blocks/blocks/microsoft_dataverse.ts +++ b/apps/sim/blocks/blocks/microsoft_dataverse.ts @@ -11,7 +11,7 @@ export const MicrosoftDataverseBlock: BlockConfig = { description: 'Manage records in Microsoft Dataverse tables', authMode: AuthMode.OAuth, longDescription: - '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.', + '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.', docsLink: 'https://docs.sim.ai/integrations/microsoft_dataverse', category: 'tools', integrationType: IntegrationType.Databases, @@ -39,6 +39,7 @@ export const MicrosoftDataverseBlock: BlockConfig = { { label: 'Download File', id: 'download_file' }, { label: 'Associate Records', id: 'associate' }, { label: 'Disassociate Records', id: 'disassociate' }, + { label: 'Get Table Metadata', id: 'get_entity_metadata' }, { label: 'WhoAmI', id: 'whoami' }, ], value: () => 'list_records', @@ -66,12 +67,12 @@ export const MicrosoftDataverseBlock: BlockConfig = { placeholder: 'Plural table name (e.g., accounts, contacts)', condition: { field: 'operation', - value: ['whoami', 'search'], + value: ['whoami', 'search', 'get_entity_metadata'], not: true, }, required: { field: 'operation', - value: ['whoami', 'search', 'execute_action', 'execute_function'], + value: ['whoami', 'search', 'execute_action', 'execute_function', 'get_entity_metadata'], not: true, }, }, @@ -203,6 +204,22 @@ Return ONLY valid FetchXML - no explanations, no markdown code blocks.`, condition: { field: 'operation', value: 'search' }, mode: 'advanced', }, + { + id: 'facets', + title: 'Facets', + type: 'long-input', + placeholder: 'JSON array of facet specs (e.g., ["entityname,count:100"])', + condition: { field: 'operation', value: 'search' }, + mode: 'advanced', + }, + { + id: 'skip', + title: 'Skip', + type: 'short-input', + placeholder: 'Number of results to skip for pagination', + condition: { field: 'operation', value: 'search' }, + mode: 'advanced', + }, // Execute Action { id: 'actionName', @@ -256,8 +273,25 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`, title: 'Table Logical Name', type: 'short-input', placeholder: 'Singular table name (e.g., account, contact)', - condition: { field: 'operation', value: ['create_multiple', 'update_multiple'] }, - required: { field: 'operation', value: ['create_multiple', 'update_multiple'] }, + condition: { + field: 'operation', + value: ['create_multiple', 'update_multiple', 'get_entity_metadata'], + }, + required: { + field: 'operation', + value: ['create_multiple', 'update_multiple', 'get_entity_metadata'], + }, + }, + { + id: 'includeAttributes', + title: 'Include Column Definitions', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'get_entity_metadata' }, }, { id: 'records', @@ -319,6 +353,16 @@ Return ONLY a valid JSON array - no explanations, no markdown code blocks.`, mode: 'advanced', required: { field: 'operation', value: 'upload_file' }, }, + // Table metadata + { + id: 'metadataSelect', + title: 'Select Metadata Properties', + type: 'short-input', + placeholder: + 'Comma-separated metadata properties (e.g., LogicalName,DisplayName,EntitySetName)', + condition: { field: 'operation', value: 'get_entity_metadata' }, + mode: 'advanced', + }, // OData query options (list_records) { id: 'select', @@ -388,10 +432,22 @@ Return ONLY the orderby expression - no $orderby= prefix, no explanations.`, id: 'top', title: 'Max Results', type: 'short-input', - placeholder: 'Maximum number of records (default: 5000)', + placeholder: 'Maximum number of records to return', condition: { field: 'operation', value: ['list_records', 'search'] }, mode: 'advanced', }, + { + id: 'count', + title: 'Include Total Count', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'list_records' }, + mode: 'advanced', + }, { id: 'expand', title: 'Expand', @@ -463,6 +519,7 @@ Return ONLY the expand expression - no $expand= prefix, no explanations.`, 'microsoft_dataverse_execute_action', 'microsoft_dataverse_execute_function', 'microsoft_dataverse_fetchxml_query', + 'microsoft_dataverse_get_entity_metadata', 'microsoft_dataverse_get_record', 'microsoft_dataverse_list_records', 'microsoft_dataverse_search', @@ -498,9 +555,18 @@ Return ONLY the expand expression - no $expand= prefix, no explanations.`, // Prevent stale action parameters from overwriting mapped function parameters rest.parameters = undefined } + if (operation === 'get_entity_metadata') { + if (rest.metadataSelect) { + cleanParams.select = rest.metadataSelect + } + // The shared `select` subBlock belongs to list/get record operations - clear it so a + // stale record-column $select can't override or fight the mapped metadataSelect value + rest.select = undefined + } // Always clean up mapped subBlock IDs so they don't leak through the loop below rest.searchEntities = undefined rest.functionParameters = undefined + rest.metadataSelect = undefined Object.entries(rest).forEach(([key, value]) => { if (value !== undefined && value !== null && value !== '') { @@ -523,6 +589,10 @@ Return ONLY the expand expression - no $expand= prefix, no explanations.`, filter: { type: 'string', description: 'OData $filter expression' }, orderBy: { type: 'string', description: 'OData $orderby expression' }, top: { type: 'string', description: 'Maximum number of records' }, + count: { + type: 'string', + description: 'Set to "true" to include total record count in the response (list records)', + }, expand: { type: 'string', description: 'Navigation properties to expand' }, navigationProperty: { type: 'string', @@ -540,6 +610,8 @@ Return ONLY the expand expression - no $expand= prefix, no explanations.`, searchEntities: { type: 'string', description: 'JSON array of search entity configurations' }, searchMode: { type: 'string', description: 'Search mode: "any" or "all"' }, searchType: { type: 'string', description: 'Query type: "simple" or "lucene"' }, + facets: { type: 'string', description: 'JSON array of facet specifications for search' }, + skip: { type: 'string', description: 'Number of search results to skip for pagination' }, actionName: { type: 'string', description: 'Dataverse action name to execute' }, functionName: { type: 'string', description: 'Dataverse function name to execute' }, functionParameters: { @@ -547,11 +619,22 @@ Return ONLY the expand expression - no $expand= prefix, no explanations.`, description: 'Function parameters as URL-encoded string', }, parameters: { type: 'json', description: 'Action parameters as JSON object' }, - entityLogicalName: { type: 'string', description: 'Table logical name for @odata.type' }, + entityLogicalName: { + type: 'string', + description: 'Table logical name for @odata.type, or to look up table metadata', + }, records: { type: 'json', description: 'Array of record objects for bulk operations' }, fileColumn: { type: 'string', description: 'File or image column logical name' }, fileName: { type: 'string', description: 'Name of the file to upload' }, file: { type: 'json', description: 'File to upload (canonical param)' }, + metadataSelect: { + type: 'string', + description: 'Comma-separated table metadata properties to return', + }, + includeAttributes: { + type: 'string', + description: 'Set to "true" to also return column (attribute) definitions', + }, }, outputs: { records: { type: 'json', description: 'Array of records (list/fetchxml/search)' }, @@ -569,7 +652,8 @@ Return ONLY the expand expression - no $expand= prefix, no explanations.`, organizationId: { type: 'string', description: 'Organization ID (WhoAmI)' }, entitySetName: { type: 'string', - description: 'Source entity set name (associate/disassociate)', + description: + 'Source entity set name (associate/disassociate), or the looked-up entity set name (get table metadata)', }, navigationProperty: { type: 'string', @@ -584,11 +668,39 @@ Return ONLY the expand expression - no $expand= prefix, no explanations.`, moreRecords: { type: 'boolean', description: 'Whether more records are available (FetchXML)' }, results: { type: 'json', description: 'Search results array' }, facets: { type: 'json', description: 'Facet results for search (when facets requested)' }, + file: { type: 'file', description: 'Downloaded file stored in execution files' }, fileContent: { type: 'string', description: 'Base64-encoded downloaded file content' }, - fileName: { type: 'string', description: 'Downloaded file name' }, + fileName: { type: 'string', description: 'Name of the uploaded or downloaded file' }, fileSize: { type: 'number', description: 'File size in bytes' }, mimeType: { type: 'string', description: 'File MIME type' }, - fileColumn: { type: 'string', description: 'File column name' }, + fileColumn: { + type: 'string', + description: 'Logical name of the file column the file was uploaded to or downloaded from', + }, + logicalName: { + type: 'string', + description: 'Singular table logical name (get table metadata)', + }, + displayName: { + type: 'string', + description: 'Localized table display name (get table metadata)', + }, + primaryIdAttribute: { + type: 'string', + description: 'Primary key column logical name (get table metadata)', + }, + primaryNameAttribute: { + type: 'string', + description: 'Primary name column logical name (get table metadata)', + }, + attributes: { + type: 'json', + description: 'Column definitions for the table (get table metadata)', + }, + metadata: { + type: 'json', + description: 'Full raw table metadata response (get table metadata)', + }, }, } @@ -690,5 +802,11 @@ export const MicrosoftDataverseBlockMeta = { content: '# 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.', }, + { + name: 'discover-table-schema', + description: 'Look up a table entity set name and column logical names before writing data.', + content: + '# 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.', + }, ], } as const satisfies BlockMeta diff --git a/apps/sim/tools/microsoft_dataverse/associate.ts b/apps/sim/tools/microsoft_dataverse/associate.ts index 3256803dce6..32978ef631b 100644 --- a/apps/sim/tools/microsoft_dataverse/associate.ts +++ b/apps/sim/tools/microsoft_dataverse/associate.ts @@ -3,6 +3,7 @@ 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') @@ -75,8 +76,8 @@ export const dataverseAssociateTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.environmentUrl.replace(/\/$/, '') - return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})/${params.navigationProperty}/$ref` + 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) => ({ @@ -87,9 +88,9 @@ export const dataverseAssociateTool: ToolConfig< Accept: 'application/json', }), body: (params) => { - const baseUrl = params.environmentUrl.replace(/\/$/, '') + const baseUrl = getDataverseBaseUrl(params.environmentUrl) return { - '@odata.id': `${baseUrl}/api/data/v9.2/${params.targetEntitySetName}(${params.targetRecordId})`, + '@odata.id': `${baseUrl}/api/data/v9.2/${params.targetEntitySetName.trim()}(${params.targetRecordId.trim()})`, } }, }, diff --git a/apps/sim/tools/microsoft_dataverse/create_multiple.ts b/apps/sim/tools/microsoft_dataverse/create_multiple.ts index 5125e849bb1..12db7cdc4d6 100644 --- a/apps/sim/tools/microsoft_dataverse/create_multiple.ts +++ b/apps/sim/tools/microsoft_dataverse/create_multiple.ts @@ -3,6 +3,7 @@ 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') @@ -57,8 +58,8 @@ export const dataverseCreateMultipleTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.environmentUrl.replace(/\/$/, '') - return `${baseUrl}/api/data/v9.2/${params.entitySetName}/Microsoft.Dynamics.CRM.CreateMultiple` + const baseUrl = getDataverseBaseUrl(params.environmentUrl) + return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}/Microsoft.Dynamics.CRM.CreateMultiple` }, method: 'POST', headers: (params) => ({ @@ -80,9 +81,10 @@ export const dataverseCreateMultipleTool: ToolConfig< 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) => ({ ...record, - '@odata.type': `Microsoft.Dynamics.CRM.${params.entityLogicalName}`, + '@odata.type': `Microsoft.Dynamics.CRM.${entityLogicalName}`, })) return { Targets: targets } }, diff --git a/apps/sim/tools/microsoft_dataverse/create_record.ts b/apps/sim/tools/microsoft_dataverse/create_record.ts index dffcd9941f5..35240b41f28 100644 --- a/apps/sim/tools/microsoft_dataverse/create_record.ts +++ b/apps/sim/tools/microsoft_dataverse/create_record.ts @@ -4,6 +4,7 @@ import type { 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') @@ -50,8 +51,8 @@ export const dataverseCreateRecordTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.environmentUrl.replace(/\/$/, '') - return `${baseUrl}/api/data/v9.2/${params.entitySetName}` + const baseUrl = getDataverseBaseUrl(params.environmentUrl) + return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}` }, method: 'POST', headers: (params) => ({ diff --git a/apps/sim/tools/microsoft_dataverse/delete_record.ts b/apps/sim/tools/microsoft_dataverse/delete_record.ts index adb8a06b7df..f9bac250fe4 100644 --- a/apps/sim/tools/microsoft_dataverse/delete_record.ts +++ b/apps/sim/tools/microsoft_dataverse/delete_record.ts @@ -3,6 +3,7 @@ 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') @@ -48,8 +49,8 @@ export const dataverseDeleteRecordTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.environmentUrl.replace(/\/$/, '') - return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})` + const baseUrl = getDataverseBaseUrl(params.environmentUrl) + return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}(${params.recordId.trim()})` }, method: 'DELETE', headers: (params) => ({ diff --git a/apps/sim/tools/microsoft_dataverse/disassociate.ts b/apps/sim/tools/microsoft_dataverse/disassociate.ts index 2aa5798e65e..d74f5a349c3 100644 --- a/apps/sim/tools/microsoft_dataverse/disassociate.ts +++ b/apps/sim/tools/microsoft_dataverse/disassociate.ts @@ -3,6 +3,7 @@ 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') @@ -63,11 +64,14 @@ export const dataverseDisassociateTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.environmentUrl.replace(/\/$/, '') + 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/${params.entitySetName}(${params.recordId})/${params.navigationProperty}(${params.targetRecordId})/$ref` + return `${baseUrl}/api/data/v9.2/${entitySetName}(${recordId})/${navigationProperty}(${params.targetRecordId.trim()})/$ref` } - return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})/${params.navigationProperty}/$ref` + return `${baseUrl}/api/data/v9.2/${entitySetName}(${recordId})/${navigationProperty}/$ref` }, method: 'DELETE', headers: (params) => ({ diff --git a/apps/sim/tools/microsoft_dataverse/download_file.ts b/apps/sim/tools/microsoft_dataverse/download_file.ts index 6d66994c2c4..155374fef27 100644 --- a/apps/sim/tools/microsoft_dataverse/download_file.ts +++ b/apps/sim/tools/microsoft_dataverse/download_file.ts @@ -3,6 +3,7 @@ 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') @@ -14,7 +15,7 @@ export const dataverseDownloadFileTool: ToolConfig< 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. Returns the file content as a base64-encoded string along with file metadata from response headers.', + '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' }, @@ -55,8 +56,8 @@ export const dataverseDownloadFileTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.environmentUrl.replace(/\/$/, '') - return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})/${params.fileColumn}/$value` + 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) => ({ @@ -66,7 +67,7 @@ export const dataverseDownloadFileTool: ToolConfig< }), }, - transformResponse: async (response: Response) => { + transformResponse: async (response: Response, params?: DataverseDownloadFileParams) => { if (!response.ok) { const errorData = await response.json().catch(() => ({})) const errorMessage = @@ -76,30 +77,43 @@ export const dataverseDownloadFileTool: ToolConfig< throw new Error(errorMessage) } - const fileName = response.headers.get('x-ms-file-name') ?? '' + 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') ?? '' + 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: fileSize ? Number.parseInt(fileSize, 10) : buffer.byteLength, + 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' }, }, } diff --git a/apps/sim/tools/microsoft_dataverse/execute_action.ts b/apps/sim/tools/microsoft_dataverse/execute_action.ts index aacaa11f8d5..1a427bb151f 100644 --- a/apps/sim/tools/microsoft_dataverse/execute_action.ts +++ b/apps/sim/tools/microsoft_dataverse/execute_action.ts @@ -3,6 +3,7 @@ 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') @@ -65,14 +66,16 @@ export const dataverseExecuteActionTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.environmentUrl.replace(/\/$/, '') + 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/${params.entitySetName}(${params.recordId})/Microsoft.Dynamics.CRM.${params.actionName}` + return `${baseUrl}/api/data/v9.2/${entitySetName}(${params.recordId.trim()})/Microsoft.Dynamics.CRM.${actionName}` } - return `${baseUrl}/api/data/v9.2/${params.entitySetName}/Microsoft.Dynamics.CRM.${params.actionName}` + return `${baseUrl}/api/data/v9.2/${entitySetName}/Microsoft.Dynamics.CRM.${actionName}` } - return `${baseUrl}/api/data/v9.2/${params.actionName}` + return `${baseUrl}/api/data/v9.2/${actionName}` }, method: 'POST', headers: (params) => ({ diff --git a/apps/sim/tools/microsoft_dataverse/execute_function.ts b/apps/sim/tools/microsoft_dataverse/execute_function.ts index dcd44f791b9..4457af4113b 100644 --- a/apps/sim/tools/microsoft_dataverse/execute_function.ts +++ b/apps/sim/tools/microsoft_dataverse/execute_function.ts @@ -3,6 +3,7 @@ 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') @@ -58,21 +59,28 @@ export const dataverseExecuteFunctionTool: ToolConfig< required: false, visibility: 'user-or-llm', description: - 'Function parameters as a comma-separated list of name=value pairs for the URL (e.g., "LocalizedStandardName=\'Pacific Standard Time\',LocaleId=1033"). Use @p1,@p2 aliases for complex values.', + '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 = params.environmentUrl.replace(/\/$/, '') - const paramStr = params.parameters ? `(${params.parameters})` : '()' + 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/${params.entitySetName}(${params.recordId})/Microsoft.Dynamics.CRM.${params.functionName}${paramStr}` + return `${baseUrl}/api/data/v9.2/${entitySetName}(${params.recordId.trim()})/Microsoft.Dynamics.CRM.${functionName}${paramStr}${querySuffix}` } - return `${baseUrl}/api/data/v9.2/${params.entitySetName}/Microsoft.Dynamics.CRM.${params.functionName}${paramStr}` + return `${baseUrl}/api/data/v9.2/${entitySetName}/Microsoft.Dynamics.CRM.${functionName}${paramStr}${querySuffix}` } - return `${baseUrl}/api/data/v9.2/${params.functionName}${paramStr}` + return `${baseUrl}/api/data/v9.2/${functionName}${paramStr}${querySuffix}` }, method: 'GET', headers: (params) => ({ diff --git a/apps/sim/tools/microsoft_dataverse/fetchxml_query.ts b/apps/sim/tools/microsoft_dataverse/fetchxml_query.ts index 96af993277a..0e6fd37fed4 100644 --- a/apps/sim/tools/microsoft_dataverse/fetchxml_query.ts +++ b/apps/sim/tools/microsoft_dataverse/fetchxml_query.ts @@ -4,6 +4,7 @@ import type { 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') @@ -51,9 +52,9 @@ export const dataverseFetchXmlQueryTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.environmentUrl.replace(/\/$/, '') + const baseUrl = getDataverseBaseUrl(params.environmentUrl) const encodedFetchXml = encodeURIComponent(params.fetchXml) - return `${baseUrl}/api/data/v9.2/${params.entitySetName}?fetchXml=${encodedFetchXml}` + return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}?fetchXml=${encodedFetchXml}` }, method: 'GET', headers: (params) => ({ diff --git a/apps/sim/tools/microsoft_dataverse/get_entity_metadata.ts b/apps/sim/tools/microsoft_dataverse/get_entity_metadata.ts new file mode 100644 index 00000000000..2dcb4d87eba --- /dev/null +++ b/apps/sim/tools/microsoft_dataverse/get_entity_metadata.ts @@ -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' }, + }, +} diff --git a/apps/sim/tools/microsoft_dataverse/get_record.ts b/apps/sim/tools/microsoft_dataverse/get_record.ts index 2f813949837..344405ba28f 100644 --- a/apps/sim/tools/microsoft_dataverse/get_record.ts +++ b/apps/sim/tools/microsoft_dataverse/get_record.ts @@ -4,6 +4,7 @@ import type { 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') @@ -62,12 +63,12 @@ export const dataverseGetRecordTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.environmentUrl.replace(/\/$/, '') + const baseUrl = getDataverseBaseUrl(params.environmentUrl) const queryParts: string[] = [] - if (params.select) queryParts.push(`$select=${params.select}`) - if (params.expand) queryParts.push(`$expand=${params.expand}`) + 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}(${params.recordId})${query}` + return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}(${params.recordId.trim()})${query}` }, method: 'GET', headers: (params) => ({ diff --git a/apps/sim/tools/microsoft_dataverse/index.ts b/apps/sim/tools/microsoft_dataverse/index.ts index df7b0e58803..cd6d7485970 100644 --- a/apps/sim/tools/microsoft_dataverse/index.ts +++ b/apps/sim/tools/microsoft_dataverse/index.ts @@ -7,6 +7,7 @@ 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' diff --git a/apps/sim/tools/microsoft_dataverse/list_records.ts b/apps/sim/tools/microsoft_dataverse/list_records.ts index 0fba025db24..b79bea05084 100644 --- a/apps/sim/tools/microsoft_dataverse/list_records.ts +++ b/apps/sim/tools/microsoft_dataverse/list_records.ts @@ -4,6 +4,7 @@ import type { 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') @@ -80,25 +81,33 @@ export const dataverseListRecordsTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.environmentUrl.replace(/\/$/, '') + const baseUrl = getDataverseBaseUrl(params.environmentUrl) const queryParts: string[] = [] - if (params.select) queryParts.push(`$select=${params.select}`) - if (params.filter) queryParts.push(`$filter=${params.filter}`) - if (params.orderBy) queryParts.push(`$orderby=${params.orderBy}`) + 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=${params.expand}`) + 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}${query}` + return `${baseUrl}/api/data/v9.2/${params.entitySetName.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="*",odata.maxpagesize=100', - }), + 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) => { diff --git a/apps/sim/tools/microsoft_dataverse/search.ts b/apps/sim/tools/microsoft_dataverse/search.ts index 20ecb76baad..d313074925f 100644 --- a/apps/sim/tools/microsoft_dataverse/search.ts +++ b/apps/sim/tools/microsoft_dataverse/search.ts @@ -3,6 +3,7 @@ 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') @@ -93,7 +94,7 @@ export const dataverseSearchTool: ToolConfig { - const baseUrl = params.environmentUrl.replace(/\/$/, '') + const baseUrl = getDataverseBaseUrl(params.environmentUrl) return `${baseUrl}/api/data/v9.2/searchquery` }, method: 'POST', diff --git a/apps/sim/tools/microsoft_dataverse/types.ts b/apps/sim/tools/microsoft_dataverse/types.ts index f7e1e7bcad2..6cdd0c6d650 100644 --- a/apps/sim/tools/microsoft_dataverse/types.ts +++ b/apps/sim/tools/microsoft_dataverse/types.ts @@ -315,10 +315,17 @@ export interface DataverseDownloadFileParams { 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 } } @@ -347,6 +354,27 @@ export interface DataverseSearchResponse extends ToolResponse { } } +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[] + metadata: Record + success: boolean + } +} + export type DataverseResponse = | DataverseCreateRecordResponse | DataverseGetRecordResponse @@ -365,3 +393,4 @@ export type DataverseResponse = | DataverseUploadFileResponse | DataverseDownloadFileResponse | DataverseSearchResponse + | DataverseGetEntityMetadataResponse diff --git a/apps/sim/tools/microsoft_dataverse/update_multiple.ts b/apps/sim/tools/microsoft_dataverse/update_multiple.ts index fd93c7d1124..d0702c1b1c9 100644 --- a/apps/sim/tools/microsoft_dataverse/update_multiple.ts +++ b/apps/sim/tools/microsoft_dataverse/update_multiple.ts @@ -3,6 +3,7 @@ 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') @@ -57,8 +58,8 @@ export const dataverseUpdateMultipleTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.environmentUrl.replace(/\/$/, '') - return `${baseUrl}/api/data/v9.2/${params.entitySetName}/Microsoft.Dynamics.CRM.UpdateMultiple` + const baseUrl = getDataverseBaseUrl(params.environmentUrl) + return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}/Microsoft.Dynamics.CRM.UpdateMultiple` }, method: 'POST', headers: (params) => ({ @@ -80,9 +81,10 @@ export const dataverseUpdateMultipleTool: ToolConfig< 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) => ({ ...record, - '@odata.type': `Microsoft.Dynamics.CRM.${params.entityLogicalName}`, + '@odata.type': `Microsoft.Dynamics.CRM.${entityLogicalName}`, })) return { Targets: targets } }, diff --git a/apps/sim/tools/microsoft_dataverse/update_record.ts b/apps/sim/tools/microsoft_dataverse/update_record.ts index a5af3820b2a..8bf3b1ff793 100644 --- a/apps/sim/tools/microsoft_dataverse/update_record.ts +++ b/apps/sim/tools/microsoft_dataverse/update_record.ts @@ -3,6 +3,7 @@ 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') @@ -55,8 +56,8 @@ export const dataverseUpdateRecordTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.environmentUrl.replace(/\/$/, '') - return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})` + const baseUrl = getDataverseBaseUrl(params.environmentUrl) + return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}(${params.recordId.trim()})` }, method: 'PATCH', headers: (params) => ({ diff --git a/apps/sim/tools/microsoft_dataverse/upsert_record.ts b/apps/sim/tools/microsoft_dataverse/upsert_record.ts index 3fa16d2b2c6..04f82356f24 100644 --- a/apps/sim/tools/microsoft_dataverse/upsert_record.ts +++ b/apps/sim/tools/microsoft_dataverse/upsert_record.ts @@ -4,6 +4,7 @@ import type { 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') @@ -56,8 +57,8 @@ export const dataverseUpsertRecordTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.environmentUrl.replace(/\/$/, '') - return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})` + const baseUrl = getDataverseBaseUrl(params.environmentUrl) + return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}(${params.recordId.trim()})` }, method: 'PATCH', headers: (params) => ({ diff --git a/apps/sim/tools/microsoft_dataverse/utils.ts b/apps/sim/tools/microsoft_dataverse/utils.ts new file mode 100644 index 00000000000..9e6e7862dca --- /dev/null +++ b/apps/sim/tools/microsoft_dataverse/utils.ts @@ -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(/\/$/, '') +} diff --git a/apps/sim/tools/microsoft_dataverse/whoami.ts b/apps/sim/tools/microsoft_dataverse/whoami.ts index 7d70e5ee124..d78174ecd42 100644 --- a/apps/sim/tools/microsoft_dataverse/whoami.ts +++ b/apps/sim/tools/microsoft_dataverse/whoami.ts @@ -3,6 +3,7 @@ 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') @@ -34,7 +35,7 @@ export const dataverseWhoAmITool: ToolConfig { - const baseUrl = params.environmentUrl.replace(/\/$/, '') + const baseUrl = getDataverseBaseUrl(params.environmentUrl) return `${baseUrl}/api/data/v9.2/WhoAmI()` }, method: 'GET', diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 02c6f7c8e7b..a5902cb1bc5 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -2297,6 +2297,7 @@ import { dataverseExecuteActionTool, dataverseExecuteFunctionTool, dataverseFetchXmlQueryTool, + dataverseGetEntityMetadataTool, dataverseGetRecordTool, dataverseListRecordsTool, dataverseSearchTool, @@ -7584,6 +7585,7 @@ export const tools: Record = { microsoft_dataverse_execute_action: dataverseExecuteActionTool, microsoft_dataverse_execute_function: dataverseExecuteFunctionTool, microsoft_dataverse_fetchxml_query: dataverseFetchXmlQueryTool, + microsoft_dataverse_get_entity_metadata: dataverseGetEntityMetadataTool, microsoft_dataverse_get_record: dataverseGetRecordTool, microsoft_dataverse_list_records: dataverseListRecordsTool, microsoft_dataverse_search: dataverseSearchTool,