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:
Adam Gough
2025-12-06 19:03:25 -08:00
committed by GitHub
parent e9d53042f6
commit 92c03b825b
10 changed files with 1446 additions and 4 deletions
+116 -1
View File
@@ -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}`)
}
+28 -1
View File
@@ -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 {
+8 -2
View File
@@ -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}`)
}
+22
View File
@@ -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,
+258
View File
@@ -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' },
},
},
},
}
+17
View File
@@ -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,
+366
View File
@@ -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' },
},
},
},
}
+378
View File
@@ -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' },
},
},
},
}
+183
View File
@@ -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
+70
View File
@@ -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})`
}
}