Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions apps/sim/app/api/tools/microsoft-dataverse/upload-file/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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,
Expand Down
138 changes: 128 additions & 10 deletions apps/sim/blocks/blocks/microsoft_dataverse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const MicrosoftDataverseBlock: BlockConfig<DataverseResponse> = {
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,
Expand Down Expand Up @@ -39,6 +39,7 @@ export const MicrosoftDataverseBlock: BlockConfig<DataverseResponse> = {
{ 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',
Expand Down Expand Up @@ -66,12 +67,12 @@ export const MicrosoftDataverseBlock: BlockConfig<DataverseResponse> = {
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,
},
},
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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
Comment thread
waleedlatif1 marked this conversation as resolved.

Object.entries(rest).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
Expand All @@ -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',
Expand All @@ -540,18 +610,31 @@ 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: {
type: 'string',
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)' },
Expand All @@ -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',
Expand All @@ -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)',
},
},
}

Expand Down Expand Up @@ -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
9 changes: 5 additions & 4 deletions apps/sim/tools/microsoft_dataverse/associate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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) => ({
Expand All @@ -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()})`,
}
},
},
Expand Down
8 changes: 5 additions & 3 deletions apps/sim/tools/microsoft_dataverse/create_multiple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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) => ({
Expand All @@ -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<string, unknown>) => ({
...record,
'@odata.type': `Microsoft.Dynamics.CRM.${params.entityLogicalName}`,
'@odata.type': `Microsoft.Dynamics.CRM.${entityLogicalName}`,
}))
return { Targets: targets }
},
Expand Down
5 changes: 3 additions & 2 deletions apps/sim/tools/microsoft_dataverse/create_record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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) => ({
Expand Down
5 changes: 3 additions & 2 deletions apps/sim/tools/microsoft_dataverse/delete_record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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) => ({
Expand Down
Loading
Loading