mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-21 13:00:04 +08:00
feat(salesforce): add Tooling API schema tools (custom field/object) + metadata query (#5209)
* feat(salesforce): add Tooling API schema tools (custom field/object) + metadata query Add salesforce_create_custom_field, salesforce_update_custom_field, salesforce_delete_custom_field, salesforce_create_custom_object, and salesforce_tooling_query so the connector can make schema/metadata changes (e.g. create a custom field on Account). Previously the integration only did record CRUD via the REST Data API. Existing `api` OAuth scope covers the Tooling API; metadata creation is profile-permission gated, so no scope change. Also: fix Opportunity closeDate being wrongly required on update_opportunity, make list_reports/list_dashboards descriptions honest (recently-viewed scope), and document run_report's includeDetails default. * improvement(salesforce): non-destructive custom field update + align metadata param types - update_custom_field now does a read-modify-write (GET existing Metadata, overlay only provided changes, PATCH) so omitted properties are preserved instead of being reset by the Tooling API's full-metadata PATCH; no more fabricated label or injected create-time defaults on update - fieldType is now optional on update (kept from the existing field unless changed) - widen length/precision/scale/visibleLines param types to number | string to match the tool param configs (type: number) * improvement(salesforce): preserve picklist values and clear stale metadata on field type change - custom field update now unions provided picklist values with the field's existing values instead of replacing the whole valueSet (no data loss) - when fieldType changes on update, drop the prior type's type-specific metadata (length/precision/scale/visibleLines/valueSet/defaultValue/unique/ externalId) and backfill the new type's required defaults * improvement(salesforce): scope custom field update to attributes, never the type update_custom_field no longer changes a field's data type: Salesforce treats a type change as a separate conversion operation, and a stale forwarded fieldType could otherwise trigger an unintended destructive migration. The merge keeps the field's existing type and overlays only the other provided properties, dropping the type-change/stale-metadata-stripping logic entirely.
This commit is contained in:
@@ -27,7 +27,7 @@ The Salesforce tool is ideal for workflows where your agents need to streamline
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Salesforce into your workflow. Manage accounts, contacts, leads, opportunities, cases, and tasks with powerful automation capabilities.
|
||||
Integrate Salesforce into your workflow. Manage accounts, contacts, leads, opportunities, cases, and tasks, run reports and SOQL queries, and manage org schema by creating custom fields and objects via the Tooling API.
|
||||
|
||||
|
||||
|
||||
@@ -717,7 +717,7 @@ Delete a task
|
||||
|
||||
### `salesforce_list_reports`
|
||||
|
||||
Get a list of reports accessible by the current user
|
||||
Get a list of up to 200 recently viewed reports for the current user
|
||||
|
||||
#### Input
|
||||
|
||||
@@ -814,7 +814,7 @@ Get a list of available report types
|
||||
|
||||
### `salesforce_list_dashboards`
|
||||
|
||||
Get a list of dashboards accessible by the current user
|
||||
Get a list of recently used dashboards for the current user
|
||||
|
||||
#### Input
|
||||
|
||||
@@ -1029,6 +1029,150 @@ Get a list of all available Salesforce objects
|
||||
| ↳ `totalReturned` | number | Number of objects returned |
|
||||
| ↳ `success` | boolean | Salesforce operation success |
|
||||
|
||||
### `salesforce_create_custom_field`
|
||||
|
||||
Create a custom field on a Salesforce object (e.g., Account) using the Tooling API
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `idToken` | string | No | No description |
|
||||
| `instanceUrl` | string | No | No description |
|
||||
| `objectName` | string | Yes | API name of the object to add the field to \(e.g., Account, Contact, Lead, MyObject__c\) |
|
||||
| `fieldName` | string | Yes | API name of the new field; the __c suffix is added automatically \(e.g., Region\) |
|
||||
| `label` | string | No | Display label shown in the UI \(defaults to the field name when omitted\) |
|
||||
| `fieldType` | string | Yes | Field data type: Text, TextArea, LongTextArea, Html, Number, Currency, Percent, Checkbox, Date, DateTime, Time, Phone, Email, Url, Picklist, or MultiselectPicklist |
|
||||
| `length` | number | No | Maximum length for Text \(1-255\), LongTextArea, Html, or MultiselectPicklist fields |
|
||||
| `precision` | number | No | Total number of digits for Number, Currency, or Percent fields \(1-18\) |
|
||||
| `scale` | number | No | Number of digits to the right of the decimal for numeric fields |
|
||||
| `visibleLines` | number | No | Number of visible lines for LongTextArea, Html, or MultiselectPicklist fields |
|
||||
| `required` | boolean | No | Whether the field is required on record create/edit |
|
||||
| `unique` | boolean | No | Whether the field enforces unique values |
|
||||
| `externalId` | boolean | No | Whether the field is an external ID \(for Text, Number, or Email fields\) |
|
||||
| `defaultValue` | string | No | Default value; for Checkbox fields use true or false |
|
||||
| `description` | string | No | Internal description of the field |
|
||||
| `inlineHelpText` | string | No | Help text shown next to the field in the UI |
|
||||
| `picklistValues` | string | No | Comma-separated values for Picklist or MultiselectPicklist fields |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Created custom field metadata |
|
||||
| ↳ `id` | string | Tooling API Id of the newly created custom field |
|
||||
| ↳ `fullName` | string | Full API name of the field, including object \(e.g., Account.Region__c\) |
|
||||
| ↳ `success` | boolean | Whether the create operation was successful |
|
||||
| ↳ `created` | boolean | Whether the field was created \(always true on success\) |
|
||||
|
||||
### `salesforce_update_custom_field`
|
||||
|
||||
Update an existing custom field on a Salesforce object using the Tooling API
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `idToken` | string | No | No description |
|
||||
| `instanceUrl` | string | No | No description |
|
||||
| `fieldId` | string | Yes | Tooling API Id of the custom field to update \(find it via the Tooling Query tool\) |
|
||||
| `label` | string | No | Display label shown in the UI |
|
||||
| `length` | number | No | Maximum length for Text, LongTextArea, Html, or MultiselectPicklist fields |
|
||||
| `precision` | number | No | Total number of digits for Number, Currency, or Percent fields |
|
||||
| `scale` | number | No | Number of digits to the right of the decimal for numeric fields |
|
||||
| `visibleLines` | number | No | Number of visible lines for LongTextArea, Html, or MultiselectPicklist fields |
|
||||
| `required` | boolean | No | Whether the field is required on record create/edit |
|
||||
| `unique` | boolean | No | Whether the field enforces unique values |
|
||||
| `externalId` | boolean | No | Whether the field is an external ID |
|
||||
| `defaultValue` | string | No | Default value; for Checkbox fields use true or false |
|
||||
| `description` | string | No | Internal description of the field |
|
||||
| `inlineHelpText` | string | No | Help text shown next to the field in the UI |
|
||||
| `picklistValues` | string | No | Comma-separated values to add to a Picklist or MultiselectPicklist field \(existing values are kept\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Updated custom field metadata |
|
||||
| ↳ `id` | string | Tooling API Id of the updated custom field |
|
||||
| ↳ `updated` | boolean | Whether the field was updated \(always true on success\) |
|
||||
|
||||
### `salesforce_delete_custom_field`
|
||||
|
||||
Delete a custom field from a Salesforce object using the Tooling API
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `idToken` | string | No | No description |
|
||||
| `instanceUrl` | string | No | No description |
|
||||
| `fieldId` | string | Yes | Tooling API Id of the custom field to delete \(find it via the Tooling Query tool\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Deleted custom field metadata |
|
||||
| ↳ `id` | string | Tooling API Id of the deleted custom field |
|
||||
| ↳ `deleted` | boolean | Whether the field was deleted \(always true on success\) |
|
||||
|
||||
### `salesforce_create_custom_object`
|
||||
|
||||
Create a custom object in Salesforce using the Tooling API
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `idToken` | string | No | No description |
|
||||
| `instanceUrl` | string | No | No description |
|
||||
| `objectName` | string | Yes | API name of the new object; the __c suffix is added automatically \(e.g., Project\) |
|
||||
| `label` | string | Yes | Singular display label for the object \(e.g., Project\) |
|
||||
| `pluralLabel` | string | Yes | Plural display label for the object \(e.g., Projects\) |
|
||||
| `nameFieldLabel` | string | No | Label for the standard Name field \(defaults to "<label> Name"\) |
|
||||
| `description` | string | No | Internal description of the object |
|
||||
| `sharingModel` | string | No | Org-wide sharing model: ReadWrite, Read, Private, or ControlledByParent \(default ReadWrite\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Created custom object metadata |
|
||||
| ↳ `id` | string | Tooling API Id of the newly created custom object |
|
||||
| ↳ `fullName` | string | Full API name of the object \(e.g., Project__c\) |
|
||||
| ↳ `success` | boolean | Whether the create operation was successful |
|
||||
| ↳ `created` | boolean | Whether the object was created \(always true on success\) |
|
||||
|
||||
### `salesforce_tooling_query`
|
||||
|
||||
Execute a SOQL query against the Tooling API to inspect metadata objects
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `idToken` | string | No | No description |
|
||||
| `instanceUrl` | string | No | No description |
|
||||
| `query` | string | Yes | Tooling SOQL query \(e.g., SELECT Id, DeveloperName FROM CustomField WHERE TableEnumOrId = 'Account'\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Tooling query results |
|
||||
| ↳ `records` | array | Array of Tooling API records matching the query |
|
||||
| ↳ `query` | string | The executed Tooling SOQL query |
|
||||
| ↳ `metadata` | object | Response metadata |
|
||||
| ↳ `totalReturned` | number | Number of records returned in this response |
|
||||
| ↳ `hasMore` | boolean | Whether more records exist \(inverse of done\) |
|
||||
| ↳ `success` | boolean | Salesforce operation success |
|
||||
|
||||
|
||||
|
||||
## Triggers
|
||||
|
||||
@@ -11,7 +11,7 @@ export const SalesforceBlock: BlockConfig<SalesforceResponse> = {
|
||||
description: 'Interact with Salesforce CRM',
|
||||
authMode: AuthMode.OAuth,
|
||||
longDescription:
|
||||
'Integrate Salesforce into your workflow. Manage accounts, contacts, leads, opportunities, cases, and tasks with powerful automation capabilities.',
|
||||
'Integrate Salesforce into your workflow. Manage accounts, contacts, leads, opportunities, cases, and tasks, run reports and SOQL queries, and manage org schema by creating custom fields and objects via the Tooling API.',
|
||||
docsLink: 'https://docs.sim.ai/integrations/salesforce',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.Sales,
|
||||
@@ -69,6 +69,11 @@ export const SalesforceBlock: BlockConfig<SalesforceResponse> = {
|
||||
{ label: 'Get More Query Results', id: 'query_more' },
|
||||
{ label: 'Describe Object', id: 'describe_object' },
|
||||
{ label: 'List Objects', id: 'list_objects' },
|
||||
{ label: 'Create Custom Field', id: 'create_custom_field' },
|
||||
{ label: 'Update Custom Field', id: 'update_custom_field' },
|
||||
{ label: 'Delete Custom Field', id: 'delete_custom_field' },
|
||||
{ label: 'Create Custom Object', id: 'create_custom_object' },
|
||||
{ label: 'Run Tooling Query', id: 'tooling_query' },
|
||||
],
|
||||
value: () => 'get_accounts',
|
||||
},
|
||||
@@ -445,7 +450,7 @@ export const SalesforceBlock: BlockConfig<SalesforceResponse> = {
|
||||
type: 'short-input',
|
||||
placeholder: 'YYYY-MM-DD (required for create)',
|
||||
condition: { field: 'operation', value: ['create_opportunity', 'update_opportunity'] },
|
||||
required: true,
|
||||
required: { field: 'operation', value: ['create_opportunity'] },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate a date in YYYY-MM-DD format based on the user's description.
|
||||
@@ -608,8 +613,8 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
title: 'SOQL Query',
|
||||
type: 'long-input',
|
||||
placeholder: 'SELECT Id, Name FROM Account LIMIT 10',
|
||||
condition: { field: 'operation', value: ['query'] },
|
||||
required: true,
|
||||
condition: { field: 'operation', value: ['query', 'tooling_query'] },
|
||||
required: { field: 'operation', value: ['query', 'tooling_query'] },
|
||||
},
|
||||
{
|
||||
id: 'nextRecordsUrl',
|
||||
@@ -624,8 +629,14 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
title: 'Object Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'API name (e.g., Account, Lead, Custom_Object__c)',
|
||||
condition: { field: 'operation', value: ['describe_object'] },
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['describe_object', 'create_custom_field', 'create_custom_object'],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: ['describe_object', 'create_custom_field', 'create_custom_object'],
|
||||
},
|
||||
},
|
||||
// Long-input fields at the bottom
|
||||
{
|
||||
@@ -649,9 +660,182 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
'update_case',
|
||||
'create_task',
|
||||
'update_task',
|
||||
'create_custom_field',
|
||||
'update_custom_field',
|
||||
'create_custom_object',
|
||||
],
|
||||
},
|
||||
},
|
||||
// Schema / metadata fields (Tooling API)
|
||||
{
|
||||
id: 'fieldName',
|
||||
title: 'Field Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'API name without __c (e.g., Region)',
|
||||
condition: { field: 'operation', value: ['create_custom_field'] },
|
||||
required: { field: 'operation', value: ['create_custom_field'] },
|
||||
},
|
||||
{
|
||||
id: 'fieldId',
|
||||
title: 'Field ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Tooling API Id (find via Run Tooling Query)',
|
||||
condition: { field: 'operation', value: ['update_custom_field', 'delete_custom_field'] },
|
||||
required: { field: 'operation', value: ['update_custom_field', 'delete_custom_field'] },
|
||||
},
|
||||
{
|
||||
id: 'fieldType',
|
||||
title: 'Field Type',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Text', id: 'Text' },
|
||||
{ label: 'Text Area', id: 'TextArea' },
|
||||
{ label: 'Text Area (Long)', id: 'LongTextArea' },
|
||||
{ label: 'Rich Text Area', id: 'Html' },
|
||||
{ label: 'Number', id: 'Number' },
|
||||
{ label: 'Currency', id: 'Currency' },
|
||||
{ label: 'Percent', id: 'Percent' },
|
||||
{ label: 'Checkbox', id: 'Checkbox' },
|
||||
{ label: 'Date', id: 'Date' },
|
||||
{ label: 'Date/Time', id: 'DateTime' },
|
||||
{ label: 'Time', id: 'Time' },
|
||||
{ label: 'Phone', id: 'Phone' },
|
||||
{ label: 'Email', id: 'Email' },
|
||||
{ label: 'URL', id: 'Url' },
|
||||
{ label: 'Picklist', id: 'Picklist' },
|
||||
{ label: 'Picklist (Multi-Select)', id: 'MultiselectPicklist' },
|
||||
],
|
||||
condition: { field: 'operation', value: ['create_custom_field'] },
|
||||
required: { field: 'operation', value: ['create_custom_field'] },
|
||||
},
|
||||
{
|
||||
id: 'label',
|
||||
title: 'Label',
|
||||
type: 'short-input',
|
||||
placeholder: 'Display label',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['create_custom_field', 'update_custom_field', 'create_custom_object'],
|
||||
},
|
||||
required: { field: 'operation', value: ['create_custom_object'] },
|
||||
},
|
||||
{
|
||||
id: 'pluralLabel',
|
||||
title: 'Plural Label',
|
||||
type: 'short-input',
|
||||
placeholder: 'Plural display label (e.g., Projects)',
|
||||
condition: { field: 'operation', value: ['create_custom_object'] },
|
||||
required: { field: 'operation', value: ['create_custom_object'] },
|
||||
},
|
||||
{
|
||||
id: 'picklistValues',
|
||||
title: 'Picklist Values',
|
||||
type: 'short-input',
|
||||
placeholder: 'Comma-separated values (e.g., Low, Medium, High)',
|
||||
condition: { field: 'operation', value: ['create_custom_field', 'update_custom_field'] },
|
||||
},
|
||||
{
|
||||
id: 'length',
|
||||
title: 'Length',
|
||||
type: 'short-input',
|
||||
placeholder: 'Max length for Text/LongTextArea/Html',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: ['create_custom_field', 'update_custom_field'] },
|
||||
},
|
||||
{
|
||||
id: 'precision',
|
||||
title: 'Precision',
|
||||
type: 'short-input',
|
||||
placeholder: 'Total digits for Number/Currency/Percent',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: ['create_custom_field', 'update_custom_field'] },
|
||||
},
|
||||
{
|
||||
id: 'scale',
|
||||
title: 'Scale',
|
||||
type: 'short-input',
|
||||
placeholder: 'Decimal places for numeric fields',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: ['create_custom_field', 'update_custom_field'] },
|
||||
},
|
||||
{
|
||||
id: 'visibleLines',
|
||||
title: 'Visible Lines',
|
||||
type: 'short-input',
|
||||
placeholder: 'Lines for LongTextArea/Html/MultiselectPicklist',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: ['create_custom_field', 'update_custom_field'] },
|
||||
},
|
||||
{
|
||||
id: 'defaultValue',
|
||||
title: 'Default Value',
|
||||
type: 'short-input',
|
||||
placeholder: 'Default value (true/false for Checkbox)',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: ['create_custom_field', 'update_custom_field'] },
|
||||
},
|
||||
{
|
||||
id: 'inlineHelpText',
|
||||
title: 'Help Text',
|
||||
type: 'short-input',
|
||||
placeholder: 'Help text shown next to the field',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: ['create_custom_field', 'update_custom_field'] },
|
||||
},
|
||||
{
|
||||
id: 'required',
|
||||
title: 'Required',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Yes', id: 'true' },
|
||||
{ label: 'No', id: 'false' },
|
||||
],
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: ['create_custom_field', 'update_custom_field'] },
|
||||
},
|
||||
{
|
||||
id: 'unique',
|
||||
title: 'Unique',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Yes', id: 'true' },
|
||||
{ label: 'No', id: 'false' },
|
||||
],
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: ['create_custom_field', 'update_custom_field'] },
|
||||
},
|
||||
{
|
||||
id: 'externalId',
|
||||
title: 'External ID',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Yes', id: 'true' },
|
||||
{ label: 'No', id: 'false' },
|
||||
],
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: ['create_custom_field', 'update_custom_field'] },
|
||||
},
|
||||
{
|
||||
id: 'nameFieldLabel',
|
||||
title: 'Name Field Label',
|
||||
type: 'short-input',
|
||||
placeholder: 'Label for the Name field (defaults to "<label> Name")',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: ['create_custom_object'] },
|
||||
},
|
||||
{
|
||||
id: 'sharingModel',
|
||||
title: 'Sharing Model',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Read/Write', id: 'ReadWrite' },
|
||||
{ label: 'Read Only', id: 'Read' },
|
||||
{ label: 'Private', id: 'Private' },
|
||||
{ label: 'Controlled By Parent', id: 'ControlledByParent' },
|
||||
],
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: ['create_custom_object'] },
|
||||
},
|
||||
...getTrigger('salesforce_record_created').subBlocks,
|
||||
...getTrigger('salesforce_record_updated').subBlocks,
|
||||
...getTrigger('salesforce_record_deleted').subBlocks,
|
||||
@@ -696,6 +880,11 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
'salesforce_query_more',
|
||||
'salesforce_describe_object',
|
||||
'salesforce_list_objects',
|
||||
'salesforce_create_custom_field',
|
||||
'salesforce_update_custom_field',
|
||||
'salesforce_delete_custom_field',
|
||||
'salesforce_create_custom_object',
|
||||
'salesforce_tooling_query',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
@@ -770,6 +959,16 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
return 'salesforce_describe_object'
|
||||
case 'list_objects':
|
||||
return 'salesforce_list_objects'
|
||||
case 'create_custom_field':
|
||||
return 'salesforce_create_custom_field'
|
||||
case 'update_custom_field':
|
||||
return 'salesforce_update_custom_field'
|
||||
case 'delete_custom_field':
|
||||
return 'salesforce_delete_custom_field'
|
||||
case 'create_custom_object':
|
||||
return 'salesforce_create_custom_object'
|
||||
case 'tooling_query':
|
||||
return 'salesforce_tooling_query'
|
||||
default:
|
||||
throw new Error(`Unknown operation: ${params.operation}`)
|
||||
}
|
||||
|
||||
@@ -13602,7 +13602,7 @@
|
||||
"slug": "salesforce",
|
||||
"name": "Salesforce",
|
||||
"description": "Interact with Salesforce CRM",
|
||||
"longDescription": "Integrate Salesforce into your workflow. Manage accounts, contacts, leads, opportunities, cases, and tasks with powerful automation capabilities.",
|
||||
"longDescription": "Integrate Salesforce into your workflow. Manage accounts, contacts, leads, opportunities, cases, and tasks, run reports and SOQL queries, and manage org schema by creating custom fields and objects via the Tooling API.",
|
||||
"bgColor": "#FFFFFF",
|
||||
"iconName": "SalesforceIcon",
|
||||
"docsUrl": "https://docs.sim.ai/integrations/salesforce",
|
||||
@@ -13705,7 +13705,7 @@
|
||||
},
|
||||
{
|
||||
"name": "List Reports",
|
||||
"description": "Get a list of reports accessible by the current user"
|
||||
"description": "Get a list of up to 200 recently viewed reports for the current user"
|
||||
},
|
||||
{
|
||||
"name": "Get Report",
|
||||
@@ -13721,7 +13721,7 @@
|
||||
},
|
||||
{
|
||||
"name": "List Dashboards",
|
||||
"description": "Get a list of dashboards accessible by the current user"
|
||||
"description": "Get a list of recently used dashboards for the current user"
|
||||
},
|
||||
{
|
||||
"name": "Get Dashboard",
|
||||
@@ -13746,9 +13746,29 @@
|
||||
{
|
||||
"name": "List Objects",
|
||||
"description": "Get a list of all available Salesforce objects"
|
||||
},
|
||||
{
|
||||
"name": "Create Custom Field",
|
||||
"description": "Create a custom field on a Salesforce object (e.g., Account) using the Tooling API"
|
||||
},
|
||||
{
|
||||
"name": "Update Custom Field",
|
||||
"description": "Update an existing custom field on a Salesforce object using the Tooling API"
|
||||
},
|
||||
{
|
||||
"name": "Delete Custom Field",
|
||||
"description": "Delete a custom field from a Salesforce object using the Tooling API"
|
||||
},
|
||||
{
|
||||
"name": "Create Custom Object",
|
||||
"description": "Create a custom object in Salesforce using the Tooling API"
|
||||
},
|
||||
{
|
||||
"name": "Run Tooling Query",
|
||||
"description": "Execute a SOQL query against the Tooling API to inspect metadata objects"
|
||||
}
|
||||
],
|
||||
"operationCount": 35,
|
||||
"operationCount": 40,
|
||||
"triggers": [
|
||||
{
|
||||
"id": "salesforce_record_created",
|
||||
|
||||
@@ -2808,12 +2808,15 @@ import {
|
||||
salesforceCreateAccountTool,
|
||||
salesforceCreateCaseTool,
|
||||
salesforceCreateContactTool,
|
||||
salesforceCreateCustomFieldTool,
|
||||
salesforceCreateCustomObjectTool,
|
||||
salesforceCreateLeadTool,
|
||||
salesforceCreateOpportunityTool,
|
||||
salesforceCreateTaskTool,
|
||||
salesforceDeleteAccountTool,
|
||||
salesforceDeleteCaseTool,
|
||||
salesforceDeleteContactTool,
|
||||
salesforceDeleteCustomFieldTool,
|
||||
salesforceDeleteLeadTool,
|
||||
salesforceDeleteOpportunityTool,
|
||||
salesforceDeleteTaskTool,
|
||||
@@ -2834,9 +2837,11 @@ import {
|
||||
salesforceQueryTool,
|
||||
salesforceRefreshDashboardTool,
|
||||
salesforceRunReportTool,
|
||||
salesforceToolingQueryTool,
|
||||
salesforceUpdateAccountTool,
|
||||
salesforceUpdateCaseTool,
|
||||
salesforceUpdateContactTool,
|
||||
salesforceUpdateCustomFieldTool,
|
||||
salesforceUpdateLeadTool,
|
||||
salesforceUpdateOpportunityTool,
|
||||
salesforceUpdateTaskTool,
|
||||
@@ -7301,6 +7306,11 @@ export const tools: Record<string, ToolConfig> = {
|
||||
salesforce_query_more: salesforceQueryMoreTool,
|
||||
salesforce_describe_object: salesforceDescribeObjectTool,
|
||||
salesforce_list_objects: salesforceListObjectsTool,
|
||||
salesforce_create_custom_field: salesforceCreateCustomFieldTool,
|
||||
salesforce_update_custom_field: salesforceUpdateCustomFieldTool,
|
||||
salesforce_delete_custom_field: salesforceDeleteCustomFieldTool,
|
||||
salesforce_create_custom_object: salesforceCreateCustomObjectTool,
|
||||
salesforce_tooling_query: salesforceToolingQueryTool,
|
||||
sap_concur_approve_expense_report: sapConcurApproveExpenseReportTool,
|
||||
sap_concur_associate_attendees: sapConcurAssociateAttendeesTool,
|
||||
sap_concur_create_cash_advance: sapConcurCreateCashAdvanceTool,
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
SalesforceCreateCustomFieldParams,
|
||||
SalesforceCreateCustomFieldResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { CUSTOM_FIELD_CREATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import {
|
||||
buildCustomFieldMetadata,
|
||||
extractErrorMessage,
|
||||
getInstanceUrl,
|
||||
requireId,
|
||||
toCustomApiName,
|
||||
} from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SalesforceCreateCustomField')
|
||||
|
||||
/**
|
||||
* Create a custom field on a Salesforce object (standard or custom) via the
|
||||
* Tooling API. This is a schema/metadata change — distinct from record CRUD —
|
||||
* and requires the user to have the "Customize Application" permission.
|
||||
* @see https://developer.salesforce.com/docs/atlas.en-us.api_tooling.meta/api_tooling/tooling_api_objects_customfield.htm
|
||||
*/
|
||||
export const salesforceCreateCustomFieldTool: ToolConfig<
|
||||
SalesforceCreateCustomFieldParams,
|
||||
SalesforceCreateCustomFieldResponse
|
||||
> = {
|
||||
id: 'salesforce_create_custom_field',
|
||||
name: 'Create Custom Field in Salesforce',
|
||||
description: 'Create a custom field on a Salesforce object (e.g., Account) using the Tooling API',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'salesforce',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: { type: 'string', required: true, visibility: 'hidden' },
|
||||
idToken: { type: 'string', required: false, visibility: 'hidden' },
|
||||
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
|
||||
objectName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'API name of the object to add the field to (e.g., Account, Contact, Lead, MyObject__c)',
|
||||
},
|
||||
fieldName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'API name of the new field; the __c suffix is added automatically (e.g., Region)',
|
||||
},
|
||||
label: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Display label shown in the UI (defaults to the field name when omitted)',
|
||||
},
|
||||
fieldType: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Field data type: Text, TextArea, LongTextArea, Html, Number, Currency, Percent, Checkbox, Date, DateTime, Time, Phone, Email, Url, Picklist, or MultiselectPicklist',
|
||||
},
|
||||
length: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Maximum length for Text (1-255), LongTextArea, Html, or MultiselectPicklist fields',
|
||||
},
|
||||
precision: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Total number of digits for Number, Currency, or Percent fields (1-18)',
|
||||
},
|
||||
scale: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of digits to the right of the decimal for numeric fields',
|
||||
},
|
||||
visibleLines: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of visible lines for LongTextArea, Html, or MultiselectPicklist fields',
|
||||
},
|
||||
required: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether the field is required on record create/edit',
|
||||
},
|
||||
unique: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether the field enforces unique values',
|
||||
},
|
||||
externalId: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether the field is an external ID (for Text, Number, or Email fields)',
|
||||
},
|
||||
defaultValue: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Default value; for Checkbox fields use true or false',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Internal description of the field',
|
||||
},
|
||||
inlineHelpText: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Help text shown next to the field in the UI',
|
||||
},
|
||||
picklistValues: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated values for Picklist or MultiselectPicklist fields',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
|
||||
return `${instanceUrl}/services/data/v59.0/tooling/sobjects/CustomField`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
const objectName = requireId(params.objectName, 'Object Name')
|
||||
const fieldApiName = toCustomApiName(params.fieldName, 'Field Name')
|
||||
const fallbackLabel = fieldApiName.replace(/__c$/, '').replace(/_/g, ' ')
|
||||
return {
|
||||
FullName: `${objectName}.${fieldApiName}`,
|
||||
Metadata: buildCustomFieldMetadata(params, fallbackLabel),
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok || data?.success === false) {
|
||||
const errorMessage = extractErrorMessage(
|
||||
data,
|
||||
response.status,
|
||||
'Failed to create custom field in Salesforce'
|
||||
)
|
||||
logger.error('Failed to create custom field', { data, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const objectName = params?.objectName?.trim() ?? ''
|
||||
const fieldApiName = params?.fieldName ? toCustomApiName(params.fieldName, 'Field Name') : ''
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id,
|
||||
fullName: objectName && fieldApiName ? `${objectName}.${fieldApiName}` : '',
|
||||
success: data.success === true,
|
||||
created: data.success === true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Created custom field metadata',
|
||||
properties: CUSTOM_FIELD_CREATE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
SalesforceCreateCustomObjectParams,
|
||||
SalesforceCreateCustomObjectResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { CUSTOM_OBJECT_CREATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl, toCustomApiName } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SalesforceCreateCustomObject')
|
||||
|
||||
/**
|
||||
* Create a custom object via the Tooling API. The object is created with a
|
||||
* Text Name field and deployed immediately. Custom fields can then be added
|
||||
* with the Create Custom Field tool.
|
||||
* @see https://developer.salesforce.com/docs/atlas.en-us.api_tooling.meta/api_tooling/tooling_api_objects_customobject.htm
|
||||
*/
|
||||
export const salesforceCreateCustomObjectTool: ToolConfig<
|
||||
SalesforceCreateCustomObjectParams,
|
||||
SalesforceCreateCustomObjectResponse
|
||||
> = {
|
||||
id: 'salesforce_create_custom_object',
|
||||
name: 'Create Custom Object in Salesforce',
|
||||
description: 'Create a custom object in Salesforce using the Tooling API',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'salesforce',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: { type: 'string', required: true, visibility: 'hidden' },
|
||||
idToken: { type: 'string', required: false, visibility: 'hidden' },
|
||||
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
|
||||
objectName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'API name of the new object; the __c suffix is added automatically (e.g., Project)',
|
||||
},
|
||||
label: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Singular display label for the object (e.g., Project)',
|
||||
},
|
||||
pluralLabel: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Plural display label for the object (e.g., Projects)',
|
||||
},
|
||||
nameFieldLabel: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Label for the standard Name field (defaults to "<label> Name")',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Internal description of the object',
|
||||
},
|
||||
sharingModel: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Org-wide sharing model: ReadWrite, Read, Private, or ControlledByParent (default ReadWrite)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
|
||||
return `${instanceUrl}/services/data/v59.0/tooling/sobjects/CustomObject`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
const objectApiName = toCustomApiName(params.objectName, 'Object Name')
|
||||
const label = params.label?.trim()
|
||||
const pluralLabel = params.pluralLabel?.trim()
|
||||
if (!label) throw new Error('Label is required to create a custom object.')
|
||||
if (!pluralLabel) throw new Error('Plural Label is required to create a custom object.')
|
||||
|
||||
const metadata: Record<string, any> = {
|
||||
label,
|
||||
pluralLabel,
|
||||
nameField: {
|
||||
type: 'Text',
|
||||
label: params.nameFieldLabel?.trim() || `${label} Name`,
|
||||
},
|
||||
deploymentStatus: 'Deployed',
|
||||
sharingModel: params.sharingModel?.trim() || 'ReadWrite',
|
||||
}
|
||||
if (params.description?.trim()) metadata.description = params.description.trim()
|
||||
|
||||
return {
|
||||
FullName: objectApiName,
|
||||
Metadata: metadata,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok || data?.success === false) {
|
||||
const errorMessage = extractErrorMessage(
|
||||
data,
|
||||
response.status,
|
||||
'Failed to create custom object in Salesforce'
|
||||
)
|
||||
logger.error('Failed to create custom object', { data, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id,
|
||||
fullName: params?.objectName ? toCustomApiName(params.objectName, 'Object Name') : '',
|
||||
success: data.success === true,
|
||||
created: data.success === true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Created custom object metadata',
|
||||
properties: CUSTOM_OBJECT_CREATE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
SalesforceDeleteCustomFieldParams,
|
||||
SalesforceDeleteCustomFieldResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { CUSTOM_FIELD_DELETE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SalesforceDeleteCustomField')
|
||||
|
||||
/**
|
||||
* Delete a custom field via the Tooling API. Deleting a field removes its data;
|
||||
* the field is moved to the org's recycle bin. Retrieve the field Id with the
|
||||
* Tooling Query tool.
|
||||
* @see https://developer.salesforce.com/docs/atlas.en-us.api_tooling.meta/api_tooling/tooling_api_objects_customfield.htm
|
||||
*/
|
||||
export const salesforceDeleteCustomFieldTool: ToolConfig<
|
||||
SalesforceDeleteCustomFieldParams,
|
||||
SalesforceDeleteCustomFieldResponse
|
||||
> = {
|
||||
id: 'salesforce_delete_custom_field',
|
||||
name: 'Delete Custom Field in Salesforce',
|
||||
description: 'Delete a custom field from a Salesforce object using the Tooling API',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'salesforce',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: { type: 'string', required: true, visibility: 'hidden' },
|
||||
idToken: { type: 'string', required: false, visibility: 'hidden' },
|
||||
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
|
||||
fieldId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Tooling API Id of the custom field to delete (find it via the Tooling Query tool)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
|
||||
const fieldId = requireId(params.fieldId, 'Field ID')
|
||||
return `${instanceUrl}/services/data/v59.0/tooling/sobjects/CustomField/${fieldId}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
const errorMessage = extractErrorMessage(
|
||||
data,
|
||||
response.status,
|
||||
'Failed to delete custom field in Salesforce'
|
||||
)
|
||||
logger.error('Failed to delete custom field', { status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: params?.fieldId?.trim() ?? '',
|
||||
deleted: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Deleted custom field metadata',
|
||||
properties: CUSTOM_FIELD_DELETE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
export { salesforceCreateAccountTool } from './create_account'
|
||||
export { salesforceCreateCaseTool } from './create_case'
|
||||
export { salesforceCreateContactTool } from './create_contact'
|
||||
export { salesforceCreateCustomFieldTool } from './create_custom_field'
|
||||
export { salesforceCreateCustomObjectTool } from './create_custom_object'
|
||||
export { salesforceCreateLeadTool } from './create_lead'
|
||||
export { salesforceCreateOpportunityTool } from './create_opportunity'
|
||||
export { salesforceCreateTaskTool } from './create_task'
|
||||
export { salesforceDeleteAccountTool } from './delete_account'
|
||||
export { salesforceDeleteCaseTool } from './delete_case'
|
||||
export { salesforceDeleteContactTool } from './delete_contact'
|
||||
export { salesforceDeleteCustomFieldTool } from './delete_custom_field'
|
||||
export { salesforceDeleteLeadTool } from './delete_lead'
|
||||
export { salesforceDeleteOpportunityTool } from './delete_opportunity'
|
||||
export { salesforceDeleteTaskTool } from './delete_task'
|
||||
@@ -27,9 +30,11 @@ export { salesforceQueryTool } from './query'
|
||||
export { salesforceQueryMoreTool } from './query_more'
|
||||
export { salesforceRefreshDashboardTool } from './refresh_dashboard'
|
||||
export { salesforceRunReportTool } from './run_report'
|
||||
export { salesforceToolingQueryTool } from './tooling_query'
|
||||
export { salesforceUpdateAccountTool } from './update_account'
|
||||
export { salesforceUpdateCaseTool } from './update_case'
|
||||
export { salesforceUpdateContactTool } from './update_contact'
|
||||
export { salesforceUpdateCustomFieldTool } from './update_custom_field'
|
||||
export { salesforceUpdateLeadTool } from './update_lead'
|
||||
export { salesforceUpdateOpportunityTool } from './update_opportunity'
|
||||
export { salesforceUpdateTaskTool } from './update_task'
|
||||
|
||||
@@ -10,7 +10,9 @@ import type { ToolConfig } from '@/tools/types'
|
||||
const logger = createLogger('SalesforceDashboards')
|
||||
|
||||
/**
|
||||
* List all dashboards accessible by the current user
|
||||
* List the current user's recently used dashboards.
|
||||
* The Dashboard List resource returns recently used dashboards, not the org's
|
||||
* full dashboard catalog.
|
||||
* @see https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_getbasic_dashboardlist.htm
|
||||
*/
|
||||
export const salesforceListDashboardsTool: ToolConfig<
|
||||
@@ -19,7 +21,7 @@ export const salesforceListDashboardsTool: ToolConfig<
|
||||
> = {
|
||||
id: 'salesforce_list_dashboards',
|
||||
name: 'List Dashboards from Salesforce',
|
||||
description: 'Get a list of dashboards accessible by the current user',
|
||||
description: 'Get a list of recently used dashboards for the current user',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
|
||||
@@ -10,7 +10,9 @@ import type { ToolConfig } from '@/tools/types'
|
||||
const logger = createLogger('SalesforceReports')
|
||||
|
||||
/**
|
||||
* List all reports accessible by the current user
|
||||
* List up to 200 of the current user's most recently viewed reports.
|
||||
* The Report List resource returns recently viewed reports, not the org's full
|
||||
* report catalog — use a SOQL query against the Report object for that.
|
||||
* @see https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_get_reportlist.htm
|
||||
*/
|
||||
export const salesforceListReportsTool: ToolConfig<
|
||||
@@ -19,7 +21,7 @@ export const salesforceListReportsTool: ToolConfig<
|
||||
> = {
|
||||
id: 'salesforce_list_reports',
|
||||
name: 'List Reports from Salesforce',
|
||||
description: 'Get a list of reports accessible by the current user',
|
||||
description: 'Get a list of up to 200 recently viewed reports for the current user',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
|
||||
@@ -58,6 +58,8 @@ export const salesforceRunReportTool: ToolConfig<
|
||||
throw new Error('Report ID is required. Please provide a valid Salesforce Report ID.')
|
||||
}
|
||||
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
|
||||
// Default to including detail rows (Salesforce's own API default is false);
|
||||
// report runs in a workflow almost always want the underlying rows.
|
||||
const includeDetails = params.includeDetails !== 'false'
|
||||
return `${instanceUrl}/services/data/v59.0/analytics/reports/${params.reportId}?includeDetails=${includeDetails}`
|
||||
},
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
SalesforceToolingQueryParams,
|
||||
SalesforceToolingQueryResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { TOOLING_QUERY_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SalesforceToolingQuery')
|
||||
|
||||
/**
|
||||
* Execute a SOQL query against the Tooling API. Use this to inspect metadata
|
||||
* objects such as CustomField and CustomObject — for example to find a field's
|
||||
* Id before updating or deleting it:
|
||||
* `SELECT Id, DeveloperName FROM CustomField WHERE TableEnumOrId = 'Account'`.
|
||||
* @see https://developer.salesforce.com/docs/atlas.en-us.api_tooling.meta/api_tooling/intro_rest_resources.htm
|
||||
*/
|
||||
export const salesforceToolingQueryTool: ToolConfig<
|
||||
SalesforceToolingQueryParams,
|
||||
SalesforceToolingQueryResponse
|
||||
> = {
|
||||
id: 'salesforce_tooling_query',
|
||||
name: 'Run Tooling SOQL Query in Salesforce',
|
||||
description: 'Execute a SOQL query against the Tooling API to inspect metadata objects',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'salesforce',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: { type: 'string', required: true, visibility: 'hidden' },
|
||||
idToken: { type: 'string', required: false, visibility: 'hidden' },
|
||||
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
|
||||
query: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
"Tooling SOQL query (e.g., SELECT Id, DeveloperName FROM CustomField WHERE TableEnumOrId = 'Account')",
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
if (!params.query || params.query.trim() === '') {
|
||||
throw new Error(
|
||||
"Tooling SOQL Query is required (e.g., SELECT Id, DeveloperName FROM CustomField WHERE TableEnumOrId = 'Account')."
|
||||
)
|
||||
}
|
||||
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
|
||||
const encodedQuery = encodeURIComponent(params.query)
|
||||
return `${instanceUrl}/services/data/v59.0/tooling/query?q=${encodedQuery}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
const errorMessage = extractErrorMessage(
|
||||
data,
|
||||
response.status,
|
||||
'Failed to execute Tooling SOQL query'
|
||||
)
|
||||
logger.error('Failed to execute Tooling SOQL query', { data, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const records = data.records || []
|
||||
const done = data.done !== false
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
records,
|
||||
totalSize: data.totalSize ?? records.length,
|
||||
done,
|
||||
nextRecordsUrl: data.nextRecordsUrl ?? null,
|
||||
query: params?.query || '',
|
||||
metadata: {
|
||||
totalReturned: records.length,
|
||||
hasMore: !done,
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Tooling query results',
|
||||
properties: TOOLING_QUERY_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1200,6 +1200,83 @@ export const REFRESH_DASHBOARD_OUTPUT_PROPERTIES = {
|
||||
success: { type: 'boolean', description: 'Salesforce operation success' },
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* Output definition for a Tooling API custom field create response.
|
||||
* Creating a CustomField via POST to /tooling/sobjects/CustomField returns the
|
||||
* standard Tooling sObject create envelope ({ id, success, errors }).
|
||||
* @see https://developer.salesforce.com/docs/atlas.en-us.api_tooling.meta/api_tooling/tooling_api_objects_customfield.htm
|
||||
*/
|
||||
export const CUSTOM_FIELD_CREATE_OUTPUT_PROPERTIES = {
|
||||
id: { type: 'string', description: 'Tooling API Id of the newly created custom field' },
|
||||
fullName: {
|
||||
type: 'string',
|
||||
description: 'Full API name of the field, including object (e.g., Account.Region__c)',
|
||||
},
|
||||
success: { type: 'boolean', description: 'Whether the create operation was successful' },
|
||||
created: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the field was created (always true on success)',
|
||||
},
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* Output definition for a Tooling API custom field update response.
|
||||
* A successful PATCH to /tooling/sobjects/CustomField/{id} returns HTTP 204.
|
||||
*/
|
||||
export const CUSTOM_FIELD_UPDATE_OUTPUT_PROPERTIES = {
|
||||
id: { type: 'string', description: 'Tooling API Id of the updated custom field' },
|
||||
updated: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the field was updated (always true on success)',
|
||||
},
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* Output definition for a Tooling API custom field delete response.
|
||||
* A successful DELETE to /tooling/sobjects/CustomField/{id} returns HTTP 204.
|
||||
*/
|
||||
export const CUSTOM_FIELD_DELETE_OUTPUT_PROPERTIES = {
|
||||
id: { type: 'string', description: 'Tooling API Id of the deleted custom field' },
|
||||
deleted: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the field was deleted (always true on success)',
|
||||
},
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* Output definition for a Tooling API custom object create response.
|
||||
* Creating a CustomObject via POST to /tooling/sobjects/CustomObject returns the
|
||||
* standard Tooling sObject create envelope ({ id, success, errors }).
|
||||
* @see https://developer.salesforce.com/docs/atlas.en-us.api_tooling.meta/api_tooling/tooling_api_objects_customobject.htm
|
||||
*/
|
||||
export const CUSTOM_OBJECT_CREATE_OUTPUT_PROPERTIES = {
|
||||
id: { type: 'string', description: 'Tooling API Id of the newly created custom object' },
|
||||
fullName: { type: 'string', description: 'Full API name of the object (e.g., Project__c)' },
|
||||
success: { type: 'boolean', description: 'Whether the create operation was successful' },
|
||||
created: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the object was created (always true on success)',
|
||||
},
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* Output definition for a Tooling API SOQL query response.
|
||||
* The Tooling query endpoint mirrors the data query endpoint shape.
|
||||
* @see https://developer.salesforce.com/docs/atlas.en-us.api_tooling.meta/api_tooling/intro_rest_resources.htm
|
||||
*/
|
||||
export const TOOLING_QUERY_OUTPUT_PROPERTIES = {
|
||||
records: {
|
||||
type: 'array',
|
||||
description: 'Array of Tooling API records matching the query',
|
||||
},
|
||||
totalSize: QUERY_PAGING_OUTPUT_PROPERTIES.totalSize,
|
||||
done: QUERY_PAGING_OUTPUT_PROPERTIES.done,
|
||||
nextRecordsUrl: QUERY_PAGING_OUTPUT_PROPERTIES.nextRecordsUrl,
|
||||
query: { type: 'string', description: 'The executed Tooling SOQL query' },
|
||||
metadata: RESPONSE_METADATA_OUTPUT,
|
||||
success: { type: 'boolean', description: 'Salesforce operation success' },
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* Base parameters shared by all Salesforce operations
|
||||
*/
|
||||
@@ -1873,6 +1950,108 @@ export interface SalesforceListObjectsResponse {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared metadata parameters for creating or updating a custom field via the
|
||||
* Tooling API. Which properties apply depends on `fieldType`:
|
||||
* - Text / LongTextArea / Html / EncryptedText: `length`
|
||||
* - LongTextArea / Html / MultiselectPicklist: `visibleLines`
|
||||
* - Number / Currency / Percent: `precision`, `scale`
|
||||
* - Checkbox: `defaultValue` (true/false, required by Salesforce)
|
||||
* - Picklist / MultiselectPicklist: `picklistValues`
|
||||
*/
|
||||
interface SalesforceCustomFieldMetadataParams {
|
||||
fieldType?: string
|
||||
label?: string
|
||||
length?: number | string
|
||||
precision?: number | string
|
||||
scale?: number | string
|
||||
visibleLines?: number | string
|
||||
required?: boolean | string
|
||||
unique?: boolean | string
|
||||
externalId?: boolean | string
|
||||
defaultValue?: string
|
||||
description?: string
|
||||
inlineHelpText?: string
|
||||
picklistValues?: string
|
||||
}
|
||||
|
||||
export interface SalesforceCreateCustomFieldParams
|
||||
extends BaseSalesforceParams,
|
||||
SalesforceCustomFieldMetadataParams {
|
||||
objectName: string
|
||||
fieldName: string
|
||||
}
|
||||
|
||||
export interface SalesforceCreateCustomFieldResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
fullName: string
|
||||
success: boolean
|
||||
created: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface SalesforceUpdateCustomFieldParams
|
||||
extends BaseSalesforceParams,
|
||||
SalesforceCustomFieldMetadataParams {
|
||||
fieldId: string
|
||||
}
|
||||
|
||||
export interface SalesforceUpdateCustomFieldResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
updated: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface SalesforceDeleteCustomFieldParams extends BaseSalesforceParams {
|
||||
fieldId: string
|
||||
}
|
||||
|
||||
export interface SalesforceDeleteCustomFieldResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
deleted: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface SalesforceCreateCustomObjectParams extends BaseSalesforceParams {
|
||||
objectName: string
|
||||
label: string
|
||||
pluralLabel: string
|
||||
nameFieldLabel?: string
|
||||
description?: string
|
||||
sharingModel?: string
|
||||
}
|
||||
|
||||
export interface SalesforceCreateCustomObjectResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
fullName: string
|
||||
success: boolean
|
||||
created: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface SalesforceToolingQueryParams extends BaseSalesforceParams {
|
||||
query: string
|
||||
}
|
||||
|
||||
export interface SalesforceToolingQueryResponse extends ToolResponse {
|
||||
output: {
|
||||
records: any[]
|
||||
totalSize: number
|
||||
done: boolean
|
||||
nextRecordsUrl?: string | null
|
||||
query: string
|
||||
metadata: {
|
||||
totalReturned: number
|
||||
hasMore: boolean
|
||||
}
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export type SalesforceResponse =
|
||||
| SalesforceGetAccountsResponse
|
||||
| SalesforceCreateAccountResponse
|
||||
@@ -1909,3 +2088,8 @@ export type SalesforceResponse =
|
||||
| SalesforceQueryMoreResponse
|
||||
| SalesforceDescribeObjectResponse
|
||||
| SalesforceListObjectsResponse
|
||||
| SalesforceCreateCustomFieldResponse
|
||||
| SalesforceUpdateCustomFieldResponse
|
||||
| SalesforceDeleteCustomFieldResponse
|
||||
| SalesforceCreateCustomObjectResponse
|
||||
| SalesforceToolingQueryResponse
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
SalesforceUpdateCustomFieldParams,
|
||||
SalesforceUpdateCustomFieldResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { CUSTOM_FIELD_UPDATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import {
|
||||
extractErrorMessage,
|
||||
getInstanceUrl,
|
||||
mergeCustomFieldMetadata,
|
||||
requireId,
|
||||
} from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SalesforceUpdateCustomField')
|
||||
|
||||
/**
|
||||
* Update an existing custom field via the Tooling API.
|
||||
*
|
||||
* Updates a field's attributes (label, length, help text, required, picklist
|
||||
* values, etc.) while keeping its existing data type — changing a field's type
|
||||
* is a separate, conversion-driven operation in Salesforce and is intentionally
|
||||
* out of scope here.
|
||||
*
|
||||
* The Tooling API PATCH replaces the field's entire `Metadata` compound, so a
|
||||
* naive partial PATCH would wipe any property the caller omits. To avoid that,
|
||||
* this tool performs a read-modify-write in `directExecution`: it GETs the
|
||||
* field's current metadata, overlays only the provided changes, then PATCHes the
|
||||
* merged result. Unspecified properties (type, length, etc.) are preserved.
|
||||
* @see https://developer.salesforce.com/docs/atlas.en-us.api_tooling.meta/api_tooling/tooling_api_objects_customfield.htm
|
||||
*/
|
||||
export const salesforceUpdateCustomFieldTool: ToolConfig<
|
||||
SalesforceUpdateCustomFieldParams,
|
||||
SalesforceUpdateCustomFieldResponse
|
||||
> = {
|
||||
id: 'salesforce_update_custom_field',
|
||||
name: 'Update Custom Field in Salesforce',
|
||||
description: 'Update an existing custom field on a Salesforce object using the Tooling API',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'salesforce',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: { type: 'string', required: true, visibility: 'hidden' },
|
||||
idToken: { type: 'string', required: false, visibility: 'hidden' },
|
||||
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
|
||||
fieldId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Tooling API Id of the custom field to update (find it via the Tooling Query tool)',
|
||||
},
|
||||
label: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Display label shown in the UI',
|
||||
},
|
||||
length: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum length for Text, LongTextArea, Html, or MultiselectPicklist fields',
|
||||
},
|
||||
precision: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Total number of digits for Number, Currency, or Percent fields',
|
||||
},
|
||||
scale: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of digits to the right of the decimal for numeric fields',
|
||||
},
|
||||
visibleLines: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of visible lines for LongTextArea, Html, or MultiselectPicklist fields',
|
||||
},
|
||||
required: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether the field is required on record create/edit',
|
||||
},
|
||||
unique: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether the field enforces unique values',
|
||||
},
|
||||
externalId: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether the field is an external ID',
|
||||
},
|
||||
defaultValue: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Default value; for Checkbox fields use true or false',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Internal description of the field',
|
||||
},
|
||||
inlineHelpText: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Help text shown next to the field in the UI',
|
||||
},
|
||||
picklistValues: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Comma-separated values to add to a Picklist or MultiselectPicklist field (existing values are kept)',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Read-modify-write so omitted properties are preserved rather than reset by
|
||||
* the Tooling API's full-metadata PATCH semantics.
|
||||
*/
|
||||
directExecution: async (params): Promise<ToolResponse> => {
|
||||
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
|
||||
const fieldId = requireId(params.fieldId, 'Field ID')
|
||||
const url = `${instanceUrl}/services/data/v59.0/tooling/sobjects/CustomField/${fieldId}`
|
||||
const headers = {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
const readResponse = await fetch(url, { headers })
|
||||
const existing = await readResponse.json().catch(() => ({}))
|
||||
if (!readResponse.ok) {
|
||||
const errorMessage = extractErrorMessage(
|
||||
existing,
|
||||
readResponse.status,
|
||||
'Failed to load custom field for update'
|
||||
)
|
||||
logger.error('Failed to read custom field metadata', { status: readResponse.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const metadata = mergeCustomFieldMetadata(existing?.Metadata, params)
|
||||
|
||||
const patchResponse = await fetch(url, {
|
||||
method: 'PATCH',
|
||||
headers,
|
||||
body: JSON.stringify({ Metadata: metadata }),
|
||||
})
|
||||
if (!patchResponse.ok) {
|
||||
const errorData = await patchResponse.json().catch(() => ({}))
|
||||
const errorMessage = extractErrorMessage(
|
||||
errorData,
|
||||
patchResponse.status,
|
||||
'Failed to update custom field in Salesforce'
|
||||
)
|
||||
logger.error('Failed to update custom field', { status: patchResponse.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: fieldId,
|
||||
updated: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Declarative fallback. `directExecution` is the authoritative path and handles
|
||||
* the read-modify-write; this is only used if direct execution is bypassed.
|
||||
*/
|
||||
request: {
|
||||
url: (params) => {
|
||||
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
|
||||
const fieldId = requireId(params.fieldId, 'Field ID')
|
||||
return `${instanceUrl}/services/data/v59.0/tooling/sobjects/CustomField/${fieldId}`
|
||||
},
|
||||
method: 'PATCH',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
body: (params) => ({ Metadata: mergeCustomFieldMetadata(undefined, params) }),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
const errorMessage = extractErrorMessage(
|
||||
data,
|
||||
response.status,
|
||||
'Failed to update custom field in Salesforce'
|
||||
)
|
||||
logger.error('Failed to update custom field', { status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: params?.fieldId?.trim() ?? '',
|
||||
updated: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Updated custom field metadata',
|
||||
properties: CUSTOM_FIELD_UPDATE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -53,6 +53,228 @@ export function requireId(value: string | undefined, label: string): string {
|
||||
return trimmed
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures a custom field/object API name carries the required `__c` suffix.
|
||||
* Salesforce metadata components created via the Tooling API must end in `__c`;
|
||||
* users commonly omit it, so we append it when missing.
|
||||
* @param value - The raw API name from params (e.g. "Region" or "Region__c")
|
||||
* @param label - Human-readable field name used in the error message
|
||||
* @returns The trimmed API name guaranteed to end with `__c`
|
||||
* @throws Error if the name is absent or whitespace-only
|
||||
*/
|
||||
export function toCustomApiName(value: string | undefined, label: string): string {
|
||||
const trimmed = value?.trim()
|
||||
if (!trimmed) {
|
||||
throw new Error(`${label} is required. Please provide a valid Salesforce API name.`)
|
||||
}
|
||||
return trimmed.endsWith('__c') ? trimmed : `${trimmed}__c`
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a boolean-ish param value into a real boolean.
|
||||
* Tool params arrive as actual booleans from the LLM or as strings from block
|
||||
* inputs; this collapses both forms and treats empty values as "unset".
|
||||
* @param value - The raw param value
|
||||
* @returns The boolean value, or undefined when the param was not provided
|
||||
*/
|
||||
export function normalizeBoolean(value: unknown): boolean | undefined {
|
||||
if (value === undefined || value === null || value === '') return undefined
|
||||
if (typeof value === 'boolean') return value
|
||||
if (typeof value === 'string') return value.trim().toLowerCase() === 'true'
|
||||
return Boolean(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a comma-separated list into trimmed, non-empty entries.
|
||||
* Used for picklist value sets supplied as a single delimited string.
|
||||
* @param value - The raw comma-separated string
|
||||
* @returns An array of trimmed values (empty when nothing parseable is present)
|
||||
*/
|
||||
export function parseDelimitedList(value: string | undefined): string[] {
|
||||
if (!value) return []
|
||||
return value
|
||||
.split(',')
|
||||
.map((entry) => entry.trim())
|
||||
.filter((entry) => entry.length > 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shape of the custom field metadata inputs accepted from tool params.
|
||||
* Numeric dimensions arrive as real numbers from the LLM (param `type: 'number'`)
|
||||
* or as strings from block inputs, so both forms are accepted.
|
||||
*/
|
||||
export interface CustomFieldMetadataInput {
|
||||
fieldType?: string
|
||||
label?: string
|
||||
length?: number | string
|
||||
precision?: number | string
|
||||
scale?: number | string
|
||||
visibleLines?: number | string
|
||||
required?: boolean | string
|
||||
unique?: boolean | string
|
||||
externalId?: boolean | string
|
||||
defaultValue?: string
|
||||
description?: string
|
||||
inlineHelpText?: string
|
||||
picklistValues?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerces a numeric-ish metadata value (number or string) into a number.
|
||||
* @returns The parsed number, or undefined when unset or unparseable
|
||||
*/
|
||||
function toFieldNumber(value?: number | string): number | undefined {
|
||||
if (value === undefined || value === null || String(value).trim() === '') return undefined
|
||||
const parsed = Number(value)
|
||||
return Number.isNaN(parsed) ? undefined : parsed
|
||||
}
|
||||
|
||||
/**
|
||||
* Overlays only the explicitly-provided custom field properties onto `target`,
|
||||
* leaving any property the caller did not supply untouched. Shared by create
|
||||
* (onto a fresh object) and update (onto the field's existing metadata), so an
|
||||
* update never fabricates values for omitted properties.
|
||||
* @param target - The metadata object to mutate in place
|
||||
* @param params - The provided custom field metadata inputs
|
||||
*/
|
||||
function applyProvidedFieldMetadata(
|
||||
target: Record<string, any>,
|
||||
params: CustomFieldMetadataInput
|
||||
): void {
|
||||
if (params.fieldType?.trim()) target.type = params.fieldType.trim()
|
||||
if (params.label?.trim()) target.label = params.label.trim()
|
||||
|
||||
const length = toFieldNumber(params.length)
|
||||
if (length !== undefined) target.length = length
|
||||
const precision = toFieldNumber(params.precision)
|
||||
if (precision !== undefined) target.precision = precision
|
||||
const scale = toFieldNumber(params.scale)
|
||||
if (scale !== undefined) target.scale = scale
|
||||
const visibleLines = toFieldNumber(params.visibleLines)
|
||||
if (visibleLines !== undefined) target.visibleLines = visibleLines
|
||||
|
||||
const required = normalizeBoolean(params.required)
|
||||
if (required !== undefined) target.required = required
|
||||
const unique = normalizeBoolean(params.unique)
|
||||
if (unique !== undefined) target.unique = unique
|
||||
const externalId = normalizeBoolean(params.externalId)
|
||||
if (externalId !== undefined) target.externalId = externalId
|
||||
|
||||
if (params.description?.trim()) target.description = params.description.trim()
|
||||
if (params.inlineHelpText?.trim()) target.inlineHelpText = params.inlineHelpText.trim()
|
||||
|
||||
if (params.defaultValue !== undefined && String(params.defaultValue).trim() !== '') {
|
||||
target.defaultValue =
|
||||
target.type === 'Checkbox'
|
||||
? (normalizeBoolean(params.defaultValue) ?? false)
|
||||
: params.defaultValue
|
||||
}
|
||||
|
||||
const picklistValues = parseDelimitedList(params.picklistValues)
|
||||
if (picklistValues.length > 0) {
|
||||
// Union with any existing values so an update adds new options without
|
||||
// dropping the field's current values (or their default flags).
|
||||
const existingValues: Array<Record<string, any>> = Array.isArray(
|
||||
target.valueSet?.valueSetDefinition?.value
|
||||
)
|
||||
? target.valueSet.valueSetDefinition.value
|
||||
: []
|
||||
const existingFullNames = new Set(existingValues.map((entry) => entry.fullName))
|
||||
const additions = picklistValues
|
||||
.filter((value) => !existingFullNames.has(value))
|
||||
.map((value) => ({ fullName: value, default: false, label: value }))
|
||||
target.valueSet = {
|
||||
...(target.valueSet ?? {}),
|
||||
valueSetDefinition: {
|
||||
...(target.valueSet?.valueSetDefinition ?? {}),
|
||||
sorted: target.valueSet?.valueSetDefinition?.sorted ?? false,
|
||||
value: [...existingValues, ...additions],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies type-specific defaults required by Salesforce when the caller did not
|
||||
* supply them, so common field types work out of the box on create.
|
||||
* @param metadata - The metadata object to mutate in place (must have a `type`)
|
||||
*/
|
||||
function applyFieldTypeDefaults(metadata: Record<string, any>): void {
|
||||
const fieldType = metadata.type
|
||||
if (fieldType === 'Text' && metadata.length === undefined) {
|
||||
metadata.length = 255
|
||||
}
|
||||
if (fieldType === 'LongTextArea' || fieldType === 'Html') {
|
||||
if (metadata.length === undefined) metadata.length = 32768
|
||||
if (metadata.visibleLines === undefined) metadata.visibleLines = 3
|
||||
}
|
||||
if (fieldType === 'MultiselectPicklist') {
|
||||
if (metadata.visibleLines === undefined) metadata.visibleLines = 4
|
||||
// Salesforce requires `length` (total characters across selected values) for
|
||||
// multi-select picklists in addition to visibleLines.
|
||||
if (metadata.length === undefined) metadata.length = 255
|
||||
}
|
||||
if (fieldType === 'Number' || fieldType === 'Currency' || fieldType === 'Percent') {
|
||||
if (metadata.precision === undefined) metadata.precision = 18
|
||||
if (metadata.scale === undefined) metadata.scale = 0
|
||||
}
|
||||
// Checkbox fields require a default value; Salesforce rejects them without one.
|
||||
if (fieldType === 'Checkbox' && metadata.defaultValue === undefined) {
|
||||
metadata.defaultValue = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the `Metadata` object for a Tooling API CustomField create body.
|
||||
* Applies type-specific defaults so common field types work without the caller
|
||||
* supplying every property (e.g. Text defaults to length 255).
|
||||
* @param params - The custom field metadata params
|
||||
* @param fallbackLabel - Label to use when none is provided
|
||||
* @returns The Salesforce CustomField Metadata object
|
||||
* @throws Error if the field type is missing
|
||||
* @see https://developer.salesforce.com/docs/atlas.en-us.api_tooling.meta/api_tooling/tooling_api_objects_customfield.htm
|
||||
*/
|
||||
export function buildCustomFieldMetadata(
|
||||
params: CustomFieldMetadataInput,
|
||||
fallbackLabel: string
|
||||
): Record<string, any> {
|
||||
const fieldType = params.fieldType?.trim()
|
||||
if (!fieldType) {
|
||||
throw new Error('Field Type is required (e.g., Text, Number, Checkbox, Date, Picklist).')
|
||||
}
|
||||
|
||||
const metadata: Record<string, any> = {
|
||||
type: fieldType,
|
||||
label: params.label?.trim() || fallbackLabel,
|
||||
}
|
||||
applyProvidedFieldMetadata(metadata, params)
|
||||
applyFieldTypeDefaults(metadata)
|
||||
return metadata
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges caller-provided custom field changes onto a field's existing metadata
|
||||
* for a Tooling API update. The Tooling API PATCH replaces the whole `Metadata`
|
||||
* compound, so we start from the field's current metadata (read first) and
|
||||
* overlay only what changed — never fabricating defaults or labels that would
|
||||
* silently clobber unspecified properties.
|
||||
* @param existing - The field's current `Metadata` object (from a GET)
|
||||
* @param params - The provided custom field changes
|
||||
* @returns The merged Salesforce CustomField Metadata object
|
||||
* @see https://developer.salesforce.com/docs/atlas.en-us.api_tooling.meta/api_tooling/tooling_api_objects_customfield.htm
|
||||
*/
|
||||
export function mergeCustomFieldMetadata(
|
||||
existing: Record<string, any> | undefined,
|
||||
params: CustomFieldMetadataInput
|
||||
): Record<string, any> {
|
||||
const metadata: Record<string, any> = { ...(existing ?? {}) }
|
||||
// An attribute update never changes the field's data type — Salesforce treats
|
||||
// a type change as a separate, conversion-driven operation. Keep the field's
|
||||
// existing type and overlay only the other provided properties.
|
||||
applyProvidedFieldMetadata(metadata, { ...params, fieldType: undefined })
|
||||
return metadata
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a descriptive error message from Salesforce API responses
|
||||
* @param data - The response data from Salesforce API
|
||||
@@ -64,6 +286,11 @@ export function extractErrorMessage(data: any, status: number, defaultMessage: s
|
||||
if (Array.isArray(data) && data[0]?.message) {
|
||||
return `Salesforce API Error (${status}): ${data[0].message}${data[0].errorCode ? ` [${data[0].errorCode}]` : ''}`
|
||||
}
|
||||
// Tooling API metadata writes return { success: false, errors: [{ message, statusCode }] }
|
||||
if (Array.isArray(data?.errors) && data.errors[0]?.message) {
|
||||
const first = data.errors[0]
|
||||
return `Salesforce API Error (${status}): ${first.message}${first.statusCode ? ` [${first.statusCode}]` : ''}`
|
||||
}
|
||||
if (data?.message) {
|
||||
return `Salesforce API Error (${status}): ${data.message}`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user