mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement(salesforce): fixed refresh and added endpoints (#2177)
* added salesforce other tools * modified scope and change salesforce refresh * fixed refresh * remove dup code
This commit is contained in:
@@ -44,6 +44,17 @@ export const SalesforceBlock: BlockConfig<SalesforceResponse> = {
|
||||
{ label: 'Create Task', id: 'create_task' },
|
||||
{ label: 'Update Task', id: 'update_task' },
|
||||
{ label: 'Delete Task', id: 'delete_task' },
|
||||
{ label: 'List Reports', id: 'list_reports' },
|
||||
{ label: 'Get Report', id: 'get_report' },
|
||||
{ label: 'Run Report', id: 'run_report' },
|
||||
{ label: 'List Report Types', id: 'list_report_types' },
|
||||
{ label: 'List Dashboards', id: 'list_dashboards' },
|
||||
{ label: 'Get Dashboard', id: 'get_dashboard' },
|
||||
{ label: 'Refresh Dashboard', id: 'refresh_dashboard' },
|
||||
{ label: 'Run SOQL Query', id: 'query' },
|
||||
{ label: 'Get More Query Results', id: 'query_more' },
|
||||
{ label: 'Describe Object', id: 'describe_object' },
|
||||
{ label: 'List Objects', id: 'list_objects' },
|
||||
],
|
||||
value: () => 'get_accounts',
|
||||
},
|
||||
@@ -52,7 +63,7 @@ export const SalesforceBlock: BlockConfig<SalesforceResponse> = {
|
||||
title: 'Salesforce Account',
|
||||
type: 'oauth-input',
|
||||
serviceId: 'salesforce',
|
||||
requiredScopes: ['api', 'refresh_token', 'openid'],
|
||||
requiredScopes: ['api', 'refresh_token', 'openid', 'offline_access'],
|
||||
placeholder: 'Select Salesforce account',
|
||||
required: true,
|
||||
},
|
||||
@@ -367,6 +378,77 @@ export const SalesforceBlock: BlockConfig<SalesforceResponse> = {
|
||||
placeholder: 'Account or Opportunity ID',
|
||||
condition: { field: 'operation', value: ['create_task'] },
|
||||
},
|
||||
// Report fields
|
||||
{
|
||||
id: 'reportId',
|
||||
title: 'Report ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Report ID',
|
||||
condition: { field: 'operation', value: ['get_report', 'run_report'] },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'folderName',
|
||||
title: 'Folder Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'Filter by folder name',
|
||||
condition: { field: 'operation', value: ['list_reports', 'list_dashboards'] },
|
||||
},
|
||||
{
|
||||
id: 'searchTerm',
|
||||
title: 'Search Term',
|
||||
type: 'short-input',
|
||||
placeholder: 'Search reports by name',
|
||||
condition: { field: 'operation', value: ['list_reports'] },
|
||||
},
|
||||
{
|
||||
id: 'includeDetails',
|
||||
title: 'Include Details',
|
||||
type: 'short-input',
|
||||
placeholder: 'Include detail rows (true/false)',
|
||||
condition: { field: 'operation', value: ['run_report'] },
|
||||
},
|
||||
{
|
||||
id: 'filters',
|
||||
title: 'Report Filters',
|
||||
type: 'long-input',
|
||||
placeholder: 'JSON array of report filters',
|
||||
condition: { field: 'operation', value: ['run_report'] },
|
||||
},
|
||||
// Dashboard fields
|
||||
{
|
||||
id: 'dashboardId',
|
||||
title: 'Dashboard ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Dashboard ID',
|
||||
condition: { field: 'operation', value: ['get_dashboard', 'refresh_dashboard'] },
|
||||
required: true,
|
||||
},
|
||||
// Query fields
|
||||
{
|
||||
id: 'query',
|
||||
title: 'SOQL Query',
|
||||
type: 'long-input',
|
||||
placeholder: 'SELECT Id, Name FROM Account LIMIT 10',
|
||||
condition: { field: 'operation', value: ['query'] },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'nextRecordsUrl',
|
||||
title: 'Next Records URL',
|
||||
type: 'short-input',
|
||||
placeholder: 'URL from previous query response',
|
||||
condition: { field: 'operation', value: ['query_more'] },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'objectName',
|
||||
title: 'Object Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'API name (e.g., Account, Lead, Custom_Object__c)',
|
||||
condition: { field: 'operation', value: ['describe_object'] },
|
||||
required: true,
|
||||
},
|
||||
// Long-input fields at the bottom
|
||||
{
|
||||
id: 'description',
|
||||
@@ -418,6 +500,17 @@ export const SalesforceBlock: BlockConfig<SalesforceResponse> = {
|
||||
'salesforce_create_task',
|
||||
'salesforce_update_task',
|
||||
'salesforce_delete_task',
|
||||
'salesforce_list_reports',
|
||||
'salesforce_get_report',
|
||||
'salesforce_run_report',
|
||||
'salesforce_list_report_types',
|
||||
'salesforce_list_dashboards',
|
||||
'salesforce_get_dashboard',
|
||||
'salesforce_refresh_dashboard',
|
||||
'salesforce_query',
|
||||
'salesforce_query_more',
|
||||
'salesforce_describe_object',
|
||||
'salesforce_list_objects',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
@@ -470,6 +563,28 @@ export const SalesforceBlock: BlockConfig<SalesforceResponse> = {
|
||||
return 'salesforce_update_task'
|
||||
case 'delete_task':
|
||||
return 'salesforce_delete_task'
|
||||
case 'list_reports':
|
||||
return 'salesforce_list_reports'
|
||||
case 'get_report':
|
||||
return 'salesforce_get_report'
|
||||
case 'run_report':
|
||||
return 'salesforce_run_report'
|
||||
case 'list_report_types':
|
||||
return 'salesforce_list_report_types'
|
||||
case 'list_dashboards':
|
||||
return 'salesforce_list_dashboards'
|
||||
case 'get_dashboard':
|
||||
return 'salesforce_get_dashboard'
|
||||
case 'refresh_dashboard':
|
||||
return 'salesforce_refresh_dashboard'
|
||||
case 'query':
|
||||
return 'salesforce_query'
|
||||
case 'query_more':
|
||||
return 'salesforce_query_more'
|
||||
case 'describe_object':
|
||||
return 'salesforce_describe_object'
|
||||
case 'list_objects':
|
||||
return 'salesforce_list_objects'
|
||||
default:
|
||||
throw new Error(`Unknown operation: ${params.operation}`)
|
||||
}
|
||||
|
||||
@@ -98,6 +98,32 @@ export const auth = betterAuth({
|
||||
},
|
||||
},
|
||||
},
|
||||
account: {
|
||||
create: {
|
||||
after: async (account) => {
|
||||
// Salesforce doesn't return expires_in in its token response (unlike other OAuth providers).
|
||||
// We set a default 2-hour expiration so token refresh logic works correctly.
|
||||
if (account.providerId === 'salesforce' && !account.accessTokenExpiresAt) {
|
||||
const twoHoursFromNow = new Date(Date.now() + 2 * 60 * 60 * 1000)
|
||||
try {
|
||||
await db
|
||||
.update(schema.account)
|
||||
.set({ accessTokenExpiresAt: twoHoursFromNow })
|
||||
.where(eq(schema.account.id, account.id))
|
||||
logger.info(
|
||||
'[databaseHooks.account.create.after] Set default expiration for Salesforce token',
|
||||
{ accountId: account.id, expiresAt: twoHoursFromNow }
|
||||
)
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'[databaseHooks.account.create.after] Failed to set Salesforce token expiration',
|
||||
{ accountId: account.id, error }
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
session: {
|
||||
create: {
|
||||
before: async (session) => {
|
||||
@@ -841,9 +867,10 @@ export const auth = betterAuth({
|
||||
authorizationUrl: 'https://login.salesforce.com/services/oauth2/authorize',
|
||||
tokenUrl: 'https://login.salesforce.com/services/oauth2/token',
|
||||
userInfoUrl: 'https://login.salesforce.com/services/oauth2/userinfo',
|
||||
scopes: ['api', 'refresh_token', 'openid'],
|
||||
scopes: ['api', 'refresh_token', 'openid', 'offline_access'],
|
||||
pkce: true,
|
||||
prompt: 'consent',
|
||||
accessType: 'offline',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/salesforce`,
|
||||
getUserInfo: async (tokens) => {
|
||||
try {
|
||||
|
||||
@@ -840,7 +840,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
providerId: 'salesforce',
|
||||
icon: (props) => SalesforceIcon(props),
|
||||
baseProviderIcon: (props) => SalesforceIcon(props),
|
||||
scopes: ['api', 'refresh_token', 'openid'],
|
||||
scopes: ['api', 'refresh_token', 'openid', 'offline_access'],
|
||||
},
|
||||
},
|
||||
defaultService: 'salesforce',
|
||||
@@ -1427,7 +1427,7 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig {
|
||||
clientId,
|
||||
clientSecret,
|
||||
useBasicAuth: false,
|
||||
supportsRefreshTokenRotation: false,
|
||||
supportsRefreshTokenRotation: true,
|
||||
}
|
||||
}
|
||||
case 'shopify': {
|
||||
@@ -1546,9 +1546,15 @@ export async function refreshOAuthToken(
|
||||
|
||||
logger.error('Token refresh failed:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
parsedError: errorData,
|
||||
providerId,
|
||||
tokenEndpoint: config.tokenEndpoint,
|
||||
hasClientId: !!config.clientId,
|
||||
hasClientSecret: !!config.clientSecret,
|
||||
hasRefreshToken: !!refreshToken,
|
||||
refreshTokenPrefix: refreshToken ? `${refreshToken.substring(0, 10)}...` : 'none',
|
||||
})
|
||||
throw new Error(`Failed to refresh token: ${response.status} ${errorText}`)
|
||||
}
|
||||
|
||||
@@ -928,12 +928,23 @@ import {
|
||||
salesforceDeleteLeadTool,
|
||||
salesforceDeleteOpportunityTool,
|
||||
salesforceDeleteTaskTool,
|
||||
salesforceDescribeObjectTool,
|
||||
salesforceGetAccountsTool,
|
||||
salesforceGetCasesTool,
|
||||
salesforceGetContactsTool,
|
||||
salesforceGetDashboardTool,
|
||||
salesforceGetLeadsTool,
|
||||
salesforceGetOpportunitiesTool,
|
||||
salesforceGetReportTool,
|
||||
salesforceGetTasksTool,
|
||||
salesforceListDashboardsTool,
|
||||
salesforceListObjectsTool,
|
||||
salesforceListReportsTool,
|
||||
salesforceListReportTypesTool,
|
||||
salesforceQueryMoreTool,
|
||||
salesforceQueryTool,
|
||||
salesforceRefreshDashboardTool,
|
||||
salesforceRunReportTool,
|
||||
salesforceUpdateAccountTool,
|
||||
salesforceUpdateCaseTool,
|
||||
salesforceUpdateContactTool,
|
||||
@@ -2275,6 +2286,17 @@ export const tools: Record<string, ToolConfig> = {
|
||||
salesforce_create_task: salesforceCreateTaskTool,
|
||||
salesforce_update_task: salesforceUpdateTaskTool,
|
||||
salesforce_delete_task: salesforceDeleteTaskTool,
|
||||
salesforce_list_reports: salesforceListReportsTool,
|
||||
salesforce_get_report: salesforceGetReportTool,
|
||||
salesforce_run_report: salesforceRunReportTool,
|
||||
salesforce_list_report_types: salesforceListReportTypesTool,
|
||||
salesforce_list_dashboards: salesforceListDashboardsTool,
|
||||
salesforce_get_dashboard: salesforceGetDashboardTool,
|
||||
salesforce_refresh_dashboard: salesforceRefreshDashboardTool,
|
||||
salesforce_query: salesforceQueryTool,
|
||||
salesforce_query_more: salesforceQueryMoreTool,
|
||||
salesforce_describe_object: salesforceDescribeObjectTool,
|
||||
salesforce_list_objects: salesforceListObjectsTool,
|
||||
pylon_list_issues: pylonListIssuesTool,
|
||||
pylon_create_issue: pylonCreateIssueTool,
|
||||
pylon_get_issue: pylonGetIssueTool,
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import { extractErrorMessage, getInstanceUrl } from './utils'
|
||||
|
||||
const logger = createLogger('SalesforceDashboards')
|
||||
|
||||
export interface SalesforceListDashboardsParams {
|
||||
accessToken: string
|
||||
idToken?: string
|
||||
instanceUrl?: string
|
||||
folderName?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* List all dashboards accessible by the current user
|
||||
* @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<any, any> = {
|
||||
id: 'salesforce_list_dashboards',
|
||||
name: 'List Dashboards from Salesforce',
|
||||
description: 'Get a list of dashboards accessible by 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' },
|
||||
folderName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Filter by folder name',
|
||||
},
|
||||
},
|
||||
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, params) => {
|
||||
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)
|
||||
}
|
||||
|
||||
let dashboards = data.dashboards || data || []
|
||||
|
||||
// Filter by folder name if provided
|
||||
if (params.folderName) {
|
||||
dashboards = dashboards.filter((dashboard: any) =>
|
||||
dashboard.folderName?.toLowerCase().includes(params.folderName.toLowerCase())
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
dashboards,
|
||||
metadata: {
|
||||
operation: 'list_dashboards',
|
||||
totalReturned: dashboards.length,
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Dashboards data',
|
||||
properties: {
|
||||
dashboards: { type: 'array', description: 'Array of dashboard objects' },
|
||||
metadata: { type: 'object', description: 'Operation metadata' },
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export interface SalesforceGetDashboardParams {
|
||||
accessToken: string
|
||||
idToken?: string
|
||||
instanceUrl?: string
|
||||
dashboardId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<any, any> = {
|
||||
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-only',
|
||||
description: 'Dashboard ID (required)',
|
||||
},
|
||||
},
|
||||
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)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
dashboard: data,
|
||||
dashboardId: params.dashboardId,
|
||||
components: data.componentData || [],
|
||||
metadata: {
|
||||
operation: 'get_dashboard',
|
||||
dashboardName: data.name,
|
||||
folderId: data.folderId,
|
||||
runningUser: data.runningUser,
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Dashboard data',
|
||||
properties: {
|
||||
dashboard: { type: 'object', description: 'Dashboard details' },
|
||||
dashboardId: { type: 'string', description: 'Dashboard ID' },
|
||||
components: { type: 'array', description: 'Dashboard component data' },
|
||||
metadata: { type: 'object', description: 'Operation metadata' },
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<any, any> = {
|
||||
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-only',
|
||||
description: 'Dashboard ID (required)',
|
||||
},
|
||||
},
|
||||
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)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
dashboard: data,
|
||||
dashboardId: params.dashboardId,
|
||||
components: data.componentData || [],
|
||||
status: data.status,
|
||||
metadata: {
|
||||
operation: 'refresh_dashboard',
|
||||
dashboardName: data.name,
|
||||
refreshDate: data.refreshDate,
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Refreshed dashboard data',
|
||||
properties: {
|
||||
dashboard: { type: 'object', description: 'Dashboard details' },
|
||||
dashboardId: { type: 'string', description: 'Dashboard ID' },
|
||||
components: { type: 'array', description: 'Dashboard component data' },
|
||||
status: { type: 'object', description: 'Dashboard status' },
|
||||
metadata: { type: 'object', description: 'Operation metadata' },
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -11,6 +11,11 @@ export {
|
||||
salesforceUpdateContactTool,
|
||||
} from './contacts'
|
||||
export { salesforceCreateAccountTool } from './create_account'
|
||||
export {
|
||||
salesforceGetDashboardTool,
|
||||
salesforceListDashboardsTool,
|
||||
salesforceRefreshDashboardTool,
|
||||
} from './dashboards'
|
||||
export { salesforceDeleteAccountTool } from './delete_account'
|
||||
export { salesforceGetAccountsTool } from './get_accounts'
|
||||
export {
|
||||
@@ -25,6 +30,18 @@ export {
|
||||
salesforceGetOpportunitiesTool,
|
||||
salesforceUpdateOpportunityTool,
|
||||
} from './opportunities'
|
||||
export {
|
||||
salesforceDescribeObjectTool,
|
||||
salesforceListObjectsTool,
|
||||
salesforceQueryMoreTool,
|
||||
salesforceQueryTool,
|
||||
} from './query'
|
||||
export {
|
||||
salesforceGetReportTool,
|
||||
salesforceListReportsTool,
|
||||
salesforceListReportTypesTool,
|
||||
salesforceRunReportTool,
|
||||
} from './reports'
|
||||
export {
|
||||
salesforceCreateTaskTool,
|
||||
salesforceDeleteTaskTool,
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import { extractErrorMessage, getInstanceUrl } from './utils'
|
||||
|
||||
const logger = createLogger('SalesforceQuery')
|
||||
|
||||
export interface SalesforceQueryParams {
|
||||
accessToken: string
|
||||
idToken?: string
|
||||
instanceUrl?: string
|
||||
query: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<any, any> = {
|
||||
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-only',
|
||||
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 || []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
records,
|
||||
totalSize: data.totalSize || records.length,
|
||||
done: data.done !== false,
|
||||
nextRecordsUrl: data.nextRecordsUrl,
|
||||
query: params.query,
|
||||
metadata: {
|
||||
operation: 'query',
|
||||
totalReturned: records.length,
|
||||
hasMore: !data.done,
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Query results',
|
||||
properties: {
|
||||
records: { type: 'array', description: 'Array of record objects' },
|
||||
totalSize: { type: 'number', description: 'Total number of records matching query' },
|
||||
done: { type: 'boolean', description: 'Whether all records have been returned' },
|
||||
nextRecordsUrl: { type: 'string', description: 'URL to fetch next batch of records' },
|
||||
query: { type: 'string', description: 'The executed SOQL query' },
|
||||
metadata: { type: 'object', description: 'Operation metadata' },
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export interface SalesforceQueryMoreParams {
|
||||
accessToken: string
|
||||
idToken?: string
|
||||
instanceUrl?: string
|
||||
nextRecordsUrl: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<any, any> = {
|
||||
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-only',
|
||||
description: 'The nextRecordsUrl from a previous query response',
|
||||
},
|
||||
},
|
||||
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 || []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
records,
|
||||
totalSize: data.totalSize || records.length,
|
||||
done: data.done !== false,
|
||||
nextRecordsUrl: data.nextRecordsUrl,
|
||||
metadata: {
|
||||
operation: 'query_more',
|
||||
totalReturned: records.length,
|
||||
hasMore: !data.done,
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Query results',
|
||||
properties: {
|
||||
records: { type: 'array', description: 'Array of record objects' },
|
||||
totalSize: { type: 'number', description: 'Total number of records matching query' },
|
||||
done: { type: 'boolean', description: 'Whether all records have been returned' },
|
||||
nextRecordsUrl: { type: 'string', description: 'URL to fetch next batch of records' },
|
||||
metadata: { type: 'object', description: 'Operation metadata' },
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export interface SalesforceDescribeObjectParams {
|
||||
accessToken: string
|
||||
idToken?: string
|
||||
instanceUrl?: string
|
||||
objectName: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Describe a Salesforce object to get its metadata/fields
|
||||
* Useful for discovering available fields for queries
|
||||
*/
|
||||
export const salesforceDescribeObjectTool: ToolConfig<any, any> = {
|
||||
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-only',
|
||||
description: 'API name of the object (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)
|
||||
return `${instanceUrl}/services/data/v59.0/sobjects/${params.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: params.objectName,
|
||||
label: data.label,
|
||||
labelPlural: data.labelPlural,
|
||||
fields: data.fields,
|
||||
keyPrefix: data.keyPrefix,
|
||||
queryable: data.queryable,
|
||||
createable: data.createable,
|
||||
updateable: data.updateable,
|
||||
deletable: data.deletable,
|
||||
childRelationships: data.childRelationships,
|
||||
recordTypeInfos: data.recordTypeInfos,
|
||||
metadata: {
|
||||
operation: 'describe_object',
|
||||
fieldCount: data.fields?.length || 0,
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Object metadata',
|
||||
properties: {
|
||||
objectName: { type: 'string', description: 'API name of the object' },
|
||||
label: { type: 'string', description: 'Display label' },
|
||||
labelPlural: { type: 'string', description: 'Plural display label' },
|
||||
fields: { type: 'array', description: 'Array of field definitions' },
|
||||
keyPrefix: { type: 'string', description: 'ID prefix for this object type' },
|
||||
queryable: { type: 'boolean', description: 'Whether object can be queried' },
|
||||
createable: { type: 'boolean', description: 'Whether records can be created' },
|
||||
updateable: { type: 'boolean', description: 'Whether records can be updated' },
|
||||
deletable: { type: 'boolean', description: 'Whether records can be deleted' },
|
||||
childRelationships: { type: 'array', description: 'Child relationship definitions' },
|
||||
recordTypeInfos: { type: 'array', description: 'Record type information' },
|
||||
metadata: { type: 'object', description: 'Operation metadata' },
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* List all available Salesforce objects
|
||||
* Useful for discovering what objects are available
|
||||
*/
|
||||
export const salesforceListObjectsTool: ToolConfig<any, any> = {
|
||||
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,
|
||||
maxBatchSize: data.maxBatchSize,
|
||||
metadata: {
|
||||
operation: 'list_objects',
|
||||
totalReturned: objects.length,
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Objects list',
|
||||
properties: {
|
||||
objects: { type: 'array', description: 'Array of available Salesforce objects' },
|
||||
encoding: { type: 'string', description: 'Encoding used' },
|
||||
maxBatchSize: { type: 'number', description: 'Maximum batch size' },
|
||||
metadata: { type: 'object', description: 'Operation metadata' },
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import { extractErrorMessage, getInstanceUrl } from './utils'
|
||||
|
||||
const logger = createLogger('SalesforceReports')
|
||||
|
||||
export interface SalesforceListReportsParams {
|
||||
accessToken: string
|
||||
idToken?: string
|
||||
instanceUrl?: string
|
||||
folderName?: string
|
||||
searchTerm?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* List all reports accessible by the current user
|
||||
* @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<any, any> = {
|
||||
id: 'salesforce_list_reports',
|
||||
name: 'List Reports from Salesforce',
|
||||
description: 'Get a list of reports accessible by 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' },
|
||||
folderName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Filter by folder name',
|
||||
},
|
||||
searchTerm: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Search term to filter reports by name',
|
||||
},
|
||||
},
|
||||
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)
|
||||
}
|
||||
|
||||
let reports = data || []
|
||||
|
||||
// Filter by folder name if provided
|
||||
if (params.folderName) {
|
||||
reports = reports.filter((report: any) =>
|
||||
report.folderName?.toLowerCase().includes(params.folderName.toLowerCase())
|
||||
)
|
||||
}
|
||||
|
||||
// Filter by search term if provided
|
||||
if (params.searchTerm) {
|
||||
reports = reports.filter(
|
||||
(report: any) =>
|
||||
report.name?.toLowerCase().includes(params.searchTerm.toLowerCase()) ||
|
||||
report.description?.toLowerCase().includes(params.searchTerm?.toLowerCase())
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
reports,
|
||||
metadata: {
|
||||
operation: 'list_reports',
|
||||
totalReturned: reports.length,
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Reports data',
|
||||
properties: {
|
||||
reports: { type: 'array', description: 'Array of report objects' },
|
||||
metadata: { type: 'object', description: 'Operation metadata' },
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export interface SalesforceGetReportParams {
|
||||
accessToken: string
|
||||
idToken?: string
|
||||
instanceUrl?: string
|
||||
reportId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metadata for a specific report
|
||||
* @see https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_get_reportmetadata.htm
|
||||
*/
|
||||
export const salesforceGetReportTool: ToolConfig<any, any> = {
|
||||
id: 'salesforce_get_report',
|
||||
name: 'Get Report Metadata from Salesforce',
|
||||
description: 'Get metadata and describe information 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-only',
|
||||
description: 'Report ID (required)',
|
||||
},
|
||||
},
|
||||
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,
|
||||
metadata: {
|
||||
operation: 'get_report',
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Report metadata',
|
||||
properties: {
|
||||
report: { type: 'object', description: 'Report metadata object' },
|
||||
reportId: { type: 'string', description: 'Report ID' },
|
||||
metadata: { type: 'object', description: 'Operation metadata' },
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export interface SalesforceRunReportParams {
|
||||
accessToken: string
|
||||
idToken?: string
|
||||
instanceUrl?: string
|
||||
reportId: string
|
||||
includeDetails?: string
|
||||
filters?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<any, any> = {
|
||||
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-only',
|
||||
description: 'Report ID (required)',
|
||||
},
|
||||
includeDetails: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Include detail rows (true/false, default: true)',
|
||||
},
|
||||
filters: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'JSON string of report filters 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)
|
||||
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: ${e instanceof Error ? e.message : 'Parse error'}. Please provide a valid JSON array of filter objects.`
|
||||
)
|
||||
}
|
||||
}
|
||||
// Return undefined for GET requests (no body)
|
||||
return undefined as any
|
||||
},
|
||||
},
|
||||
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,
|
||||
reportExtendedMetadata: data.reportExtendedMetadata,
|
||||
factMap: data.factMap,
|
||||
groupingsDown: data.groupingsDown,
|
||||
groupingsAcross: data.groupingsAcross,
|
||||
hasDetailRows: data.hasDetailRows,
|
||||
allData: data.allData,
|
||||
metadata: {
|
||||
operation: 'run_report',
|
||||
reportName: data.reportMetadata?.name,
|
||||
reportFormat: data.reportMetadata?.reportFormat,
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Report results',
|
||||
properties: {
|
||||
reportId: { type: 'string', description: 'Report ID' },
|
||||
reportMetadata: { type: 'object', description: 'Report metadata' },
|
||||
reportExtendedMetadata: { type: 'object', description: 'Extended metadata' },
|
||||
factMap: { type: 'object', description: 'Report data organized by groupings' },
|
||||
groupingsDown: { type: 'object', description: 'Row groupings' },
|
||||
groupingsAcross: { type: 'object', description: 'Column groupings' },
|
||||
hasDetailRows: { type: 'boolean', description: 'Whether report has detail rows' },
|
||||
allData: { type: 'boolean', description: 'Whether all data is returned' },
|
||||
metadata: { type: 'object', description: 'Operation metadata' },
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<any, any> = {
|
||||
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,
|
||||
metadata: {
|
||||
operation: 'list_report_types',
|
||||
totalReturned: Array.isArray(data) ? data.length : 0,
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Report types data',
|
||||
properties: {
|
||||
reportTypes: { type: 'array', description: 'Array of report type objects' },
|
||||
metadata: { type: 'object', description: 'Operation metadata' },
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -137,6 +137,178 @@ export interface SalesforceDeleteContactResponse {
|
||||
}
|
||||
}
|
||||
|
||||
// Report types
|
||||
export interface SalesforceListReportsResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
reports: any[]
|
||||
metadata: {
|
||||
operation: 'list_reports'
|
||||
totalReturned: number
|
||||
}
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface SalesforceGetReportResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
report: any
|
||||
reportId: string
|
||||
metadata: {
|
||||
operation: 'get_report'
|
||||
}
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface SalesforceRunReportResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
reportId: string
|
||||
reportMetadata: any
|
||||
reportExtendedMetadata: any
|
||||
factMap: any
|
||||
groupingsDown: any
|
||||
groupingsAcross: any
|
||||
hasDetailRows: boolean
|
||||
allData: boolean
|
||||
metadata: {
|
||||
operation: 'run_report'
|
||||
reportName: string
|
||||
reportFormat: string
|
||||
}
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface SalesforceListReportTypesResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
reportTypes: any[]
|
||||
metadata: {
|
||||
operation: 'list_report_types'
|
||||
totalReturned: number
|
||||
}
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
// Dashboard types
|
||||
export interface SalesforceListDashboardsResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
dashboards: any[]
|
||||
metadata: {
|
||||
operation: 'list_dashboards'
|
||||
totalReturned: number
|
||||
}
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface SalesforceGetDashboardResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
dashboard: any
|
||||
dashboardId: string
|
||||
components: any[]
|
||||
metadata: {
|
||||
operation: 'get_dashboard'
|
||||
dashboardName: string
|
||||
folderId: string
|
||||
runningUser: any
|
||||
}
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface SalesforceRefreshDashboardResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
dashboard: any
|
||||
dashboardId: string
|
||||
components: any[]
|
||||
status: any
|
||||
metadata: {
|
||||
operation: 'refresh_dashboard'
|
||||
dashboardName: string
|
||||
refreshDate: string
|
||||
}
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
// Query types
|
||||
export interface SalesforceQueryResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
records: any[]
|
||||
totalSize: number
|
||||
done: boolean
|
||||
nextRecordsUrl?: string
|
||||
query: string
|
||||
metadata: {
|
||||
operation: 'query'
|
||||
totalReturned: number
|
||||
hasMore: boolean
|
||||
}
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface SalesforceQueryMoreResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
records: any[]
|
||||
totalSize: number
|
||||
done: boolean
|
||||
nextRecordsUrl?: string
|
||||
metadata: {
|
||||
operation: 'query_more'
|
||||
totalReturned: number
|
||||
hasMore: boolean
|
||||
}
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface SalesforceDescribeObjectResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
objectName: string
|
||||
label: string
|
||||
labelPlural: string
|
||||
fields: any[]
|
||||
keyPrefix: string
|
||||
queryable: boolean
|
||||
createable: boolean
|
||||
updateable: boolean
|
||||
deletable: boolean
|
||||
childRelationships: any[]
|
||||
recordTypeInfos: any[]
|
||||
metadata: {
|
||||
operation: 'describe_object'
|
||||
fieldCount: number
|
||||
}
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface SalesforceListObjectsResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
objects: any[]
|
||||
encoding: string
|
||||
maxBatchSize: number
|
||||
metadata: {
|
||||
operation: 'list_objects'
|
||||
totalReturned: number
|
||||
}
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
// Generic Salesforce response type for the block
|
||||
export type SalesforceResponse =
|
||||
| SalesforceGetAccountsResponse
|
||||
@@ -147,4 +319,15 @@ export type SalesforceResponse =
|
||||
| SalesforceCreateContactResponse
|
||||
| SalesforceUpdateContactResponse
|
||||
| SalesforceDeleteContactResponse
|
||||
| SalesforceListReportsResponse
|
||||
| SalesforceGetReportResponse
|
||||
| SalesforceRunReportResponse
|
||||
| SalesforceListReportTypesResponse
|
||||
| SalesforceListDashboardsResponse
|
||||
| SalesforceGetDashboardResponse
|
||||
| SalesforceRefreshDashboardResponse
|
||||
| SalesforceQueryResponse
|
||||
| SalesforceQueryMoreResponse
|
||||
| SalesforceDescribeObjectResponse
|
||||
| SalesforceListObjectsResponse
|
||||
| { success: boolean; output: any } // Generic for leads, opportunities, cases, tasks
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { createLogger } from '@/lib/logs/console/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')
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}]` : ''}`
|
||||
}
|
||||
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