chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
import type {
|
||||
SalesforceCreateAccountParams,
|
||||
SalesforceCreateAccountResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { SOBJECT_CREATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const salesforceCreateAccountTool: ToolConfig<
|
||||
SalesforceCreateAccountParams,
|
||||
SalesforceCreateAccountResponse
|
||||
> = {
|
||||
id: 'salesforce_create_account',
|
||||
name: 'Create Account in Salesforce',
|
||||
description: 'Create a new account in Salesforce CRM',
|
||||
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',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Account name (required)',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Account type (e.g., Customer, Partner, Prospect)',
|
||||
},
|
||||
industry: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Industry (e.g., Technology, Healthcare, Finance)',
|
||||
},
|
||||
phone: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Phone number',
|
||||
},
|
||||
website: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Website URL',
|
||||
},
|
||||
billingStreet: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Billing street address',
|
||||
},
|
||||
billingCity: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Billing city',
|
||||
},
|
||||
billingState: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Billing state/province',
|
||||
},
|
||||
billingPostalCode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Billing postal code',
|
||||
},
|
||||
billingCountry: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Billing country',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Account description',
|
||||
},
|
||||
annualRevenue: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Annual revenue as a number',
|
||||
},
|
||||
numberOfEmployees: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of employees as an integer',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
|
||||
|
||||
return `${instanceUrl}/services/data/v59.0/sobjects/Account`
|
||||
},
|
||||
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 body: Record<string, any> = {
|
||||
Name: params.name,
|
||||
}
|
||||
|
||||
if (params.type) body.Type = params.type
|
||||
if (params.industry) body.Industry = params.industry
|
||||
if (params.phone) body.Phone = params.phone
|
||||
if (params.website) body.Website = params.website
|
||||
if (params.billingStreet) body.BillingStreet = params.billingStreet
|
||||
if (params.billingCity) body.BillingCity = params.billingCity
|
||||
if (params.billingState) body.BillingState = params.billingState
|
||||
if (params.billingPostalCode) body.BillingPostalCode = params.billingPostalCode
|
||||
if (params.billingCountry) body.BillingCountry = params.billingCountry
|
||||
if (params.description) body.Description = params.description
|
||||
if (params.annualRevenue) body.AnnualRevenue = Number.parseFloat(params.annualRevenue)
|
||||
if (params.numberOfEmployees)
|
||||
body.NumberOfEmployees = Number.parseInt(params.numberOfEmployees)
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
extractErrorMessage(data, response.status, 'Failed to create account in Salesforce')
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id,
|
||||
success: data.success === true,
|
||||
created: data.success === true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Created account data',
|
||||
properties: SOBJECT_CREATE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import type {
|
||||
SalesforceCreateCaseParams,
|
||||
SalesforceCreateCaseResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { SOBJECT_CREATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const salesforceCreateCaseTool: ToolConfig<
|
||||
SalesforceCreateCaseParams,
|
||||
SalesforceCreateCaseResponse
|
||||
> = {
|
||||
id: 'salesforce_create_case',
|
||||
name: 'Create Case in Salesforce',
|
||||
description: 'Create a new case',
|
||||
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',
|
||||
},
|
||||
subject: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Case subject (required)',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Status (e.g., New, Working, Escalated)',
|
||||
},
|
||||
priority: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Priority (e.g., Low, Medium, High)',
|
||||
},
|
||||
origin: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Origin (e.g., Phone, Email, Web)',
|
||||
},
|
||||
contactId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Salesforce Contact ID (18-character string starting with 003)',
|
||||
},
|
||||
accountId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Salesforce Account ID (18-character string starting with 001)',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Case description',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Case`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, any> = { Subject: params.subject }
|
||||
if (params.status) body.Status = params.status
|
||||
if (params.priority) body.Priority = params.priority
|
||||
if (params.origin) body.Origin = params.origin
|
||||
if (params.contactId) body.ContactId = params.contactId.trim()
|
||||
if (params.accountId) body.AccountId = params.accountId.trim()
|
||||
if (params.description) body.Description = params.description
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok)
|
||||
throw new Error(extractErrorMessage(data, response.status, 'Failed to create case'))
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id,
|
||||
success: data.success === true,
|
||||
created: data.success === true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Created case data',
|
||||
properties: SOBJECT_CREATE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
SalesforceCreateContactParams,
|
||||
SalesforceCreateContactResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { SOBJECT_CREATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SalesforceContacts')
|
||||
|
||||
export const salesforceCreateContactTool: ToolConfig<
|
||||
SalesforceCreateContactParams,
|
||||
SalesforceCreateContactResponse
|
||||
> = {
|
||||
id: 'salesforce_create_contact',
|
||||
name: 'Create Contact in Salesforce',
|
||||
description: 'Create a new contact in Salesforce CRM',
|
||||
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' },
|
||||
lastName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Last name (required)',
|
||||
},
|
||||
firstName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'First name',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Email address',
|
||||
},
|
||||
phone: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Phone number',
|
||||
},
|
||||
accountId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Salesforce Account ID (18-character string starting with 001)',
|
||||
},
|
||||
title: { type: 'string', required: false, visibility: 'user-or-llm', description: 'Job title' },
|
||||
department: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Department',
|
||||
},
|
||||
mailingStreet: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Mailing street',
|
||||
},
|
||||
mailingCity: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Mailing city',
|
||||
},
|
||||
mailingState: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Mailing state',
|
||||
},
|
||||
mailingPostalCode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Mailing postal code',
|
||||
},
|
||||
mailingCountry: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Mailing country',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Contact description',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
|
||||
return `${instanceUrl}/services/data/v59.0/sobjects/Contact`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, any> = { LastName: params.lastName }
|
||||
|
||||
if (params.firstName) body.FirstName = params.firstName
|
||||
if (params.email) body.Email = params.email
|
||||
if (params.phone) body.Phone = params.phone
|
||||
if (params.accountId) body.AccountId = params.accountId.trim()
|
||||
if (params.title) body.Title = params.title
|
||||
if (params.department) body.Department = params.department
|
||||
if (params.mailingStreet) body.MailingStreet = params.mailingStreet
|
||||
if (params.mailingCity) body.MailingCity = params.mailingCity
|
||||
if (params.mailingState) body.MailingState = params.mailingState
|
||||
if (params.mailingPostalCode) body.MailingPostalCode = params.mailingPostalCode
|
||||
if (params.mailingCountry) body.MailingCountry = params.mailingCountry
|
||||
if (params.description) body.Description = params.description
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
logger.error('Salesforce API request failed', { data, status: response.status })
|
||||
throw new Error(
|
||||
extractErrorMessage(data, response.status, 'Failed to create contact in Salesforce')
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id,
|
||||
success: data.success === true,
|
||||
created: data.success === true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Created contact data',
|
||||
properties: SOBJECT_CREATE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,121 @@
|
||||
import type {
|
||||
SalesforceCreateLeadParams,
|
||||
SalesforceCreateLeadResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { SOBJECT_CREATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const salesforceCreateLeadTool: ToolConfig<
|
||||
SalesforceCreateLeadParams,
|
||||
SalesforceCreateLeadResponse
|
||||
> = {
|
||||
id: 'salesforce_create_lead',
|
||||
name: 'Create Lead in Salesforce',
|
||||
description: 'Create a new lead',
|
||||
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' },
|
||||
lastName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Last name (required)',
|
||||
},
|
||||
company: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Company name (required)',
|
||||
},
|
||||
firstName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'First name',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Email address',
|
||||
},
|
||||
phone: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Phone number',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Lead status (e.g., Open, Working, Closed)',
|
||||
},
|
||||
leadSource: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Lead source (e.g., Web, Referral, Campaign)',
|
||||
},
|
||||
title: { type: 'string', required: false, visibility: 'user-or-llm', description: 'Job title' },
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Lead description',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Lead`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, any> = { LastName: params.lastName, Company: params.company }
|
||||
if (params.firstName) body.FirstName = params.firstName
|
||||
if (params.email) body.Email = params.email
|
||||
if (params.phone) body.Phone = params.phone
|
||||
if (params.status) body.Status = params.status
|
||||
if (params.leadSource) body.LeadSource = params.leadSource
|
||||
if (params.title) body.Title = params.title
|
||||
if (params.description) body.Description = params.description
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok)
|
||||
throw new Error(extractErrorMessage(data, response.status, 'Failed to create lead'))
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id,
|
||||
success: data.success === true,
|
||||
created: data.success === true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Created lead data',
|
||||
properties: SOBJECT_CREATE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
SalesforceCreateOpportunityParams,
|
||||
SalesforceCreateOpportunityResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { SOBJECT_CREATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SalesforceCreateOpportunity')
|
||||
|
||||
export const salesforceCreateOpportunityTool: ToolConfig<
|
||||
SalesforceCreateOpportunityParams,
|
||||
SalesforceCreateOpportunityResponse
|
||||
> = {
|
||||
id: 'salesforce_create_opportunity',
|
||||
name: 'Create Opportunity in Salesforce',
|
||||
description: 'Create a new opportunity',
|
||||
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' },
|
||||
name: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Opportunity name (required)',
|
||||
},
|
||||
stageName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Stage name (required, e.g., Prospecting, Qualification, Closed Won)',
|
||||
},
|
||||
closeDate: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Close date in YYYY-MM-DD format (required)',
|
||||
},
|
||||
accountId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Salesforce Account ID (18-character string starting with 001)',
|
||||
},
|
||||
amount: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Deal amount as a number',
|
||||
},
|
||||
probability: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Win probability as integer (0-100)',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Opportunity description',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Opportunity`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, any> = {
|
||||
Name: params.name,
|
||||
StageName: params.stageName,
|
||||
CloseDate: params.closeDate,
|
||||
}
|
||||
if (params.accountId) body.AccountId = params.accountId.trim()
|
||||
if (params.amount) body.Amount = Number.parseFloat(params.amount)
|
||||
if (params.probability) body.Probability = Number.parseInt(params.probability)
|
||||
if (params.description) body.Description = params.description
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
logger.error('Failed to create opportunity', { data, status: response.status })
|
||||
throw new Error(extractErrorMessage(data, response.status, 'Failed to create opportunity'))
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id,
|
||||
success: data.success === true,
|
||||
created: data.success === true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Created opportunity data',
|
||||
properties: SOBJECT_CREATE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import type {
|
||||
SalesforceCreateTaskParams,
|
||||
SalesforceCreateTaskResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { SOBJECT_CREATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const salesforceCreateTaskTool: ToolConfig<
|
||||
SalesforceCreateTaskParams,
|
||||
SalesforceCreateTaskResponse
|
||||
> = {
|
||||
id: 'salesforce_create_task',
|
||||
name: 'Create Task in Salesforce',
|
||||
description: 'Create a new task',
|
||||
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',
|
||||
},
|
||||
subject: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Task subject (required)',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Status (e.g., Not Started, In Progress, Completed)',
|
||||
},
|
||||
priority: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Priority (e.g., Low, Normal, High)',
|
||||
},
|
||||
activityDate: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Due date in YYYY-MM-DD format',
|
||||
},
|
||||
whoId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Related Contact ID (003...) or Lead ID (00Q...)',
|
||||
},
|
||||
whatId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Related Account ID (001...) or Opportunity ID (006...)',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Task description',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Task`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, any> = { Subject: params.subject }
|
||||
if (params.status) body.Status = params.status
|
||||
if (params.priority) body.Priority = params.priority
|
||||
if (params.activityDate) body.ActivityDate = params.activityDate
|
||||
if (params.whoId) body.WhoId = params.whoId.trim()
|
||||
if (params.whatId) body.WhatId = params.whatId.trim()
|
||||
if (params.description) body.Description = params.description
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok)
|
||||
throw new Error(extractErrorMessage(data, response.status, 'Failed to create task'))
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id,
|
||||
success: data.success === true,
|
||||
created: data.success === true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Created task data',
|
||||
properties: SOBJECT_CREATE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import type {
|
||||
SalesforceDeleteAccountParams,
|
||||
SalesforceDeleteAccountResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { SOBJECT_DELETE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const salesforceDeleteAccountTool: ToolConfig<
|
||||
SalesforceDeleteAccountParams,
|
||||
SalesforceDeleteAccountResponse
|
||||
> = {
|
||||
id: 'salesforce_delete_account',
|
||||
name: 'Delete Account from Salesforce',
|
||||
description: 'Delete an account from Salesforce CRM',
|
||||
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',
|
||||
},
|
||||
accountId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Salesforce Account ID to delete (18-character string starting with 001)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
|
||||
const accountId = requireId(params.accountId, 'Account ID')
|
||||
|
||||
return `${instanceUrl}/services/data/v59.0/sobjects/Account/${accountId}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
throw new Error(
|
||||
extractErrorMessage(data, response.status, 'Failed to delete account from Salesforce')
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: params?.accountId?.trim() || '',
|
||||
deleted: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Deleted account data',
|
||||
properties: SOBJECT_DELETE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import type {
|
||||
SalesforceDeleteCaseParams,
|
||||
SalesforceDeleteCaseResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { SOBJECT_DELETE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const salesforceDeleteCaseTool: ToolConfig<
|
||||
SalesforceDeleteCaseParams,
|
||||
SalesforceDeleteCaseResponse
|
||||
> = {
|
||||
id: 'salesforce_delete_case',
|
||||
name: 'Delete Case from Salesforce',
|
||||
description: 'Delete a case',
|
||||
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',
|
||||
},
|
||||
caseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Salesforce Case ID to delete (18-character string starting with 500)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const caseId = requireId(params.caseId, 'Case ID')
|
||||
return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Case/${caseId}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response, params?) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
throw new Error(extractErrorMessage(data, response.status, 'Failed to delete case'))
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: params?.caseId?.trim() || '',
|
||||
deleted: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Deleted case data',
|
||||
properties: SOBJECT_DELETE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
SalesforceDeleteContactParams,
|
||||
SalesforceDeleteContactResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { SOBJECT_DELETE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SalesforceContacts')
|
||||
|
||||
export const salesforceDeleteContactTool: ToolConfig<
|
||||
SalesforceDeleteContactParams,
|
||||
SalesforceDeleteContactResponse
|
||||
> = {
|
||||
id: 'salesforce_delete_contact',
|
||||
name: 'Delete Contact from Salesforce',
|
||||
description: 'Delete a contact from Salesforce CRM',
|
||||
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' },
|
||||
contactId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Salesforce Contact ID to delete (18-character string starting with 003)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
|
||||
const contactId = requireId(params.contactId, 'Contact ID')
|
||||
return `${instanceUrl}/services/data/v59.0/sobjects/Contact/${contactId}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
logger.error('Salesforce API request failed', { data, status: response.status })
|
||||
throw new Error(
|
||||
extractErrorMessage(data, response.status, 'Failed to delete contact from Salesforce')
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: params?.contactId?.trim() || '',
|
||||
deleted: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Deleted contact data',
|
||||
properties: SOBJECT_DELETE_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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import type {
|
||||
SalesforceDeleteLeadParams,
|
||||
SalesforceDeleteLeadResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { SOBJECT_DELETE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const salesforceDeleteLeadTool: ToolConfig<
|
||||
SalesforceDeleteLeadParams,
|
||||
SalesforceDeleteLeadResponse
|
||||
> = {
|
||||
id: 'salesforce_delete_lead',
|
||||
name: 'Delete Lead from Salesforce',
|
||||
description: 'Delete a lead',
|
||||
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' },
|
||||
leadId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Salesforce Lead ID to delete (18-character string starting with 00Q)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const leadId = requireId(params.leadId, 'Lead ID')
|
||||
return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Lead/${leadId}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({ Authorization: `Bearer ${params.accessToken}` }),
|
||||
},
|
||||
|
||||
transformResponse: async (response, params?) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
throw new Error(extractErrorMessage(data, response.status, 'Failed to delete lead'))
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: params?.leadId?.trim() || '',
|
||||
deleted: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Deleted lead data',
|
||||
properties: SOBJECT_DELETE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
SalesforceDeleteOpportunityParams,
|
||||
SalesforceDeleteOpportunityResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { SOBJECT_DELETE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SalesforceDeleteOpportunity')
|
||||
|
||||
export const salesforceDeleteOpportunityTool: ToolConfig<
|
||||
SalesforceDeleteOpportunityParams,
|
||||
SalesforceDeleteOpportunityResponse
|
||||
> = {
|
||||
id: 'salesforce_delete_opportunity',
|
||||
name: 'Delete Opportunity from Salesforce',
|
||||
description: 'Delete an opportunity',
|
||||
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' },
|
||||
opportunityId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Salesforce Opportunity ID to delete (18-character string starting with 006)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const opportunityId = requireId(params.opportunityId, 'Opportunity ID')
|
||||
return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Opportunity/${opportunityId}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({ Authorization: `Bearer ${params.accessToken}` }),
|
||||
},
|
||||
|
||||
transformResponse: async (response, params?) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
logger.error('Failed to delete opportunity', { data, status: response.status })
|
||||
throw new Error(extractErrorMessage(data, response.status, 'Failed to delete opportunity'))
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: params?.opportunityId?.trim() || '',
|
||||
deleted: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Deleted opportunity data',
|
||||
properties: SOBJECT_DELETE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import type {
|
||||
SalesforceDeleteTaskParams,
|
||||
SalesforceDeleteTaskResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { SOBJECT_DELETE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const salesforceDeleteTaskTool: ToolConfig<
|
||||
SalesforceDeleteTaskParams,
|
||||
SalesforceDeleteTaskResponse
|
||||
> = {
|
||||
id: 'salesforce_delete_task',
|
||||
name: 'Delete Task from Salesforce',
|
||||
description: 'Delete a task',
|
||||
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',
|
||||
},
|
||||
taskId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Salesforce Task ID to delete (18-character string starting with 00T)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const taskId = requireId(params.taskId, 'Task ID')
|
||||
return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Task/${taskId}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response, params?) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
throw new Error(extractErrorMessage(data, response.status, 'Failed to delete task'))
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: params?.taskId?.trim() || '',
|
||||
deleted: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Deleted task data',
|
||||
properties: SOBJECT_DELETE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
SalesforceDescribeObjectParams,
|
||||
SalesforceDescribeObjectResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { DESCRIBE_OBJECT_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SalesforceQuery')
|
||||
|
||||
/**
|
||||
* Describe a Salesforce object to get its metadata/fields
|
||||
* Useful for discovering available fields for queries
|
||||
*/
|
||||
export const salesforceDescribeObjectTool: ToolConfig<
|
||||
SalesforceDescribeObjectParams,
|
||||
SalesforceDescribeObjectResponse
|
||||
> = {
|
||||
id: 'salesforce_describe_object',
|
||||
name: 'Describe Salesforce Object',
|
||||
description: 'Get metadata and field information for a Salesforce object',
|
||||
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: 'Salesforce object API name (e.g., Account, Contact, Lead, Custom_Object__c)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
if (!params.objectName || params.objectName.trim() === '') {
|
||||
throw new Error(
|
||||
'Object Name is required. Please provide a valid Salesforce object API name (e.g., Account, Contact, Lead).'
|
||||
)
|
||||
}
|
||||
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
|
||||
const objectName = params.objectName.trim()
|
||||
return `${instanceUrl}/services/data/v59.0/sobjects/${objectName}/describe`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response, params?) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
const errorMessage = extractErrorMessage(
|
||||
data,
|
||||
response.status,
|
||||
`Failed to describe object: ${params?.objectName}`
|
||||
)
|
||||
logger.error('Failed to describe object', { data, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
objectName: data.name ?? params?.objectName ?? '',
|
||||
label: data.label,
|
||||
labelPlural: data.labelPlural,
|
||||
fields: data.fields,
|
||||
keyPrefix: data.keyPrefix ?? null,
|
||||
queryable: data.queryable,
|
||||
createable: data.createable,
|
||||
updateable: data.updateable,
|
||||
deletable: data.deletable,
|
||||
childRelationships: data.childRelationships,
|
||||
recordTypeInfos: data.recordTypeInfos,
|
||||
fieldCount: data.fields?.length || 0,
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Object metadata',
|
||||
properties: DESCRIBE_OBJECT_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import type {
|
||||
SalesforceGetAccountsParams,
|
||||
SalesforceGetAccountsResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { QUERY_PAGING_OUTPUT, RESPONSE_METADATA_OUTPUT } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const salesforceGetAccountsTool: ToolConfig<
|
||||
SalesforceGetAccountsParams,
|
||||
SalesforceGetAccountsResponse
|
||||
> = {
|
||||
id: 'salesforce_get_accounts',
|
||||
name: 'Get Accounts from Salesforce',
|
||||
description: 'Retrieve accounts from Salesforce CRM',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'salesforce',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Salesforce API',
|
||||
},
|
||||
idToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'The ID token from Salesforce OAuth (contains instance URL)',
|
||||
},
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'The Salesforce instance URL',
|
||||
},
|
||||
limit: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of results (default: 100, max: 2000)',
|
||||
},
|
||||
fields: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated field API names (e.g., "Id,Name,Industry,Phone")',
|
||||
},
|
||||
orderBy: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Field and direction for sorting (e.g., "Name ASC" or "CreatedDate DESC")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
|
||||
|
||||
const limit = params.limit ? Number.parseInt(params.limit) : 100
|
||||
const fields =
|
||||
params.fields ||
|
||||
'Id,Name,Type,Industry,BillingCity,BillingState,BillingCountry,Phone,Website'
|
||||
const orderBy = params.orderBy || 'Name ASC'
|
||||
|
||||
// Build SOQL query
|
||||
const query = `SELECT ${fields} FROM Account ORDER BY ${orderBy} LIMIT ${limit}`
|
||||
const encodedQuery = encodeURIComponent(query)
|
||||
|
||||
return `${instanceUrl}/services/data/v59.0/query?q=${encodedQuery}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
extractErrorMessage(data, response.status, 'Failed to fetch accounts from Salesforce')
|
||||
)
|
||||
}
|
||||
|
||||
const accounts = data.records || []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
accounts,
|
||||
paging: {
|
||||
nextRecordsUrl: data.nextRecordsUrl ?? null,
|
||||
totalSize: data.totalSize || accounts.length,
|
||||
done: data.done !== false,
|
||||
},
|
||||
metadata: {
|
||||
totalReturned: accounts.length,
|
||||
hasMore: !data.done,
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Accounts data',
|
||||
properties: {
|
||||
accounts: { type: 'array', description: 'Array of account objects' },
|
||||
paging: QUERY_PAGING_OUTPUT,
|
||||
metadata: RESPONSE_METADATA_OUTPUT,
|
||||
success: { type: 'boolean', description: 'Salesforce operation success' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import type { SalesforceGetCasesParams, SalesforceGetCasesResponse } from '@/tools/salesforce/types'
|
||||
import { QUERY_PAGING_OUTPUT, RESPONSE_METADATA_OUTPUT } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const salesforceGetCasesTool: ToolConfig<
|
||||
SalesforceGetCasesParams,
|
||||
SalesforceGetCasesResponse
|
||||
> = {
|
||||
id: 'salesforce_get_cases',
|
||||
name: 'Get Cases from Salesforce',
|
||||
description: 'Get case(s) from Salesforce',
|
||||
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' },
|
||||
caseId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Salesforce Case ID (18-character string starting with 500) to get a single case',
|
||||
},
|
||||
limit: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of results to return (default: 100)',
|
||||
},
|
||||
fields: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of field API names to return',
|
||||
},
|
||||
orderBy: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Field and direction for sorting (e.g., CreatedDate DESC)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
|
||||
if (params.caseId) {
|
||||
const caseId = requireId(params.caseId, 'Case ID')
|
||||
const fields =
|
||||
params.fields || 'Id,CaseNumber,Subject,Status,Priority,Origin,ContactId,AccountId'
|
||||
return `${instanceUrl}/services/data/v59.0/sobjects/Case/${caseId}?fields=${encodeURIComponent(fields)}`
|
||||
}
|
||||
const limit = params.limit ? Number.parseInt(params.limit) : 100
|
||||
const fields =
|
||||
params.fields || 'Id,CaseNumber,Subject,Status,Priority,Origin,ContactId,AccountId'
|
||||
const orderBy = params.orderBy || 'CreatedDate DESC'
|
||||
const query = `SELECT ${fields} FROM Case ORDER BY ${orderBy} LIMIT ${limit}`
|
||||
return `${instanceUrl}/services/data/v59.0/query?q=${encodeURIComponent(query)}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response, params?) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok)
|
||||
throw new Error(extractErrorMessage(data, response.status, 'Failed to fetch cases'))
|
||||
if (params?.caseId) {
|
||||
return {
|
||||
success: true,
|
||||
output: { case: data, success: true },
|
||||
}
|
||||
}
|
||||
const cases = data.records || []
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
cases,
|
||||
paging: {
|
||||
nextRecordsUrl: data.nextRecordsUrl ?? null,
|
||||
totalSize: data.totalSize || cases.length,
|
||||
done: data.done !== false,
|
||||
},
|
||||
metadata: {
|
||||
totalReturned: cases.length,
|
||||
hasMore: !data.done,
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Case data',
|
||||
properties: {
|
||||
case: { type: 'object', description: 'Single case object (when caseId provided)' },
|
||||
cases: { type: 'array', description: 'Array of case objects (when listing)' },
|
||||
paging: QUERY_PAGING_OUTPUT,
|
||||
metadata: RESPONSE_METADATA_OUTPUT,
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
SalesforceGetContactsParams,
|
||||
SalesforceGetContactsResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { QUERY_PAGING_OUTPUT, RESPONSE_METADATA_OUTPUT } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SalesforceContacts')
|
||||
|
||||
export const salesforceGetContactsTool: ToolConfig<
|
||||
SalesforceGetContactsParams,
|
||||
SalesforceGetContactsResponse
|
||||
> = {
|
||||
id: 'salesforce_get_contacts',
|
||||
name: 'Get Contacts from Salesforce',
|
||||
description: 'Get contact(s) from Salesforce - single contact if ID provided, or list if not',
|
||||
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' },
|
||||
contactId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Salesforce Contact ID (18-character string starting with 003) to get a single contact',
|
||||
},
|
||||
limit: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of results (default: 100, max: 2000). Only for list query.',
|
||||
},
|
||||
fields: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated field API names (e.g., "Id,FirstName,LastName,Email,Phone")',
|
||||
},
|
||||
orderBy: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Field and direction for sorting (e.g., "LastName ASC"). Only for list query.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
|
||||
|
||||
// Single contact by ID
|
||||
if (params.contactId) {
|
||||
const contactId = requireId(params.contactId, 'Contact ID')
|
||||
const fields =
|
||||
params.fields || 'Id,FirstName,LastName,Email,Phone,AccountId,Title,Department'
|
||||
return `${instanceUrl}/services/data/v59.0/sobjects/Contact/${contactId}?fields=${encodeURIComponent(fields)}`
|
||||
}
|
||||
|
||||
// List contacts with SOQL query
|
||||
const limit = params.limit ? Number.parseInt(params.limit) : 100
|
||||
const fields = params.fields || 'Id,FirstName,LastName,Email,Phone,AccountId,Title,Department'
|
||||
const orderBy = params.orderBy || 'LastName ASC'
|
||||
const query = `SELECT ${fields} FROM Contact ORDER BY ${orderBy} LIMIT ${limit}`
|
||||
const encodedQuery = encodeURIComponent(query)
|
||||
|
||||
return `${instanceUrl}/services/data/v59.0/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) {
|
||||
logger.error('Salesforce API request failed', { data, status: response.status })
|
||||
throw new Error(
|
||||
extractErrorMessage(data, response.status, 'Failed to fetch contacts from Salesforce')
|
||||
)
|
||||
}
|
||||
|
||||
// Single contact response
|
||||
if (params?.contactId) {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
contact: data,
|
||||
singleContact: true,
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// List contacts response
|
||||
const contacts = data.records || []
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
contacts,
|
||||
paging: {
|
||||
nextRecordsUrl: data.nextRecordsUrl ?? null,
|
||||
totalSize: data.totalSize || contacts.length,
|
||||
done: data.done !== false,
|
||||
},
|
||||
metadata: {
|
||||
totalReturned: contacts.length,
|
||||
hasMore: !data.done,
|
||||
},
|
||||
singleContact: false,
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Contact(s) data',
|
||||
properties: {
|
||||
contacts: { type: 'array', description: 'Array of contacts (list query)' },
|
||||
contact: { type: 'object', description: 'Single contact (by ID)' },
|
||||
paging: QUERY_PAGING_OUTPUT,
|
||||
metadata: RESPONSE_METADATA_OUTPUT,
|
||||
singleContact: { type: 'boolean', description: 'Whether single contact was returned' },
|
||||
success: { type: 'boolean', description: 'Salesforce operation success' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
SalesforceGetDashboardParams,
|
||||
SalesforceGetDashboardResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { DASHBOARD_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SalesforceDashboards')
|
||||
|
||||
/**
|
||||
* Get details for a specific dashboard
|
||||
* @see https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_dashboard_results.htm
|
||||
*/
|
||||
export const salesforceGetDashboardTool: ToolConfig<
|
||||
SalesforceGetDashboardParams,
|
||||
SalesforceGetDashboardResponse
|
||||
> = {
|
||||
id: 'salesforce_get_dashboard',
|
||||
name: 'Get Dashboard from Salesforce',
|
||||
description: 'Get details and results for a specific dashboard',
|
||||
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' },
|
||||
dashboardId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Salesforce Dashboard ID (18-character string starting with 01Z)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
if (!params.dashboardId || params.dashboardId.trim() === '') {
|
||||
throw new Error('Dashboard ID is required. Please provide a valid Salesforce Dashboard ID.')
|
||||
}
|
||||
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
|
||||
return `${instanceUrl}/services/data/v59.0/analytics/dashboards/${params.dashboardId}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response, params?) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
const errorMessage = extractErrorMessage(
|
||||
data,
|
||||
response.status,
|
||||
`Failed to get dashboard ID: ${params?.dashboardId}`
|
||||
)
|
||||
logger.error('Failed to get dashboard', { data, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const meta = data.dashboardMetadata ?? {}
|
||||
const attrs = meta.attributes ?? {}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
dashboard: data,
|
||||
dashboardId: attrs.dashboardId ?? params?.dashboardId ?? '',
|
||||
components: data.componentData || [],
|
||||
dashboardName: attrs.dashboardName ?? null,
|
||||
dashboardMetadata: data.dashboardMetadata ?? null,
|
||||
runningUser: meta.runningUser ?? null,
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Dashboard data',
|
||||
properties: DASHBOARD_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import type { SalesforceGetLeadsParams, SalesforceGetLeadsResponse } from '@/tools/salesforce/types'
|
||||
import { QUERY_PAGING_OUTPUT, RESPONSE_METADATA_OUTPUT } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const salesforceGetLeadsTool: ToolConfig<
|
||||
SalesforceGetLeadsParams,
|
||||
SalesforceGetLeadsResponse
|
||||
> = {
|
||||
id: 'salesforce_get_leads',
|
||||
name: 'Get Leads from Salesforce',
|
||||
description: 'Retrieve lead(s) from Salesforce CRM',
|
||||
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' },
|
||||
leadId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Salesforce Lead ID (18-character string starting with 00Q) to get a single lead',
|
||||
},
|
||||
limit: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of results to return (default: 100)',
|
||||
},
|
||||
fields: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of field API names to return',
|
||||
},
|
||||
orderBy: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Field and direction for sorting (e.g., LastName ASC)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
|
||||
if (params.leadId) {
|
||||
const leadId = requireId(params.leadId, 'Lead ID')
|
||||
const fields =
|
||||
params.fields || 'Id,FirstName,LastName,Company,Email,Phone,Status,LeadSource'
|
||||
return `${instanceUrl}/services/data/v59.0/sobjects/Lead/${leadId}?fields=${encodeURIComponent(fields)}`
|
||||
}
|
||||
const limit = params.limit ? Number.parseInt(params.limit) : 100
|
||||
const fields = params.fields || 'Id,FirstName,LastName,Company,Email,Phone,Status,LeadSource'
|
||||
const orderBy = params.orderBy || 'LastName ASC'
|
||||
const query = `SELECT ${fields} FROM Lead ORDER BY ${orderBy} LIMIT ${limit}`
|
||||
return `${instanceUrl}/services/data/v59.0/query?q=${encodeURIComponent(query)}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response, params?) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok)
|
||||
throw new Error(extractErrorMessage(data, response.status, 'Failed to fetch leads'))
|
||||
if (params?.leadId) {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
lead: data,
|
||||
singleLead: true,
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
const leads = data.records || []
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
leads,
|
||||
paging: {
|
||||
nextRecordsUrl: data.nextRecordsUrl ?? null,
|
||||
totalSize: data.totalSize || leads.length,
|
||||
done: data.done !== false,
|
||||
},
|
||||
metadata: {
|
||||
totalReturned: leads.length,
|
||||
hasMore: !data.done,
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Lead data',
|
||||
properties: {
|
||||
lead: { type: 'object', description: 'Single lead object (when leadId provided)' },
|
||||
leads: { type: 'array', description: 'Array of lead objects (when listing)' },
|
||||
paging: QUERY_PAGING_OUTPUT,
|
||||
metadata: RESPONSE_METADATA_OUTPUT,
|
||||
singleLead: { type: 'boolean', description: 'Whether single lead was returned' },
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
SalesforceGetOpportunitiesParams,
|
||||
SalesforceGetOpportunitiesResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { QUERY_PAGING_OUTPUT, RESPONSE_METADATA_OUTPUT } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SalesforceGetOpportunities')
|
||||
|
||||
export const salesforceGetOpportunitiesTool: ToolConfig<
|
||||
SalesforceGetOpportunitiesParams,
|
||||
SalesforceGetOpportunitiesResponse
|
||||
> = {
|
||||
id: 'salesforce_get_opportunities',
|
||||
name: 'Get Opportunities from Salesforce',
|
||||
description: 'Get opportunity(ies) from Salesforce',
|
||||
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' },
|
||||
opportunityId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Salesforce Opportunity ID (18-character string starting with 006) to get a single opportunity',
|
||||
},
|
||||
limit: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of results to return (default: 100)',
|
||||
},
|
||||
fields: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of field API names to return',
|
||||
},
|
||||
orderBy: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Field and direction for sorting (e.g., CloseDate DESC)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
|
||||
if (params.opportunityId) {
|
||||
const opportunityId = requireId(params.opportunityId, 'Opportunity ID')
|
||||
const fields = params.fields || 'Id,Name,AccountId,Amount,StageName,CloseDate,Probability'
|
||||
return `${instanceUrl}/services/data/v59.0/sobjects/Opportunity/${opportunityId}?fields=${encodeURIComponent(fields)}`
|
||||
}
|
||||
const limit = params.limit ? Number.parseInt(params.limit) : 100
|
||||
const fields = params.fields || 'Id,Name,AccountId,Amount,StageName,CloseDate,Probability'
|
||||
const orderBy = params.orderBy || 'CloseDate DESC'
|
||||
const query = `SELECT ${fields} FROM Opportunity ORDER BY ${orderBy} LIMIT ${limit}`
|
||||
return `${instanceUrl}/services/data/v59.0/query?q=${encodeURIComponent(query)}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response, params?) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
logger.error('Failed to fetch opportunities', { data, status: response.status })
|
||||
throw new Error(extractErrorMessage(data, response.status, 'Failed to fetch opportunities'))
|
||||
}
|
||||
if (params?.opportunityId) {
|
||||
return {
|
||||
success: true,
|
||||
output: { opportunity: data, success: true },
|
||||
}
|
||||
}
|
||||
const opportunities = data.records || []
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
opportunities,
|
||||
paging: {
|
||||
nextRecordsUrl: data.nextRecordsUrl ?? null,
|
||||
totalSize: data.totalSize || opportunities.length,
|
||||
done: data.done !== false,
|
||||
},
|
||||
metadata: {
|
||||
totalReturned: opportunities.length,
|
||||
hasMore: !data.done,
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Opportunity data',
|
||||
properties: {
|
||||
opportunity: {
|
||||
type: 'object',
|
||||
description: 'Single opportunity object (when opportunityId provided)',
|
||||
},
|
||||
opportunities: {
|
||||
type: 'array',
|
||||
description: 'Array of opportunity objects (when listing)',
|
||||
},
|
||||
paging: QUERY_PAGING_OUTPUT,
|
||||
metadata: RESPONSE_METADATA_OUTPUT,
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
SalesforceGetReportParams,
|
||||
SalesforceGetReportResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { GET_REPORT_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SalesforceReports')
|
||||
|
||||
/**
|
||||
* Get the describe (definition and metadata) for a specific report
|
||||
* @see https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/analytics_api_report_describe.htm
|
||||
*/
|
||||
export const salesforceGetReportTool: ToolConfig<
|
||||
SalesforceGetReportParams,
|
||||
SalesforceGetReportResponse
|
||||
> = {
|
||||
id: 'salesforce_get_report',
|
||||
name: 'Get Report Metadata from Salesforce',
|
||||
description: 'Get the describe (definition and metadata) for a specific report',
|
||||
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' },
|
||||
reportId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Salesforce Report ID (18-character string starting with 00O)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
if (!params.reportId || params.reportId.trim() === '') {
|
||||
throw new Error('Report ID is required. Please provide a valid Salesforce Report ID.')
|
||||
}
|
||||
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
|
||||
return `${instanceUrl}/services/data/v59.0/analytics/reports/${params.reportId}/describe`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response, params?) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
const errorMessage = extractErrorMessage(
|
||||
data,
|
||||
response.status,
|
||||
`Failed to get report metadata for report ID: ${params?.reportId}`
|
||||
)
|
||||
logger.error('Failed to get report metadata', { data, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
report: data,
|
||||
reportId: params?.reportId || '',
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Report metadata',
|
||||
properties: GET_REPORT_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import type { SalesforceGetTasksParams, SalesforceGetTasksResponse } from '@/tools/salesforce/types'
|
||||
import { QUERY_PAGING_OUTPUT, RESPONSE_METADATA_OUTPUT } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const salesforceGetTasksTool: ToolConfig<
|
||||
SalesforceGetTasksParams,
|
||||
SalesforceGetTasksResponse
|
||||
> = {
|
||||
id: 'salesforce_get_tasks',
|
||||
name: 'Get Tasks from Salesforce',
|
||||
description: 'Get task(s) from Salesforce',
|
||||
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',
|
||||
},
|
||||
taskId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Salesforce Task ID (18-character string starting with 00T) to get a single task',
|
||||
},
|
||||
limit: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of results to return (default: 100)',
|
||||
},
|
||||
fields: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of field API names to return',
|
||||
},
|
||||
orderBy: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Field and direction for sorting (e.g., ActivityDate DESC)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
|
||||
if (params.taskId) {
|
||||
const taskId = requireId(params.taskId, 'Task ID')
|
||||
const fields =
|
||||
params.fields || 'Id,Subject,Status,Priority,ActivityDate,WhoId,WhatId,OwnerId'
|
||||
return `${instanceUrl}/services/data/v59.0/sobjects/Task/${taskId}?fields=${encodeURIComponent(fields)}`
|
||||
}
|
||||
const limit = params.limit ? Number.parseInt(params.limit) : 100
|
||||
const fields = params.fields || 'Id,Subject,Status,Priority,ActivityDate,WhoId,WhatId,OwnerId'
|
||||
const orderBy = params.orderBy || 'ActivityDate DESC'
|
||||
const query = `SELECT ${fields} FROM Task ORDER BY ${orderBy} LIMIT ${limit}`
|
||||
return `${instanceUrl}/services/data/v59.0/query?q=${encodeURIComponent(query)}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response, params?) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok)
|
||||
throw new Error(extractErrorMessage(data, response.status, 'Failed to fetch tasks'))
|
||||
if (params?.taskId) {
|
||||
return {
|
||||
success: true,
|
||||
output: { task: data, success: true },
|
||||
}
|
||||
}
|
||||
const tasks = data.records || []
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
tasks,
|
||||
paging: {
|
||||
nextRecordsUrl: data.nextRecordsUrl ?? null,
|
||||
totalSize: data.totalSize || tasks.length,
|
||||
done: data.done !== false,
|
||||
},
|
||||
metadata: {
|
||||
totalReturned: tasks.length,
|
||||
hasMore: !data.done,
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Task data',
|
||||
properties: {
|
||||
task: { type: 'object', description: 'Single task object (when taskId provided)' },
|
||||
tasks: { type: 'array', description: 'Array of task objects (when listing)' },
|
||||
paging: QUERY_PAGING_OUTPUT,
|
||||
metadata: RESPONSE_METADATA_OUTPUT,
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
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'
|
||||
export { salesforceDescribeObjectTool } from './describe_object'
|
||||
export { salesforceGetAccountsTool } from './get_accounts'
|
||||
export { salesforceGetCasesTool } from './get_cases'
|
||||
export { salesforceGetContactsTool } from './get_contacts'
|
||||
export { salesforceGetDashboardTool } from './get_dashboard'
|
||||
export { salesforceGetLeadsTool } from './get_leads'
|
||||
export { salesforceGetOpportunitiesTool } from './get_opportunities'
|
||||
export { salesforceGetReportTool } from './get_report'
|
||||
export { salesforceGetTasksTool } from './get_tasks'
|
||||
export { salesforceListDashboardsTool } from './list_dashboards'
|
||||
export { salesforceListObjectsTool } from './list_objects'
|
||||
export { salesforceListReportTypesTool } from './list_report_types'
|
||||
export { salesforceListReportsTool } from './list_reports'
|
||||
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'
|
||||
@@ -0,0 +1,91 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
SalesforceListDashboardsParams,
|
||||
SalesforceListDashboardsResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { LIST_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SalesforceDashboards')
|
||||
|
||||
/**
|
||||
* 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<
|
||||
SalesforceListDashboardsParams,
|
||||
SalesforceListDashboardsResponse
|
||||
> = {
|
||||
id: 'salesforce_list_dashboards',
|
||||
name: 'List Dashboards from Salesforce',
|
||||
description: 'Get a list of recently used dashboards for the current user',
|
||||
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' },
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
|
||||
return `${instanceUrl}/services/data/v59.0/analytics/dashboards`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
const errorMessage = extractErrorMessage(
|
||||
data,
|
||||
response.status,
|
||||
'Failed to list dashboards from Salesforce'
|
||||
)
|
||||
logger.error('Failed to list dashboards', { data, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
// GET /analytics/dashboards returns a bare top-level array of dashboard objects;
|
||||
// fall back to a `dashboards` wrapper defensively in case the shape varies by org.
|
||||
const dashboards = Array.isArray(data)
|
||||
? data
|
||||
: Array.isArray(data?.dashboards)
|
||||
? data.dashboards
|
||||
: []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
dashboards,
|
||||
totalReturned: dashboards.length,
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Dashboards data',
|
||||
properties: {
|
||||
dashboards: { type: 'array', description: 'Array of dashboard objects' },
|
||||
...LIST_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
SalesforceListObjectsParams,
|
||||
SalesforceListObjectsResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { LIST_OBJECTS_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SalesforceQuery')
|
||||
|
||||
/**
|
||||
* List all available Salesforce objects
|
||||
* Useful for discovering what objects are available
|
||||
*/
|
||||
export const salesforceListObjectsTool: ToolConfig<
|
||||
SalesforceListObjectsParams,
|
||||
SalesforceListObjectsResponse
|
||||
> = {
|
||||
id: 'salesforce_list_objects',
|
||||
name: 'List Salesforce Objects',
|
||||
description: 'Get a list of all available Salesforce 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' },
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
|
||||
return `${instanceUrl}/services/data/v59.0/sobjects`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
const errorMessage = extractErrorMessage(
|
||||
data,
|
||||
response.status,
|
||||
'Failed to list Salesforce objects'
|
||||
)
|
||||
logger.error('Failed to list objects', { data, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const objects = data.sobjects || []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
objects,
|
||||
encoding: data.encoding ?? null,
|
||||
maxBatchSize: data.maxBatchSize ?? null,
|
||||
totalReturned: objects.length,
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Objects list',
|
||||
properties: LIST_OBJECTS_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
SalesforceListReportTypesParams,
|
||||
SalesforceListReportTypesResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { LIST_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SalesforceReports')
|
||||
|
||||
/**
|
||||
* Get list of available report types
|
||||
* @see https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_list_reporttypes.htm
|
||||
*/
|
||||
export const salesforceListReportTypesTool: ToolConfig<
|
||||
SalesforceListReportTypesParams,
|
||||
SalesforceListReportTypesResponse
|
||||
> = {
|
||||
id: 'salesforce_list_report_types',
|
||||
name: 'List Report Types from Salesforce',
|
||||
description: 'Get a list of available report types',
|
||||
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' },
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
|
||||
return `${instanceUrl}/services/data/v59.0/analytics/reportTypes`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
const errorMessage = extractErrorMessage(
|
||||
data,
|
||||
response.status,
|
||||
'Failed to list report types from Salesforce'
|
||||
)
|
||||
logger.error('Failed to list report types', { data, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
reportTypes: data,
|
||||
totalReturned: Array.isArray(data) ? data.length : 0,
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Report types data',
|
||||
properties: {
|
||||
reportTypes: { type: 'array', description: 'Array of report type objects' },
|
||||
...LIST_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
SalesforceListReportsParams,
|
||||
SalesforceListReportsResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { LIST_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SalesforceReports')
|
||||
|
||||
/**
|
||||
* 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<
|
||||
SalesforceListReportsParams,
|
||||
SalesforceListReportsResponse
|
||||
> = {
|
||||
id: 'salesforce_list_reports',
|
||||
name: 'List Reports from Salesforce',
|
||||
description: 'Get a list of up to 200 recently viewed reports for the current user',
|
||||
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' },
|
||||
searchTerm: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter reports by name (case-insensitive partial match)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
|
||||
return `${instanceUrl}/services/data/v59.0/analytics/reports`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response, params?) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
const errorMessage = extractErrorMessage(
|
||||
data,
|
||||
response.status,
|
||||
'Failed to list reports from Salesforce'
|
||||
)
|
||||
logger.error('Failed to list reports', { data, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
// GET /analytics/reports returns a bare top-level array of report objects,
|
||||
// each with name, id, url, describeUrl, and instancesUrl.
|
||||
let reports = Array.isArray(data) ? data : []
|
||||
|
||||
// The list resource only returns the report name (no folder/description),
|
||||
// so searchTerm can only match against the report name.
|
||||
if (params?.searchTerm) {
|
||||
reports = reports.filter((report: any) =>
|
||||
report.name?.toLowerCase().includes(params.searchTerm!.toLowerCase())
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
reports,
|
||||
totalReturned: reports.length,
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Reports data',
|
||||
properties: {
|
||||
reports: { type: 'array', description: 'Array of report objects' },
|
||||
...LIST_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { SalesforceQueryParams, SalesforceQueryResponse } from '@/tools/salesforce/types'
|
||||
import { QUERY_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SalesforceQuery')
|
||||
|
||||
/**
|
||||
* Execute a custom SOQL query
|
||||
* @see https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_query.htm
|
||||
*/
|
||||
export const salesforceQueryTool: ToolConfig<SalesforceQueryParams, SalesforceQueryResponse> = {
|
||||
id: 'salesforce_query',
|
||||
name: 'Run SOQL Query in Salesforce',
|
||||
description: 'Execute a custom SOQL query to retrieve data from Salesforce',
|
||||
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: 'SOQL query to execute (e.g., SELECT Id, Name FROM Account LIMIT 10)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
if (!params.query || params.query.trim() === '') {
|
||||
throw new Error(
|
||||
'SOQL Query is required. Please provide a valid SOQL query (e.g., SELECT Id, Name FROM Account LIMIT 10).'
|
||||
)
|
||||
}
|
||||
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
|
||||
const encodedQuery = encodeURIComponent(params.query)
|
||||
return `${instanceUrl}/services/data/v59.0/query?q=${encodedQuery}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response, params?) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
const errorMessage = extractErrorMessage(
|
||||
data,
|
||||
response.status,
|
||||
'Failed to execute SOQL query'
|
||||
)
|
||||
logger.error('Failed to execute 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: 'Query results',
|
||||
properties: QUERY_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
SalesforceQueryMoreParams,
|
||||
SalesforceQueryMoreResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { QUERY_MORE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SalesforceQuery')
|
||||
|
||||
/**
|
||||
* Retrieve additional query results using the nextRecordsUrl
|
||||
* @see https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_query.htm
|
||||
*/
|
||||
export const salesforceQueryMoreTool: ToolConfig<
|
||||
SalesforceQueryMoreParams,
|
||||
SalesforceQueryMoreResponse
|
||||
> = {
|
||||
id: 'salesforce_query_more',
|
||||
name: 'Get More Query Results from Salesforce',
|
||||
description: 'Retrieve additional query results using the nextRecordsUrl from a previous query',
|
||||
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' },
|
||||
nextRecordsUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The nextRecordsUrl value from a previous query response (e.g., /services/data/v59.0/query/01g...)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
if (!params.nextRecordsUrl || params.nextRecordsUrl.trim() === '') {
|
||||
throw new Error(
|
||||
'Next Records URL is required. This should be the nextRecordsUrl value from a previous query response.'
|
||||
)
|
||||
}
|
||||
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
|
||||
// nextRecordsUrl is typically a relative path like /services/data/v59.0/query/01g...
|
||||
const nextUrl = params.nextRecordsUrl.startsWith('/')
|
||||
? params.nextRecordsUrl
|
||||
: `/${params.nextRecordsUrl}`
|
||||
return `${instanceUrl}${nextUrl}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
const errorMessage = extractErrorMessage(
|
||||
data,
|
||||
response.status,
|
||||
'Failed to get more query results'
|
||||
)
|
||||
logger.error('Failed to get more query results', { 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,
|
||||
metadata: {
|
||||
totalReturned: records.length,
|
||||
hasMore: !done,
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Query results',
|
||||
properties: QUERY_MORE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
SalesforceRefreshDashboardParams,
|
||||
SalesforceRefreshDashboardResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { REFRESH_DASHBOARD_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SalesforceDashboards')
|
||||
|
||||
/**
|
||||
* Refresh a dashboard to get latest data
|
||||
* @see https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_refresh_dashboard.htm
|
||||
*/
|
||||
export const salesforceRefreshDashboardTool: ToolConfig<
|
||||
SalesforceRefreshDashboardParams,
|
||||
SalesforceRefreshDashboardResponse
|
||||
> = {
|
||||
id: 'salesforce_refresh_dashboard',
|
||||
name: 'Refresh Dashboard in Salesforce',
|
||||
description: 'Refresh a dashboard to get the latest data',
|
||||
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' },
|
||||
dashboardId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Salesforce Dashboard ID (18-character string starting with 01Z)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
if (!params.dashboardId || params.dashboardId.trim() === '') {
|
||||
throw new Error('Dashboard ID is required. Please provide a valid Salesforce Dashboard ID.')
|
||||
}
|
||||
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
|
||||
return `${instanceUrl}/services/data/v59.0/analytics/dashboards/${params.dashboardId}`
|
||||
},
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: () => ({}),
|
||||
},
|
||||
|
||||
transformResponse: async (response, params?) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
const errorMessage = extractErrorMessage(
|
||||
data,
|
||||
response.status,
|
||||
`Failed to refresh dashboard ID: ${params?.dashboardId}`
|
||||
)
|
||||
logger.error('Failed to refresh dashboard', { data, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const meta = data.dashboardMetadata ?? {}
|
||||
const attrs = meta.attributes ?? {}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
dashboard: data,
|
||||
dashboardId: attrs.dashboardId ?? params?.dashboardId ?? '',
|
||||
components: data.componentData ?? data.componentStatus ?? [],
|
||||
status: data.dashboardStatus ?? data.status ?? null,
|
||||
statusUrl: data.statusUrl ?? attrs.statusUrl ?? null,
|
||||
dashboardName: attrs.dashboardName ?? null,
|
||||
dashboardMetadata: data.dashboardMetadata ?? null,
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Refreshed dashboard data',
|
||||
properties: REFRESH_DASHBOARD_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import type {
|
||||
SalesforceRunReportParams,
|
||||
SalesforceRunReportResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { RUN_REPORT_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SalesforceReports')
|
||||
|
||||
/**
|
||||
* Run a report and return the results
|
||||
* @see https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_get_reportdata.htm
|
||||
*/
|
||||
export const salesforceRunReportTool: ToolConfig<
|
||||
SalesforceRunReportParams,
|
||||
SalesforceRunReportResponse
|
||||
> = {
|
||||
id: 'salesforce_run_report',
|
||||
name: 'Run Report in Salesforce',
|
||||
description: 'Execute a report and retrieve the results',
|
||||
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' },
|
||||
reportId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Salesforce Report ID (18-character string starting with 00O)',
|
||||
},
|
||||
includeDetails: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Include detail rows (true/false, default: true)',
|
||||
},
|
||||
filters: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'JSON array of report filter objects to apply',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
if (!params.reportId || params.reportId.trim() === '') {
|
||||
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}`
|
||||
},
|
||||
// Use GET for simple report runs, POST only when filters are provided
|
||||
method: (params) => (params.filters ? 'POST' : 'GET'),
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
// Only send a body when filters are provided (POST request)
|
||||
if (params.filters) {
|
||||
try {
|
||||
const filters = JSON.parse(params.filters)
|
||||
return { reportMetadata: { reportFilters: filters } }
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Invalid report filters JSON: ${getErrorMessage(e, 'Parse error')}. Please provide a valid JSON array of filter objects.`
|
||||
)
|
||||
}
|
||||
}
|
||||
// Return undefined for GET requests (no body)
|
||||
return undefined
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response, params?) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
const errorMessage = extractErrorMessage(
|
||||
data,
|
||||
response.status,
|
||||
`Failed to run report ID: ${params?.reportId}`
|
||||
)
|
||||
logger.error('Failed to run report', { data, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
reportId: params?.reportId || '',
|
||||
reportMetadata: data.reportMetadata ?? null,
|
||||
reportExtendedMetadata: data.reportExtendedMetadata ?? null,
|
||||
factMap: data.factMap ?? null,
|
||||
groupingsDown: data.groupingsDown ?? null,
|
||||
groupingsAcross: data.groupingsAcross ?? null,
|
||||
hasDetailRows: data.hasDetailRows ?? null,
|
||||
allData: data.allData ?? null,
|
||||
reportName: data.reportMetadata?.name ?? data.attributes?.reportName ?? null,
|
||||
reportFormat: data.reportMetadata?.reportFormat ?? null,
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Report results',
|
||||
properties: RUN_REPORT_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,190 @@
|
||||
import type {
|
||||
SalesforceUpdateAccountParams,
|
||||
SalesforceUpdateAccountResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { SOBJECT_UPDATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const salesforceUpdateAccountTool: ToolConfig<
|
||||
SalesforceUpdateAccountParams,
|
||||
SalesforceUpdateAccountResponse
|
||||
> = {
|
||||
id: 'salesforce_update_account',
|
||||
name: 'Update Account in Salesforce',
|
||||
description: 'Update an existing account in Salesforce CRM',
|
||||
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',
|
||||
},
|
||||
accountId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Salesforce Account ID to update (18-character string starting with 001)',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Account name',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Account type (e.g., Customer, Partner, Prospect)',
|
||||
},
|
||||
industry: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Industry (e.g., Technology, Healthcare, Finance)',
|
||||
},
|
||||
phone: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Phone number',
|
||||
},
|
||||
website: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Website URL',
|
||||
},
|
||||
billingStreet: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Billing street address',
|
||||
},
|
||||
billingCity: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Billing city',
|
||||
},
|
||||
billingState: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Billing state/province',
|
||||
},
|
||||
billingPostalCode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Billing postal code',
|
||||
},
|
||||
billingCountry: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Billing country',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Account description',
|
||||
},
|
||||
annualRevenue: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Annual revenue as a number',
|
||||
},
|
||||
numberOfEmployees: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of employees as an integer',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
|
||||
const accountId = requireId(params.accountId, 'Account ID')
|
||||
|
||||
return `${instanceUrl}/services/data/v59.0/sobjects/Account/${accountId}`
|
||||
},
|
||||
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) => {
|
||||
const body: Record<string, any> = {}
|
||||
|
||||
if (params.name) body.Name = params.name
|
||||
if (params.type) body.Type = params.type
|
||||
if (params.industry) body.Industry = params.industry
|
||||
if (params.phone) body.Phone = params.phone
|
||||
if (params.website) body.Website = params.website
|
||||
if (params.billingStreet) body.BillingStreet = params.billingStreet
|
||||
if (params.billingCity) body.BillingCity = params.billingCity
|
||||
if (params.billingState) body.BillingState = params.billingState
|
||||
if (params.billingPostalCode) body.BillingPostalCode = params.billingPostalCode
|
||||
if (params.billingCountry) body.BillingCountry = params.billingCountry
|
||||
if (params.description) body.Description = params.description
|
||||
if (params.annualRevenue) body.AnnualRevenue = Number.parseFloat(params.annualRevenue)
|
||||
if (params.numberOfEmployees)
|
||||
body.NumberOfEmployees = Number.parseInt(params.numberOfEmployees)
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json()
|
||||
throw new Error(
|
||||
extractErrorMessage(data, response.status, 'Failed to update account in Salesforce')
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: params?.accountId?.trim() || '',
|
||||
updated: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Updated account data',
|
||||
properties: SOBJECT_UPDATE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import type {
|
||||
SalesforceUpdateCaseParams,
|
||||
SalesforceUpdateCaseResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { SOBJECT_UPDATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const salesforceUpdateCaseTool: ToolConfig<
|
||||
SalesforceUpdateCaseParams,
|
||||
SalesforceUpdateCaseResponse
|
||||
> = {
|
||||
id: 'salesforce_update_case',
|
||||
name: 'Update Case in Salesforce',
|
||||
description: 'Update an existing case',
|
||||
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',
|
||||
},
|
||||
caseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Salesforce Case ID to update (18-character string starting with 500)',
|
||||
},
|
||||
subject: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Case subject',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Status (e.g., New, Working, Escalated, Closed)',
|
||||
},
|
||||
priority: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Priority (e.g., Low, Medium, High)',
|
||||
},
|
||||
origin: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Origin (e.g., Phone, Email, Web)',
|
||||
},
|
||||
contactId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Salesforce Contact ID (18-character string starting with 003)',
|
||||
},
|
||||
accountId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Salesforce Account ID (18-character string starting with 001)',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Case description',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const caseId = requireId(params.caseId, 'Case ID')
|
||||
return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Case/${caseId}`
|
||||
},
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, any> = {}
|
||||
if (params.subject) body.Subject = params.subject
|
||||
if (params.status) body.Status = params.status
|
||||
if (params.priority) body.Priority = params.priority
|
||||
if (params.origin) body.Origin = params.origin
|
||||
if (params.contactId) body.ContactId = params.contactId.trim()
|
||||
if (params.accountId) body.AccountId = params.accountId.trim()
|
||||
if (params.description) body.Description = params.description
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response, params?) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json()
|
||||
throw new Error(extractErrorMessage(data, response.status, 'Failed to update case'))
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: params?.caseId?.trim() || '',
|
||||
updated: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Updated case data',
|
||||
properties: SOBJECT_UPDATE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
SalesforceUpdateContactParams,
|
||||
SalesforceUpdateContactResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { SOBJECT_UPDATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SalesforceContacts')
|
||||
|
||||
export const salesforceUpdateContactTool: ToolConfig<
|
||||
SalesforceUpdateContactParams,
|
||||
SalesforceUpdateContactResponse
|
||||
> = {
|
||||
id: 'salesforce_update_contact',
|
||||
name: 'Update Contact in Salesforce',
|
||||
description: 'Update an existing contact in Salesforce CRM',
|
||||
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' },
|
||||
contactId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Salesforce Contact ID to update (18-character string starting with 003)',
|
||||
},
|
||||
lastName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Last name',
|
||||
},
|
||||
firstName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'First name',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Email address',
|
||||
},
|
||||
phone: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Phone number',
|
||||
},
|
||||
accountId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Salesforce Account ID (18-character string starting with 001)',
|
||||
},
|
||||
title: { type: 'string', required: false, visibility: 'user-or-llm', description: 'Job title' },
|
||||
department: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Department',
|
||||
},
|
||||
mailingStreet: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Mailing street',
|
||||
},
|
||||
mailingCity: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Mailing city',
|
||||
},
|
||||
mailingState: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Mailing state',
|
||||
},
|
||||
mailingPostalCode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Mailing postal code',
|
||||
},
|
||||
mailingCountry: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Mailing country',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Contact description',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
|
||||
const contactId = requireId(params.contactId, 'Contact ID')
|
||||
return `${instanceUrl}/services/data/v59.0/sobjects/Contact/${contactId}`
|
||||
},
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, any> = {}
|
||||
|
||||
if (params.lastName) body.LastName = params.lastName
|
||||
if (params.firstName) body.FirstName = params.firstName
|
||||
if (params.email) body.Email = params.email
|
||||
if (params.phone) body.Phone = params.phone
|
||||
if (params.accountId) body.AccountId = params.accountId.trim()
|
||||
if (params.title) body.Title = params.title
|
||||
if (params.department) body.Department = params.department
|
||||
if (params.mailingStreet) body.MailingStreet = params.mailingStreet
|
||||
if (params.mailingCity) body.MailingCity = params.mailingCity
|
||||
if (params.mailingState) body.MailingState = params.mailingState
|
||||
if (params.mailingPostalCode) body.MailingPostalCode = params.mailingPostalCode
|
||||
if (params.mailingCountry) body.MailingCountry = params.mailingCountry
|
||||
if (params.description) body.Description = params.description
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json()
|
||||
logger.error('Salesforce API request failed', { data, status: response.status })
|
||||
throw new Error(
|
||||
extractErrorMessage(data, response.status, 'Failed to update contact in Salesforce')
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: params?.contactId?.trim() || '',
|
||||
updated: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Updated contact data',
|
||||
properties: SOBJECT_UPDATE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import type {
|
||||
SalesforceUpdateLeadParams,
|
||||
SalesforceUpdateLeadResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { SOBJECT_UPDATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const salesforceUpdateLeadTool: ToolConfig<
|
||||
SalesforceUpdateLeadParams,
|
||||
SalesforceUpdateLeadResponse
|
||||
> = {
|
||||
id: 'salesforce_update_lead',
|
||||
name: 'Update Lead in Salesforce',
|
||||
description: 'Update an existing lead',
|
||||
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' },
|
||||
leadId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Salesforce Lead ID to update (18-character string starting with 00Q)',
|
||||
},
|
||||
lastName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Last name',
|
||||
},
|
||||
company: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Company name',
|
||||
},
|
||||
firstName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'First name',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Email address',
|
||||
},
|
||||
phone: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Phone number',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Lead status (e.g., Open, Working, Closed)',
|
||||
},
|
||||
leadSource: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Lead source (e.g., Web, Referral, Campaign)',
|
||||
},
|
||||
title: { type: 'string', required: false, visibility: 'user-or-llm', description: 'Job title' },
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Lead description',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const leadId = requireId(params.leadId, 'Lead ID')
|
||||
return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Lead/${leadId}`
|
||||
},
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, any> = {}
|
||||
if (params.lastName) body.LastName = params.lastName
|
||||
if (params.company) body.Company = params.company
|
||||
if (params.firstName) body.FirstName = params.firstName
|
||||
if (params.email) body.Email = params.email
|
||||
if (params.phone) body.Phone = params.phone
|
||||
if (params.status) body.Status = params.status
|
||||
if (params.leadSource) body.LeadSource = params.leadSource
|
||||
if (params.title) body.Title = params.title
|
||||
if (params.description) body.Description = params.description
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response, params?) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json()
|
||||
throw new Error(extractErrorMessage(data, response.status, 'Failed to update lead'))
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: params?.leadId?.trim() || '',
|
||||
updated: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Updated lead data',
|
||||
properties: SOBJECT_UPDATE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
SalesforceUpdateOpportunityParams,
|
||||
SalesforceUpdateOpportunityResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { SOBJECT_UPDATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SalesforceUpdateOpportunity')
|
||||
|
||||
export const salesforceUpdateOpportunityTool: ToolConfig<
|
||||
SalesforceUpdateOpportunityParams,
|
||||
SalesforceUpdateOpportunityResponse
|
||||
> = {
|
||||
id: 'salesforce_update_opportunity',
|
||||
name: 'Update Opportunity in Salesforce',
|
||||
description: 'Update an existing opportunity',
|
||||
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' },
|
||||
opportunityId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Salesforce Opportunity ID to update (18-character string starting with 006)',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Opportunity name',
|
||||
},
|
||||
stageName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Stage name (e.g., Prospecting, Qualification, Closed Won)',
|
||||
},
|
||||
closeDate: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Close date in YYYY-MM-DD format',
|
||||
},
|
||||
accountId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Salesforce Account ID (18-character string starting with 001)',
|
||||
},
|
||||
amount: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Deal amount as a number',
|
||||
},
|
||||
probability: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Win probability as integer (0-100)',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Opportunity description',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const opportunityId = requireId(params.opportunityId, 'Opportunity ID')
|
||||
return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Opportunity/${opportunityId}`
|
||||
},
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, any> = {}
|
||||
if (params.name) body.Name = params.name
|
||||
if (params.stageName) body.StageName = params.stageName
|
||||
if (params.closeDate) body.CloseDate = params.closeDate
|
||||
if (params.accountId) body.AccountId = params.accountId.trim()
|
||||
if (params.amount) body.Amount = Number.parseFloat(params.amount)
|
||||
if (params.probability) body.Probability = Number.parseInt(params.probability)
|
||||
if (params.description) body.Description = params.description
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response, params?) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json()
|
||||
logger.error('Failed to update opportunity', { data, status: response.status })
|
||||
throw new Error(extractErrorMessage(data, response.status, 'Failed to update opportunity'))
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: params?.opportunityId?.trim() || '',
|
||||
updated: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Updated opportunity data',
|
||||
properties: SOBJECT_UPDATE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import type {
|
||||
SalesforceUpdateTaskParams,
|
||||
SalesforceUpdateTaskResponse,
|
||||
} from '@/tools/salesforce/types'
|
||||
import { SOBJECT_UPDATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
|
||||
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const salesforceUpdateTaskTool: ToolConfig<
|
||||
SalesforceUpdateTaskParams,
|
||||
SalesforceUpdateTaskResponse
|
||||
> = {
|
||||
id: 'salesforce_update_task',
|
||||
name: 'Update Task in Salesforce',
|
||||
description: 'Update an existing task',
|
||||
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',
|
||||
},
|
||||
taskId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Salesforce Task ID to update (18-character string starting with 00T)',
|
||||
},
|
||||
subject: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Task subject',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Status (e.g., Not Started, In Progress, Completed)',
|
||||
},
|
||||
priority: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Priority (e.g., Low, Normal, High)',
|
||||
},
|
||||
activityDate: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Due date in YYYY-MM-DD format',
|
||||
},
|
||||
whoId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Related Contact ID (003...) or Lead ID (00Q...)',
|
||||
},
|
||||
whatId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Related Account ID (001...) or Opportunity ID (006...)',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Task description',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const taskId = requireId(params.taskId, 'Task ID')
|
||||
return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Task/${taskId}`
|
||||
},
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, any> = {}
|
||||
if (params.subject) body.Subject = params.subject
|
||||
if (params.status) body.Status = params.status
|
||||
if (params.priority) body.Priority = params.priority
|
||||
if (params.activityDate) body.ActivityDate = params.activityDate
|
||||
if (params.whoId) body.WhoId = params.whoId.trim()
|
||||
if (params.whatId) body.WhatId = params.whatId.trim()
|
||||
if (params.description) body.Description = params.description
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response, params?) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json()
|
||||
throw new Error(extractErrorMessage(data, response.status, 'Failed to update task'))
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: params?.taskId?.trim() || '',
|
||||
updated: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Updated task data',
|
||||
properties: SOBJECT_UPDATE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
|
||||
const logger = createLogger('SalesforceUtils')
|
||||
|
||||
/**
|
||||
* Extracts Salesforce instance URL from ID token or uses provided instance URL
|
||||
* @param idToken - The Salesforce ID token containing instance URL
|
||||
* @param instanceUrl - Direct instance URL if provided
|
||||
* @returns The Salesforce instance URL
|
||||
* @throws Error if instance URL cannot be determined
|
||||
*/
|
||||
export function getInstanceUrl(idToken?: string, instanceUrl?: string): string {
|
||||
if (instanceUrl) return instanceUrl
|
||||
if (idToken) {
|
||||
try {
|
||||
const base64Url = idToken.split('.')[1]
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
|
||||
const jsonPayload = decodeURIComponent(
|
||||
atob(base64)
|
||||
.split('')
|
||||
.map((c) => `%${(`00${c.charCodeAt(0).toString(16)}`).slice(-2)}`)
|
||||
.join('')
|
||||
)
|
||||
const decoded = JSON.parse(jsonPayload)
|
||||
if (decoded.profile) {
|
||||
const match = decoded.profile.match(/^(https:\/\/[^/]+)/)
|
||||
if (match) return match[1]
|
||||
} else if (decoded.sub) {
|
||||
const match = decoded.sub.match(/^(https:\/\/[^/]+)/)
|
||||
if (match && match[1] !== 'https://login.salesforce.com') return match[1]
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to decode Salesforce idToken', { error })
|
||||
}
|
||||
}
|
||||
throw new Error('Salesforce instance URL is required but not provided')
|
||||
}
|
||||
|
||||
/**
|
||||
* Trims a record ID and throws if it is missing or whitespace-only.
|
||||
* Prevents whitespace-only IDs from collapsing into an empty URL path segment
|
||||
* (e.g. `/sobjects/Account/`) and hitting Salesforce with a malformed request.
|
||||
* @param value - The raw ID value from params
|
||||
* @param label - Human-readable field name used in the error message
|
||||
* @returns The trimmed, non-empty ID
|
||||
* @throws Error if the ID is absent or whitespace-only
|
||||
*/
|
||||
export function requireId(value: string | undefined, label: string): string {
|
||||
const trimmed = value?.trim()
|
||||
if (!trimmed) {
|
||||
throw new Error(`${label} is required. Please provide a valid Salesforce ${label}.`)
|
||||
}
|
||||
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
|
||||
* @param status - HTTP status code
|
||||
* @param defaultMessage - Default message to use if no specific error found
|
||||
* @returns Formatted error message
|
||||
*/
|
||||
export function extractErrorMessage(data: any, status: number, defaultMessage: string): string {
|
||||
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}`
|
||||
}
|
||||
if (data?.error) {
|
||||
return `Salesforce API Error (${status}): ${data.error}${data.error_description ? ` - ${data.error_description}` : ''}`
|
||||
}
|
||||
switch (status) {
|
||||
case 400:
|
||||
return `Salesforce API Error (400): Bad Request - The request was malformed or missing required parameters`
|
||||
case 401:
|
||||
return `Salesforce API Error (401): Unauthorized - Invalid or expired access token. Please re-authenticate.`
|
||||
case 403:
|
||||
return `Salesforce API Error (403): Forbidden - You do not have permission to access this resource.`
|
||||
case 404:
|
||||
return `Salesforce API Error (404): Not Found - The requested resource does not exist or you do not have access to it.`
|
||||
case 500:
|
||||
return `Salesforce API Error (500): Internal Server Error - An error occurred on Salesforce's servers.`
|
||||
default:
|
||||
return `${defaultMessage} (HTTP ${status})`
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user