diff --git a/packages/corsair/core/constants.ts b/packages/corsair/core/constants.ts index 7f168fd0c..eae7e90fc 100644 --- a/packages/corsair/core/constants.ts +++ b/packages/corsair/core/constants.ts @@ -106,6 +106,7 @@ export const BaseProviders = [ 'reddit', 'resend', 'retailed', + 'salesforce', 'sentry', 'sharepoint', 'slack', @@ -231,6 +232,7 @@ export const ProviderDisplayNames = { reddit: 'Reddit', resend: 'Resend', retailed: 'Retailed', + salesforce: 'Salesforce', sentry: 'Sentry', sharepoint: 'SharePoint', slack: 'Slack', @@ -363,6 +365,7 @@ export type AllProviders = | 'reddit' | 'resend' | 'retailed' + | 'salesforce' | 'sentry' | 'sharepoint' | 'slack' diff --git a/packages/salesforce/api.test.ts b/packages/salesforce/api.test.ts new file mode 100644 index 000000000..ddc56446f --- /dev/null +++ b/packages/salesforce/api.test.ts @@ -0,0 +1,843 @@ +import { salesforce } from './index'; + +jest.mock('./client', () => ({ + SALESFORCE_API_VERSION: '60.0', + SALESFORCE_LOGIN_HOST: 'https://login.salesforce.com', + discoverSalesforceInstanceUrl: jest.fn( + async () => 'https://example.my.salesforce.com', + ), + makeSalesforceRequest: jest.fn( + async (endpoint: string, _apiKey: string, options: any) => { + if (endpoint.includes('tree')) { + return { hasErrors: false, results: [] }; + } + if (endpoint.includes('quickActions')) { + return { success: true, recordId: 'rec_123' }; + } + if (endpoint.includes('Account')) { + if (options?.method === 'DELETE') return { success: true }; + return { + id: 'acc_123', + Id: 'acc_123', + Name: 'Acme Corp', + totalSize: 1, + done: true, + records: [{ Id: 'acc_123', Name: 'Acme Corp' }], + }; + } + if (endpoint.includes('Contact')) { + if (options?.method === 'DELETE') return { success: true }; + return { + id: 'con_123', + Id: 'con_123', + LastName: 'Doe', + totalSize: 1, + done: true, + records: [{ Id: 'con_123', LastName: 'Doe' }], + }; + } + if (endpoint.includes('Lead')) { + if (options?.method === 'DELETE') return { success: true }; + return { + id: 'lead_123', + Id: 'lead_123', + LastName: 'Smith', + totalSize: 1, + done: true, + records: [{ Id: 'lead_123' }], + }; + } + if (endpoint.includes('Opportunity')) { + if (options?.method === 'DELETE') return { success: true }; + return { + id: 'opp_123', + Id: 'opp_123', + Name: 'Big Deal', + totalSize: 1, + done: true, + records: [{ Id: 'opp_123' }], + }; + } + if (endpoint.includes('Campaign')) { + if (options?.method === 'DELETE') return { success: true }; + return { + id: 'camp_123', + Id: 'camp_123', + Name: 'Q1 Campaign', + totalSize: 1, + done: true, + records: [{ Id: 'camp_123' }], + }; + } + if (endpoint.includes('Note')) { + if (options?.method === 'DELETE') return { success: true }; + return { + id: 'note_123', + Id: 'note_123', + Title: 'Memo', + records: [{ Id: 'note_123' }], + }; + } + if (endpoint.includes('Task')) { + return { id: 'task_123' }; + } + if (endpoint.includes('jobs')) { + return { + id: 'job_123', + state: 'UploadComplete', + records: [], + data: 'csv_data', + }; + } + if (endpoint.includes('query') || endpoint.includes('search')) { + return { + totalSize: 1, + done: true, + records: [{ Id: 'rec_1' }], + searchRecords: [{ Id: 'rec_1' }], + }; + } + if (endpoint.includes('composite')) { + return { hasErrors: false, results: [], graphs: [], records: [] }; + } + if (endpoint.includes('VersionData')) { + return Buffer.from('sample content'); + } + if ( + endpoint.includes('ContentVersion') || + endpoint.includes('ContentDocument') + ) { + if (options?.method === 'DELETE') return { success: true }; + return { content: 'sample content', fileId: 'doc_123', shares: [] }; + } + if (endpoint.includes('analytics') || endpoint.includes('wave')) { + return { dashboards: [], reports: [], templates: [] }; + } + return { + success: true, + id: 'res_123', + Id: 'res_123', + records: [], + actions: [], + sObjects: [], + }; + }, + ), +})); + +describe('Salesforce Plugin API', () => { + const plugin = salesforce({ key: 'test_token' }); + const endpoints = plugin.endpoints!; + const ctx = { + key: 'test_token', + authType: 'api_key' as const, + options: { + key: 'test_token', + instanceUrl: 'https://example.my.salesforce.com', + }, + $getAccountId: () => 'acc_test', + } as any; + + describe('Accounts', () => { + it('updates account', async () => { + const res = await endpoints.accounts.updateAccount(ctx, { + id: 'acc_123', + Name: 'Acme Updated', + }); + expect(res.success).toBe(true); + }); + + it('creates account', async () => { + const res = await endpoints.accounts.createAccount(ctx, { Name: 'Acme' }); + expect(res).toBeDefined(); + expect(res.id).toBe('acc_123'); + }); + + it('gets account', async () => { + const res = await endpoints.accounts.getAccount(ctx, { id: 'acc_123' }); + expect(res.Id).toBe('acc_123'); + }); + + it('lists accounts', async () => { + const res = await endpoints.accounts.listAccounts(ctx, { limit: 10 }); + expect(res.records).toHaveLength(1); + }); + + it('searches accounts', async () => { + const res = await endpoints.accounts.searchAccounts(ctx, { + name: 'Acme', + }); + expect(res.records).toBeDefined(); + }); + + it('deletes account', async () => { + const res = await endpoints.accounts.deleteAccount(ctx, { + id: 'acc_123', + }); + expect(res.success).toBe(true); + }); + + it('handles deprecated account endpoints', async () => { + const createRes = + await endpoints.accounts.accountCreationWithContentTypeOption(ctx, { + Name: 'Acme', + }); + expect(createRes.id).toBeDefined(); + + const fetchRes = await endpoints.accounts.fetchAccountByIdWithQuery(ctx, { + id: 'acc_123', + }); + expect(fetchRes.Id).toBe('acc_123'); + + const removeRes = + await endpoints.accounts.removeAccountByUniqueIdentifier(ctx, { + id: 'acc_123', + }); + expect(removeRes.success).toBe(true); + + const retrieveRes = + await endpoints.accounts.retrieveAccountDataAndErrorResponses(ctx, { + id: 'acc_123', + }); + expect(retrieveRes.objectDescribe).toBeDefined(); + }); + }); + + describe('Contacts', () => { + it('creates contact', async () => { + const res = await endpoints.contacts.createContact(ctx, { + LastName: 'Doe', + }); + expect(res.id).toBe('con_123'); + }); + + it('gets contact', async () => { + const res = await endpoints.contacts.getContact(ctx, { id: 'con_123' }); + expect(res.Id).toBe('con_123'); + }); + + it('lists contacts', async () => { + const res = await endpoints.contacts.listContacts(ctx, {}); + expect(res.records).toHaveLength(1); + }); + + it('deletes contact', async () => { + const res = await endpoints.contacts.deleteContact(ctx, { + id: 'con_123', + }); + expect(res.success).toBe(true); + }); + + it('associates contact to account', async () => { + const res = await endpoints.contacts.associateContactToAccount(ctx, { + contactId: 'con_123', + accountId: 'acc_123', + }); + expect(res.success).toBe(true); + }); + + it('handles deprecated contact endpoints', async () => { + const createRes = await endpoints.contacts.createNewContactWithJsonHeader( + ctx, + { LastName: 'Doe' }, + ); + expect(createRes.id).toBeDefined(); + + const queryRes = await endpoints.contacts.queryContactsByName(ctx, { + name: 'Doe', + }); + expect(queryRes.records).toBeDefined(); + + const removeRes = await endpoints.contacts.removeASpecificContactById( + ctx, + { id: 'con_123' }, + ); + expect(removeRes.success).toBe(true); + + const retrieveRes = + await endpoints.contacts.retrieveContactInfoWithStandardResponses(ctx, { + id: 'con_123', + }); + expect(retrieveRes.metadata).toBeDefined(); + + const getByIdRes = await endpoints.contacts.getContactById(ctx, { + id: 'con_123', + }); + expect(getByIdRes.Id).toBe('con_123'); + }); + }); + + describe('Leads', () => { + it('creates lead', async () => { + const res = await endpoints.leads.createLead(ctx, { + LastName: 'Smith', + Company: 'Corp', + }); + expect(res.id).toBe('lead_123'); + }); + + it('gets lead', async () => { + const res = await endpoints.leads.getLead(ctx, { id: 'lead_123' }); + expect(res.Id).toBe('lead_123'); + }); + + it('lists leads', async () => { + const res = await endpoints.leads.listLeads(ctx, {}); + expect(res.records).toHaveLength(1); + }); + + it('deletes lead', async () => { + const res = await endpoints.leads.deleteLead(ctx, { id: 'lead_123' }); + expect(res.success).toBe(true); + }); + + it('applies lead assignment rules', async () => { + const res = await endpoints.leads.applyLeadAssignmentRules(ctx, { + leadId: 'lead_123', + }); + expect(res.success).toBe(true); + }); + + it('handles deprecated lead endpoints', async () => { + const createRes = + await endpoints.leads.createLeadWithSpecifiedContentType(ctx, { + LastName: 'Smith', + Company: 'Corp', + }); + expect(createRes.id).toBeDefined(); + + const delRes = await endpoints.leads.deleteALeadObjectByItsId(ctx, { + id: 'lead_123', + }); + expect(delRes.success).toBe(true); + + const retrieveByIdRes = await endpoints.leads.retrieveLeadById(ctx, { + id: 'lead_123', + }); + expect(retrieveByIdRes.Id).toBe('lead_123'); + + const retrieveDataRes = + await endpoints.leads.retrieveLeadDataWithVariousResponses(ctx, { + id: 'lead_123', + }); + expect(retrieveDataRes.records).toBeDefined(); + }); + }); + + describe('Opportunities', () => { + it('creates opportunity', async () => { + const res = await endpoints.opportunities.createOpportunity(ctx, { + Name: 'Deal', + StageName: 'Prospecting', + CloseDate: '2026-12-31', + }); + expect(res.id).toBe('opp_123'); + }); + + it('gets opportunity', async () => { + const res = await endpoints.opportunities.getOpportunity(ctx, { + id: 'opp_123', + }); + expect(res.Id).toBe('opp_123'); + }); + + it('lists opportunities', async () => { + const res = await endpoints.opportunities.listOpportunities(ctx, {}); + expect(res.records).toHaveLength(1); + }); + + it('deletes opportunity', async () => { + const res = await endpoints.opportunities.deleteOpportunity(ctx, { + id: 'opp_123', + }); + expect(res.success).toBe(true); + }); + + it('adds line item to opportunity', async () => { + const res = await endpoints.opportunities.addOpportunityLineItem(ctx, { + OpportunityId: 'opp_123', + PricebookEntryId: 'pbe_123', + Quantity: 2, + }); + expect(res.id).toBeDefined(); + }); + + it('clones opportunity with products', async () => { + const res = await endpoints.opportunities.cloneOpportunityWithProducts( + ctx, + { + opportunityId: 'opp_123', + cloneProducts: true, + }, + ); + expect(res.id).toBeDefined(); + }); + + it('lists pricebook entries & pricebooks', async () => { + const pbe = await endpoints.opportunities.listPricebookEntries(ctx, {}); + expect(pbe.records).toBeDefined(); + const pb = await endpoints.opportunities.listPricebooks(ctx, {}); + expect(pb.records).toBeDefined(); + }); + + it('handles deprecated opportunity endpoints', async () => { + const createRes = await endpoints.opportunities.createOpportunityRecord( + ctx, + { + Name: 'Deal', + StageName: 'Prospecting', + CloseDate: '2026-12-31', + }, + ); + expect(createRes.id).toBeDefined(); + + const remRes = await endpoints.opportunities.removeOpportunityById(ctx, { + id: 'opp_123', + }); + expect(remRes.success).toBe(true); + + const retDataRes = + await endpoints.opportunities.retrieveOpportunitiesData(ctx, {}); + expect(retDataRes.records).toBeDefined(); + + const retByIdRes = + await endpoints.opportunities.retrieveOpportunityByIdWithOptionalFields( + ctx, + { id: 'opp_123' }, + ); + expect(retByIdRes.Id).toBe('opp_123'); + }); + }); + + describe('Campaigns', () => { + it('creates campaign', async () => { + const res = await endpoints.campaigns.createCampaign(ctx, { + Name: 'Spring Promo', + }); + expect(res.id).toBe('camp_123'); + }); + + it('gets campaign', async () => { + const res = await endpoints.campaigns.getCampaign(ctx, { + id: 'camp_123', + }); + expect(res.Id).toBe('camp_123'); + }); + + it('lists campaigns', async () => { + const res = await endpoints.campaigns.listCampaigns(ctx, {}); + expect(res.records).toHaveLength(1); + }); + + it('deletes campaign', async () => { + const res = await endpoints.campaigns.deleteCampaign(ctx, { + id: 'camp_123', + }); + expect(res.success).toBe(true); + }); + + it('adds contact & lead to campaign and removes', async () => { + const addCon = await endpoints.campaigns.addContactToCampaign(ctx, { + campaignId: 'camp_123', + contactId: 'con_123', + }); + expect(addCon.id).toBeDefined(); + + const addLd = await endpoints.campaigns.addLeadToCampaign(ctx, { + campaign_id: 'camp_123', + lead_id: 'lead_123', + }); + expect(addLd.id).toBeDefined(); + + const rem = await endpoints.campaigns.removeFromCampaign(ctx, { + campaign_member_id: 'cm_123', + }); + expect(rem.success).toBe(true); + }); + + it('searches campaigns and deprecated endpoints', async () => { + const searchRes = await endpoints.campaigns.searchCampaigns(ctx, { + name: 'Spring', + }); + expect(searchRes.records).toBeDefined(); + + const createPostRes = + await endpoints.campaigns.createCampaignRecordViaPost(ctx, { + Name: 'Campaign', + }); + expect(createPostRes.id).toBeDefined(); + + const remObjRes = await endpoints.campaigns.removeCampaignObjectById( + ctx, + { id: 'camp_123' }, + ); + expect(remObjRes.success).toBe(true); + + const retErrRes = + await endpoints.campaigns.retrieveCampaignDataWithErrorHandling(ctx, { + id: 'camp_123', + }); + expect(retErrRes.metadata).toBeDefined(); + + const retSpecRes = + await endpoints.campaigns.retrieveSpecificCampaignObjectDetails(ctx, { + id: 'camp_123', + }); + expect(retSpecRes.Id).toBe('camp_123'); + }); + }); + + describe('Notes, Tasks, Jobs, SOQL/SOSL, Composite, Metadata, UI API, Files, Analytics', () => { + it('handles notes', async () => { + const cNote = await endpoints.notes.createNote(ctx, { Title: 'Note 1' }); + expect(cNote.id).toBe('note_123'); + + const gNote = await endpoints.notes.getNote(ctx, { id: 'note_123' }); + expect(gNote.Id).toBe('note_123'); + + const lNotes = await endpoints.notes.listNotes(ctx, {}); + expect(lNotes.records).toBeDefined(); + + const dNote = await endpoints.notes.deleteNote(ctx, { id: 'note_123' }); + expect(dNote.success).toBe(true); + + const cNoteDep = + await endpoints.notes.createNoteRecordWithContentTypeHeader(ctx, { + Title: 'Note', + ParentId: 'acc_123', + }); + expect(cNoteDep.id).toBeDefined(); + + const rNoteDep = await endpoints.notes.removeNoteObjectById(ctx, { + id: 'note_123', + }); + expect(rNoteDep.success).toBe(true); + + const gNoteFields = await endpoints.notes.getNoteByIdWithFields(ctx, { + id: 'note_123', + }); + expect(gNoteFields.Id).toBe('note_123'); + + const retInfo = await endpoints.notes.retrieveNoteObjectInformation(ctx, { + id: 'note_123', + }); + expect(retInfo.metadata).toBeDefined(); + }); + + it('handles tasks', async () => { + const cTask = await endpoints.tasks.createTask(ctx, { + Subject: 'Follow up', + }); + expect(cTask.id).toBe('task_123'); + + const compTask = await endpoints.tasks.completeTask(ctx, { + taskId: 'task_123', + }); + expect(compTask.success).toBe(true); + + const logC = await endpoints.tasks.logCall(ctx, { + Subject: 'Discovery Call', + }); + expect(logC.id).toBe('task_123'); + + const logE = await endpoints.tasks.logEmailActivity(ctx, { + Subject: 'Intro Email', + }); + expect(logE.id).toBeDefined(); + + const updated = await endpoints.tasks.updateTask(ctx, { + id: 'task_123', + Status: 'In Progress', + }); + expect(updated.success).toBe(true); + + const searched = await endpoints.tasks.searchTasks(ctx, { + subject: 'Follow', + }); + expect(searched.records).toBeDefined(); + + const sent = await endpoints.tasks.sendEmail(ctx, { + toAddresses: ['a@example.com'], + subject: 'Hi', + body: 'Hello', + }); + expect(sent.result).toBeDefined(); + }); + + it('handles bulk jobs', async () => { + const closeRes = await endpoints.jobs.closeOrAbortJob(ctx, { + jobId: 'job_123', + state: 'UploadComplete', + }); + expect(closeRes.id).toBe('job_123'); + + const delQ = await endpoints.jobs.deleteJobQuery(ctx, { + jobId: 'job_123', + }); + expect(delQ.success).toBe(true); + + const failedR = await endpoints.jobs.getJobFailedRecordResults(ctx, { + jobId: 'job_123', + }); + expect(failedR.records).toBeDefined(); + + const qInfo = await endpoints.jobs.getQueryJobInfo(ctx, { + jobId: 'job_123', + }); + expect(qInfo.id).toBe('job_123'); + + const qRes = await endpoints.jobs.getQueryJobResults(ctx, { + jobId: 'job_123', + }); + expect(qRes.data).toBeDefined(); + + const succR = await endpoints.jobs.getJobSuccessfulRecordResults(ctx, { + jobId: 'job_123', + }); + expect(succR.records).toBeDefined(); + + const unprocR = await endpoints.jobs.getJobUnprocessedRecordResults(ctx, { + jobId: 'job_123', + }); + expect(unprocR.records).toBeDefined(); + + const uploaded = await endpoints.jobs.uploadJobData(ctx, { + jobId: 'job_123', + csv: 'Name\nAcme', + }); + expect(uploaded.success).toBe(true); + }); + + it('handles SOQL and SOSL queries', async () => { + const runSoql = await endpoints.soqlSosl.runSoqlQuery(ctx, { + q: 'SELECT Id FROM Account', + }); + expect(runSoql.records).toBeDefined(); + + const qAll = await endpoints.soqlSosl.queryAll(ctx, { + q: 'SELECT Id FROM Account', + }); + expect(qAll.records).toBeDefined(); + + const srch = await endpoints.soqlSosl.search(ctx, { q: 'FIND {Acme}' }); + expect(srch.searchRecords).toBeDefined(); + + const sosl = await endpoints.soqlSosl.executeSoslSearch(ctx, { + q: 'FIND {Acme}', + }); + expect(sosl.searchRecords).toBeDefined(); + + const toolQ = await endpoints.soqlSosl.toolingQuery(ctx, { + q: 'SELECT Id FROM ApexClass', + }); + expect(toolQ.records).toBeDefined(); + + const paramSearch = await endpoints.soqlSosl.parameterizedSearch(ctx, { + q: 'Acme', + }); + expect(paramSearch.searchRecords).toBeDefined(); + + const postParamSearch = await endpoints.soqlSosl.postParameterizedSearch( + ctx, + { q: 'Acme' }, + ); + expect(postParamSearch.searchRecords).toBeDefined(); + + const searchLayout = await endpoints.soqlSosl.getSearchLayout(ctx, { + sobjects: 'Account', + }); + expect(searchLayout).toBeDefined(); + + const qDep = await endpoints.soqlSosl.query(ctx, { + q: 'SELECT Id FROM Account', + }); + expect(qDep.records).toBeDefined(); + + const execSoqlDep = await endpoints.soqlSosl.executeSoqlQuery(ctx, { + q: 'SELECT Id FROM Account', + }); + expect(execSoqlDep.records).toBeDefined(); + }); + + it('handles composite operations', async () => { + const postComp = await endpoints.composite.postCompositeSobjects(ctx, { + records: [{ attributes: { type: 'Account' }, Name: 'Acme' }], + }); + expect(postComp).toBeDefined(); + + const tree = await endpoints.composite.createSobjectTree(ctx, { + sobject: 'Account', + records: [], + }); + expect(tree.hasErrors).toBe(false); + + const delColl = await endpoints.composite.deleteSobjectCollections(ctx, { + ids: ['acc_1'], + }); + expect(delColl).toBeDefined(); + + const postGraph = await endpoints.composite.postCompositeGraph(ctx, { + graphs: [], + }); + expect(postGraph.graphs).toBeDefined(); + + const graphActDep = await endpoints.composite.compositeGraphAction(ctx, { + graphs: [], + }); + expect(graphActDep.graphs).toBeDefined(); + + const batchRec = await endpoints.composite.getABatchOfRecords(ctx, { + ids: ['acc_1'], + }); + expect(batchRec.results).toBeDefined(); + + const compRes = await endpoints.composite.getCompositeResources(ctx, {}); + expect(compRes).toBeDefined(); + + const compSob = await endpoints.composite.getCompositeSobjects(ctx, { + ids: ['acc_1'], + }); + expect(compSob).toBeDefined(); + + const sobColl = await endpoints.composite.getSobjectCollections(ctx, { + ids: ['acc_1'], + }); + expect(sobColl).toBeDefined(); + }); + + it('handles files', async () => { + const content = await endpoints.files.getFileContent(ctx, { + fileId: 'doc_123', + }); + expect(content.content).toBeDefined(); + + const info = await endpoints.files.getFileInformation(ctx, { + fileId: 'doc_123', + }); + expect(info.fileId).toBe('doc_123'); + + const shares = await endpoints.files.getFileShares(ctx, { + fileId: 'doc_123', + }); + expect(shares.shares).toBeDefined(); + + const delF = await endpoints.files.deleteFile(ctx, { fileId: 'doc_123' }); + expect(delF.success).toBe(true); + + const uploaded = await endpoints.files.uploadFile(ctx, { + title: 'notes.txt', + versionData: 'aGVsbG8=', + }); + expect(uploaded).toBeDefined(); + }); + + it('handles analytics and reports', async () => { + const dash = await endpoints.analyticsReports.getDashboard(ctx, { + dashboardId: 'dash_123', + }); + expect(dash).toBeDefined(); + + const listD = await endpoints.analyticsReports.listDashboards(ctx, {}); + expect(listD.dashboards).toBeDefined(); + + const listET = await endpoints.analyticsReports.listEmailTemplates( + ctx, + {}, + ); + expect(listET.templates).toBeDefined(); + + const listR = await endpoints.analyticsReports.listReports(ctx, {}); + expect(listR.reports).toBeDefined(); + + const runR = await endpoints.analyticsReports.runReport(ctx, { + reportId: 'rep_123', + }); + expect(runR).toBeDefined(); + + const listAT = await endpoints.analyticsReports.listAnalyticsTemplates( + ctx, + {}, + ); + expect(listAT.templates).toBeDefined(); + + const getRI = await endpoints.analyticsReports.getReportInstance(ctx, { + reportId: 'rep_123', + instanceId: 'inst_123', + }); + expect(getRI).toBeDefined(); + + const getR = await endpoints.analyticsReports.getReport(ctx, { + reportId: 'rep_123', + }); + expect(getR).toBeDefined(); + + const qR = await endpoints.analyticsReports.queryReport(ctx, { + id: 'rep_123', + }); + expect(qR).toBeDefined(); + }); + + it('handles metadata operations', async () => { + const cSob = await endpoints.metadata.createSObjectRecord(ctx, { + sobject: 'Account', + fields: { Name: 'Test' }, + }); + expect(cSob.id).toBeDefined(); + + const cloneR = await endpoints.metadata.cloneRecord(ctx, { + sobject: 'Account', + recordId: 'acc_123', + }); + expect(cloneR.id).toBeDefined(); + + const cField = await endpoints.metadata.createCustomField(ctx, { + sobject: 'Account', + developerName: 'Custom', + label: 'Custom', + type: 'Text', + }); + expect(cField.id).toBeDefined(); + + const cObj = await endpoints.metadata.createCustomObject(ctx, { + developerName: 'Custom', + label: 'Custom', + pluralLabel: 'Customs', + }); + expect(cObj.id).toBeDefined(); + + const dSob = await endpoints.metadata.deleteSobject(ctx, { + sobject: 'Account', + id: 'acc_123', + }); + expect(dSob.success).toBe(true); + + const dRows = await endpoints.metadata.deleteSobjectRows(ctx, { + sobject: 'Account', + id: 'acc_123', + }); + expect(dRows.success).toBe(true); + + const getSobs = await endpoints.metadata.getSobjects(ctx, {}); + expect(getSobs).toBeDefined(); + + const execQA = await endpoints.metadata.executeSobjectQuickAction(ctx, { + sobject: 'Account', + actionName: 'NewContact', + }); + expect(execQA.success).toBe(true); + + const orgLimits = await endpoints.metadata.getOrgLimits(ctx, {}); + expect(orgLimits).toBeDefined(); + + const userInfo = await endpoints.metadata.getUserInfo(ctx, {}); + expect(userInfo).toBeDefined(); + + const massXfer = await endpoints.metadata.massTransferOwnership(ctx, { + sobject: 'Account', + fromUserId: 'u1', + toUserId: 'u2', + }); + expect(massXfer.success).toBe(true); + }); + }); +}); diff --git a/packages/salesforce/client.test.ts b/packages/salesforce/client.test.ts new file mode 100644 index 000000000..48c88ced9 --- /dev/null +++ b/packages/salesforce/client.test.ts @@ -0,0 +1,231 @@ +import { AuthMissingError } from 'corsair/core'; +import { ApiError } from 'corsair/http'; +import { + makeSalesforceRequest, + SALESFORCE_API_VERSION, + SalesforceInstanceUrlMissingError, + SalesforceRequestOriginError, +} from './client'; + +type Captured = { + url: string; + method: string; + headers: Record; + body?: string; +}; + +let captured: Captured | undefined; + +function mockFetch(response: { + ok?: boolean; + status?: number; + body?: unknown; + headers?: Record; + text?: string; + bytes?: Uint8Array; +}) { + captured = undefined; + global.fetch = (async (url: unknown, init?: RequestInit) => { + const headers: Record = {}; + const raw = init?.headers; + if (raw instanceof Headers) { + raw.forEach((v, k) => { + headers[k] = v; + headers[k.toLowerCase()] = v; + }); + } else if (raw && typeof raw === 'object' && !Array.isArray(raw)) { + for (const [k, v] of Object.entries(raw as Record)) { + headers[k] = v; + headers[k.toLowerCase()] = v; + } + } + captured = { + url: String(url), + method: String(init?.method ?? 'GET'), + headers, + body: typeof init?.body === 'string' ? init.body : undefined, + }; + const headerMap = new Headers( + response.headers ?? { 'content-type': 'application/json' }, + ); + return { + ok: response.ok ?? true, + status: response.status ?? 200, + statusText: 'OK', + headers: headerMap, + json: async () => response.body ?? {}, + text: async () => response.text ?? JSON.stringify(response.body ?? {}), + arrayBuffer: async () => + response.bytes + ? response.bytes.slice().buffer + : new Uint8Array().buffer, + clone() { + return this; + }, + } as Response; + }) as typeof fetch; +} + +describe('makeSalesforceRequest', () => { + const instanceUrl = 'https://example.my.salesforce.com'; + + it('throws AuthMissingError when the token is empty', async () => { + await expect( + makeSalesforceRequest('sobjects/Account', '', { instanceUrl }), + ).rejects.toBeInstanceOf(AuthMissingError); + }); + + it('throws when instance URL is missing', async () => { + await expect( + makeSalesforceRequest('sobjects/Account', 'token'), + ).rejects.toBeInstanceOf(SalesforceInstanceUrlMissingError); + }); + + it('calls the org host, not login.salesforce.com', async () => { + mockFetch({ body: { Id: '001xx' } }); + await makeSalesforceRequest('sobjects/Account/001xx', 'session', { + instanceUrl, + }); + expect(captured?.url).toContain(instanceUrl); + expect(captured?.url).not.toContain('login.salesforce.com'); + expect(captured?.url).toContain( + `/services/data/v${SALESFORCE_API_VERSION}/`, + ); + expect( + captured?.headers.authorization ?? captured?.headers.Authorization, + ).toBe('Bearer session'); + }); + + it('does not wrap ApiError so 429 retryAfter survives', async () => { + mockFetch({ + ok: false, + status: 429, + body: [{ errorCode: 'REQUEST_LIMIT_EXCEEDED', message: 'slow down' }], + headers: { 'content-type': 'application/json', 'retry-after': '2' }, + }); + await expect( + makeSalesforceRequest('query', 'token', { instanceUrl }), + ).rejects.toBeInstanceOf(ApiError); + }); + + it('sends PUT CSV as text/csv to the org host', async () => { + mockFetch({ ok: true, status: 201, text: '', body: undefined }); + await makeSalesforceRequest('jobs/ingest/job1/batches', 'token', { + instanceUrl, + method: 'PUT', + body: 'Name\nAcme', + mediaType: 'text/csv', + }); + expect(captured?.method).toBe('PUT'); + expect( + captured?.headers['content-type'] ?? captured?.headers['Content-Type'], + ).toContain('text/csv'); + expect(captured?.body).toBe('Name\nAcme'); + }); + + it('discovers instance URL from userinfo urls.rest', async () => { + const { discoverSalesforceInstanceUrl } = await import('./client'); + mockFetch({ + body: { + urls: { + rest: 'https://na1.salesforce.com/services/data/v60.0/', + }, + }, + }); + await expect(discoverSalesforceInstanceUrl('token')).resolves.toBe( + 'https://na1.salesforce.com', + ); + }); + + it('rejects HTTP instance URLs and off-origin absolute endpoints', async () => { + mockFetch({ body: { Id: '001xx' } }); + await expect( + makeSalesforceRequest('sobjects/Account', 'token', { + instanceUrl: 'http://example.my.salesforce.com', + }), + ).rejects.toBeInstanceOf(SalesforceRequestOriginError); + + captured = undefined; + await expect( + makeSalesforceRequest('https://evil.example/steal', 'token', { + instanceUrl, + }), + ).rejects.toBeInstanceOf(SalesforceRequestOriginError); + expect(captured).toBeUndefined(); + }); + + it('keeps same-origin absolute endpoints on the org host', async () => { + mockFetch({ body: { records: [] } }); + await makeSalesforceRequest( + `${instanceUrl}/services/data/v${SALESFORCE_API_VERSION}/query/01gxx`, + 'token', + { instanceUrl }, + ); + expect(new URL(captured?.url ?? '').origin).toBe(instanceUrl); + expect(captured?.url).toContain('/query/01gxx'); + }); + + it('returns VersionData as raw bytes', async () => { + const bytes = Uint8Array.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10]); + mockFetch({ + bytes, + headers: { 'content-type': 'application/octet-stream' }, + }); + const buf = await makeSalesforceRequest( + 'sobjects/ContentVersion/068xx/VersionData', + 'token', + { instanceUrl, responseType: 'binary' }, + ); + expect(Buffer.from(buf as Buffer)).toEqual(Buffer.from(bytes)); + expect(captured?.headers.accept ?? captured?.headers.Accept).toContain( + 'octet-stream', + ); + }); + + it('retries binary VersionData on 429', async () => { + const bytes = Uint8Array.from([0xff, 0xd8]); + let calls = 0; + global.fetch = (async (url: unknown, init?: RequestInit) => { + calls += 1; + captured = { + url: String(url), + method: String(init?.method ?? 'GET'), + headers: {}, + }; + if (calls === 1) { + return { + ok: false, + status: 429, + statusText: 'Too Many Requests', + headers: new Headers({ 'retry-after': '0' }), + json: async () => [{ errorCode: 'REQUEST_LIMIT_EXCEEDED' }], + text: async () => '', + arrayBuffer: async () => new Uint8Array().buffer, + clone() { + return this; + }, + } as Response; + } + return { + ok: true, + status: 200, + statusText: 'OK', + headers: new Headers({ 'content-type': 'application/octet-stream' }), + json: async () => ({}), + text: async () => '', + arrayBuffer: async () => bytes.slice().buffer, + clone() { + return this; + }, + } as Response; + }) as typeof fetch; + + const buf = await makeSalesforceRequest( + 'sobjects/ContentVersion/068xx/VersionData', + 'token', + { instanceUrl, responseType: 'binary' }, + ); + expect(calls).toBe(2); + expect(Buffer.from(buf as Buffer)).toEqual(Buffer.from(bytes)); + }); +}); diff --git a/packages/salesforce/client.ts b/packages/salesforce/client.ts new file mode 100644 index 000000000..72794d4bb --- /dev/null +++ b/packages/salesforce/client.ts @@ -0,0 +1,278 @@ +import { AuthMissingError } from 'corsair/core'; +import type { + ApiRequestOptions, + OpenAPIConfig, + RateLimitConfig, +} from 'corsair/http'; +import { ApiError, request } from 'corsair/http'; + +/** + * REST API version used in `/services/data/vXX.X/…` paths. + * Docs: https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_discoveryresource.htm + */ +export const SALESFORCE_API_VERSION = '60.0'; + +/** + * Login host for OAuth and userinfo. API calls go to the org instance URL, + * never here. + * Docs: https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_endpoints.htm + */ +export const SALESFORCE_LOGIN_HOST = 'https://login.salesforce.com'; + +/** + * Concurrent API request limit is org-specific; 429 / REQUEST_LIMIT_EXCEEDED + * carries Retry-After when present. + * Docs: https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/errorcodes.htm + */ +const SALESFORCE_RATE_LIMIT_CONFIG: RateLimitConfig = { + enabled: true, + maxRetries: 3, + initialRetryDelay: 1000, + backoffMultiplier: 2, + headerNames: { + retryAfter: 'Retry-After', + }, +}; + +export class SalesforceInstanceUrlMissingError extends Error { + constructor() { + super( + 'Salesforce requires an instance URL. OAuth token responses include ' + + '`instance_url`; set `instanceUrl` on the plugin options or store it ' + + 'under the `instance_url` account key.', + ); + this.name = 'SalesforceInstanceUrlMissingError'; + } +} + +export class SalesforceRequestOriginError extends Error { + constructor() { + super( + 'Salesforce request URL origin must be HTTPS and match the org instance URL', + ); + this.name = 'SalesforceRequestOriginError'; + } +} + +export type SalesforceRequestOptions = { + method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD'; + body?: Record | unknown[] | string; + query?: Record; + headers?: Record; + instanceUrl?: string; + responseType?: 'json' | 'text' | 'binary'; + mediaType?: string; +}; + +type UserInfoUrls = { + rest?: string; + enterprise?: string; + custom_domain?: string; +}; + +/** + * Discovers the org instance URL from the OpenID userinfo endpoint. + * Docs: https://help.salesforce.com/s/articleView?id=sf.remoteaccess_using_userinfo_endpoint.htm + */ +export async function discoverSalesforceInstanceUrl( + accessToken: string, + loginHost = SALESFORCE_LOGIN_HOST, +): Promise { + const config: OpenAPIConfig = { + BASE: loginHost, + VERSION: SALESFORCE_API_VERSION, + WITH_CREDENTIALS: false, + CREDENTIALS: 'omit', + TOKEN: undefined, + HEADERS: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + }, + }; + + const payload = await request<{ + urls?: UserInfoUrls; + }>(config, { method: 'GET', url: '/services/oauth2/userinfo' }); + + const rest = payload?.urls?.rest; + if (typeof rest === 'string' && rest.startsWith('https://')) { + return new URL(rest).origin; + } + + throw new SalesforceInstanceUrlMissingError(); +} + +function compactQuery( + query: Record | undefined, +): Record | undefined { + if (!query) return undefined; + const out: Record = {}; + for (const [key, value] of Object.entries(query)) { + if (value !== undefined) out[key] = value; + } + return Object.keys(out).length > 0 ? out : undefined; +} + +function toPath(endpoint: string): string { + if (endpoint.startsWith('http://') || endpoint.startsWith('https://')) { + const parsed = new URL(endpoint); + return parsed.pathname + parsed.search; + } + if (endpoint.startsWith('/')) return endpoint; + return `/services/data/v${SALESFORCE_API_VERSION}/${endpoint}`; +} + +function httpsOrigin(url: string): string { + const parsed = new URL(url); + if (parsed.protocol !== 'https:') { + throw new SalesforceRequestOriginError(); + } + return parsed.origin; +} + +function originOf(endpoint: string, instanceUrl: string): string { + const instanceOrigin = httpsOrigin(instanceUrl); + if (endpoint.startsWith('http://') || endpoint.startsWith('https://')) { + if (httpsOrigin(endpoint) !== instanceOrigin) { + throw new SalesforceRequestOriginError(); + } + } + return instanceOrigin; +} + +async function errorBody(res: Response): Promise { + try { + return await res.json(); + } catch { + return await res.text(); + } +} + +function retryDelayMs(res: Response, attempt: number): number { + const retryAfter = res.headers.get( + SALESFORCE_RATE_LIMIT_CONFIG.headerNames.retryAfter ?? 'Retry-After', + ); + const seconds = retryAfter ? Number.parseInt(retryAfter, 10) : Number.NaN; + if (!Number.isNaN(seconds)) return Math.min(seconds * 1000, 60_000); + return Math.min( + SALESFORCE_RATE_LIMIT_CONFIG.initialRetryDelay * + SALESFORCE_RATE_LIMIT_CONFIG.backoffMultiplier ** (attempt - 1), + 60_000, + ); +} + +async function fetchSalesforceBinary( + url: string, + path: string, + method: NonNullable, + headers: Record, +): Promise { + const maxAttempts = SALESFORCE_RATE_LIMIT_CONFIG.maxRetries + 1; + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + const res = await fetch(url, { method, headers }); + if ( + SALESFORCE_RATE_LIMIT_CONFIG.enabled && + res.status === 429 && + attempt < maxAttempts + ) { + await new Promise((resolve) => + setTimeout(resolve, retryDelayMs(res, attempt)), + ); + continue; + } + if (!res.ok) { + throw new ApiError( + { method, url: path }, + { + url, + ok: false, + status: res.status, + statusText: res.statusText, + body: await errorBody(res), + }, + res.statusText, + res.status === 429 + ? { retryAfter: retryDelayMs(res, attempt) } + : undefined, + ); + } + return Buffer.from(await res.arrayBuffer()); + } + throw new Error('Salesforce binary request failed'); +} + +/** + * Issues a Salesforce REST request. + * + * Failures stay as `ApiError` so status, Salesforce error arrays, and + * Retry-After reach the plugin error handlers. Wrapping them would drop + * `retryAfter` and skip RATE_LIMIT_ERROR. + * + * Auth: `Authorization: Bearer `. + * Docs: https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/quickstart_oauth.htm + */ +export async function makeSalesforceRequest( + endpoint: string, + apiKey: string, + options: SalesforceRequestOptions = {}, +): Promise { + if (!apiKey) { + throw new AuthMissingError('salesforce', 'oauth_2'); + } + + const instanceUrl = options.instanceUrl; + if (!instanceUrl) { + throw new SalesforceInstanceUrlMissingError(); + } + + const method = options.method ?? 'GET'; + const origin = originOf(endpoint, instanceUrl); + const path = toPath(endpoint); + const authorization = apiKey.startsWith('Bearer ') + ? apiKey + : `Bearer ${apiKey}`; + + if (options.responseType === 'binary') { + return (await fetchSalesforceBinary(`${origin}${path}`, path, method, { + Accept: 'application/octet-stream', + Authorization: authorization, + ...options.headers, + })) as T; + } + + const hasJsonBody = + method === 'POST' || method === 'PUT' || method === 'PATCH'; + const mediaType = + options.mediaType ?? + (hasJsonBody && typeof options.body !== 'string' + ? 'application/json; charset=utf-8' + : options.mediaType); + + const headers: Record = { + Accept: 'application/json', + Authorization: authorization, + ...options.headers, + }; + + const config: OpenAPIConfig = { + BASE: origin, + VERSION: SALESFORCE_API_VERSION, + WITH_CREDENTIALS: false, + CREDENTIALS: 'omit', + TOKEN: undefined, + HEADERS: headers, + }; + + const requestOptions: ApiRequestOptions = { + method, + url: path, + body: hasJsonBody ? options.body : undefined, + mediaType, + query: compactQuery(options.query), + responseHeader: options.responseType === 'text' ? 'text' : undefined, + }; + + return await request(config, requestOptions, { + rateLimitConfig: SALESFORCE_RATE_LIMIT_CONFIG, + }); +} diff --git a/packages/salesforce/endpoints/accounts.ts b/packages/salesforce/endpoints/accounts.ts new file mode 100644 index 000000000..04632f10a --- /dev/null +++ b/packages/salesforce/endpoints/accounts.ts @@ -0,0 +1,210 @@ +import { logEventFromContext } from 'corsair/core'; +import type { SalesforceEndpoints } from '..'; +import { SalesforceAccountEntity } from '../schema/database'; +import { escapeSoql } from '../utils'; +import { cacheEntities, cacheEntity, evictEntity } from './persist'; +import { flattenFields, salesforceCall, soqlList } from './shared'; + +const LABEL = 'account'; +const DEFAULT_FIELDS = [ + 'Id', + 'Name', + 'Type', + 'Industry', + 'Phone', + 'Website', + 'OwnerId', + 'CreatedDate', + 'LastModifiedDate', +]; + +export const createAccount: SalesforceEndpoints['createAccount'] = async ( + ctx, + input, +) => { + const body = flattenFields(input); + const response = await salesforceCall<{ + id: string; + success: boolean; + errors?: unknown[]; + }>(ctx, 'sobjects/Account', { method: 'POST', body }); + + await cacheEntity( + ctx.db?.account, + SalesforceAccountEntity, + { + Id: response.id, + ...body, + }, + { label: LABEL }, + ); + + await logEventFromContext( + ctx, + 'salesforce.account.created', + { Name: input.Name }, + 'completed', + ); + return response; +}; + +export const getAccount: SalesforceEndpoints['getAccount'] = async ( + ctx, + input, +) => { + const fields = input.fields?.join(',') || DEFAULT_FIELDS.join(','); + const response = await salesforceCall<{ + Id: string; + Name?: string; + }>(ctx, `sobjects/Account/${input.id}`, { method: 'GET', query: { fields } }); + + await cacheEntity(ctx.db?.account, SalesforceAccountEntity, response, { + label: LABEL, + }); + + await logEventFromContext(ctx, 'salesforce.account.get', input, 'completed'); + return response; +}; + +export const listAccounts: SalesforceEndpoints['listAccounts'] = async ( + ctx, + input, +) => { + const fields = input.fields?.length ? input.fields : DEFAULT_FIELDS; + const q = soqlList('Account', fields, input); + + const response = await salesforceCall<{ + totalSize: number; + done: boolean; + records: Array>; + nextRecordsUrl?: string; + }>(ctx, 'query', { method: 'GET', query: { q } }); + + await cacheEntities( + ctx.db?.account, + SalesforceAccountEntity, + response.records, + { label: LABEL }, + ); + + await logEventFromContext(ctx, 'salesforce.account.list', input, 'completed'); + return response; +}; + +export const searchAccounts: SalesforceEndpoints['searchAccounts'] = async ( + ctx, + input, +) => { + const terms: string[] = []; + if (input.name) terms.push(`Name LIKE '%${escapeSoql(input.name)}%'`); + if (input.industry) terms.push(`Industry = '${escapeSoql(input.industry)}'`); + if (input.type) terms.push(`Type = '${escapeSoql(input.type)}'`); + if (input.phone) terms.push(`Phone LIKE '%${escapeSoql(input.phone)}%'`); + + const q = soqlList('Account', DEFAULT_FIELDS, { + limit: input.limit ?? 50, + query: terms.length > 0 ? terms.join(' AND ') : undefined, + }); + + const response = await salesforceCall<{ + records: Array>; + }>(ctx, 'query', { method: 'GET', query: { q } }); + + await cacheEntities( + ctx.db?.account, + SalesforceAccountEntity, + response.records, + { label: LABEL }, + ); + + await logEventFromContext( + ctx, + 'salesforce.account.search', + input, + 'completed', + ); + return { records: response.records ?? [] }; +}; + +export const updateAccount: SalesforceEndpoints['updateAccount'] = async ( + ctx, + input, +) => { + const { id, ...fields } = input; + const body = flattenFields(fields); + await salesforceCall(ctx, `sobjects/Account/${id}`, { + method: 'PATCH', + body, + }); + + await cacheEntity( + ctx.db?.account, + SalesforceAccountEntity, + { + Id: id, + ...body, + }, + { label: LABEL }, + ); + + await logEventFromContext( + ctx, + 'salesforce.account.update', + { id }, + 'completed', + ); + return { success: true }; +}; + +export const deleteAccount: SalesforceEndpoints['deleteAccount'] = async ( + ctx, + input, +) => { + await salesforceCall(ctx, `sobjects/Account/${input.id}`, { + method: 'DELETE', + }); + + await evictEntity(ctx.db?.account, input.id, LABEL); + + await logEventFromContext( + ctx, + 'salesforce.account.deleted', + input, + 'completed', + ); + return { success: true }; +}; + +export const accountCreationWithContentTypeOption: SalesforceEndpoints['accountCreationWithContentTypeOption'] = + createAccount as unknown as SalesforceEndpoints['accountCreationWithContentTypeOption']; + +export const fetchAccountByIdWithQuery: SalesforceEndpoints['fetchAccountByIdWithQuery'] = + async (ctx, input) => { + const fields = input.fields + ? input.fields.split(',').map((f) => f.trim()) + : undefined; + return await getAccount(ctx, { id: input.id, fields }); + }; + +export const removeAccountByUniqueIdentifier: SalesforceEndpoints['removeAccountByUniqueIdentifier'] = + deleteAccount as unknown as SalesforceEndpoints['removeAccountByUniqueIdentifier']; + +export const retrieveAccountDataAndErrorResponses: SalesforceEndpoints['retrieveAccountDataAndErrorResponses'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + 'sobjects/Account/describe', + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.account.retrieve_data_and_error_responses', + input, + 'completed', + ); + return { objectDescribe: response }; + }; + +export const updateAccountObjectById: SalesforceEndpoints['updateAccountObjectById'] = + updateAccount as unknown as SalesforceEndpoints['updateAccountObjectById']; diff --git a/packages/salesforce/endpoints/analytics-reports.ts b/packages/salesforce/endpoints/analytics-reports.ts new file mode 100644 index 000000000..b3a21e547 --- /dev/null +++ b/packages/salesforce/endpoints/analytics-reports.ts @@ -0,0 +1,177 @@ +import { logEventFromContext } from 'corsair/core'; +import type { SalesforceEndpoints } from '..'; +import { escapeSoql } from '../utils'; +import { salesforceCall } from './shared'; + +export const getDashboard: SalesforceEndpoints['getDashboard'] = async ( + ctx, + input, +) => { + const response = await salesforceCall>( + ctx, + `analytics/dashboards/${input.dashboardId}`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.analytics.get_dashboard', + input, + 'completed', + ); + return response; +}; + +export const listDashboards: SalesforceEndpoints['listDashboards'] = async ( + ctx, + _input, +) => { + const response = await salesforceCall>>( + ctx, + 'analytics/dashboards', + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.analytics.list_dashboards', + {}, + 'completed', + ); + return { dashboards: Array.isArray(response) ? response : [] }; +}; + +export const listEmailTemplates: SalesforceEndpoints['listEmailTemplates'] = + async (ctx, input) => { + const terms: string[] = []; + if (input.name) terms.push(`Name LIKE '%${escapeSoql(input.name)}%'`); + if (input.developerName) + terms.push(`DeveloperName = '${escapeSoql(input.developerName)}'`); + if (input.folderId) + terms.push(`FolderId = '${escapeSoql(input.folderId)}'`); + const whereStr = terms.length > 0 ? ` WHERE ${terms.join(' AND ')}` : ''; + const q = `SELECT Id, Name, DeveloperName, FolderId, Subject FROM EmailTemplate${whereStr}`; + + const response = await salesforceCall<{ + records: Array>; + }>(ctx, 'query', { method: 'GET', query: { q } }); + + await logEventFromContext( + ctx, + 'salesforce.analytics.list_email_templates', + input, + 'completed', + ); + return { templates: response.records ?? [] }; + }; + +export const listReports: SalesforceEndpoints['listReports'] = async ( + ctx, + _input, +) => { + const response = await salesforceCall>>( + ctx, + 'analytics/reports', + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.analytics.list_reports', + {}, + 'completed', + ); + return { reports: Array.isArray(response) ? response : [] }; +}; + +export const runReport: SalesforceEndpoints['runReport'] = async ( + ctx, + input, +) => { + const response = await salesforceCall>( + ctx, + `analytics/reports/${input.reportId}`, + { method: 'POST' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.analytics.run_report', + input, + 'completed', + ); + return response; +}; + +export const listAnalyticsTemplates: SalesforceEndpoints['listAnalyticsTemplates'] = + async (ctx, _input) => { + const response = await salesforceCall<{ + templates: Array>; + }>(ctx, 'wave/templates', { method: 'GET' }); + + await logEventFromContext( + ctx, + 'salesforce.analytics.list_analytics_templates', + {}, + 'completed', + ); + return { templates: response.templates ?? [] }; + }; + +/** @deprecated */ +export const getReportInstance: SalesforceEndpoints['getReportInstance'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + `analytics/reports/${input.reportId}/instances/${input.instanceId}`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.analytics.get_report_instance_deprecated', + input, + 'completed', + ); + return response; + }; + +/** @deprecated */ +export const getReport: SalesforceEndpoints['getReport'] = async ( + ctx, + input, +) => { + const response = await salesforceCall>( + ctx, + `analytics/reports/${input.reportId}/describe`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.analytics.get_report_deprecated', + input, + 'completed', + ); + return response; +}; + +/** @deprecated */ +export const queryReport: SalesforceEndpoints['queryReport'] = async ( + ctx, + input, +) => { + const response = await salesforceCall>( + ctx, + `analytics/reports/${input.id}`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.analytics.query_report_deprecated', + input, + 'completed', + ); + return response; +}; diff --git a/packages/salesforce/endpoints/campaigns.ts b/packages/salesforce/endpoints/campaigns.ts new file mode 100644 index 000000000..40d937cdb --- /dev/null +++ b/packages/salesforce/endpoints/campaigns.ts @@ -0,0 +1,294 @@ +import { logEventFromContext } from 'corsair/core'; +import type { SalesforceEndpoints } from '..'; +import { escapeSoql, soqlWhere } from '../utils'; +import { flattenFields, salesforceCall } from './shared'; + +export const createCampaign: SalesforceEndpoints['createCampaign'] = async ( + ctx, + input, +) => { + const response = await salesforceCall<{ + id: string; + success?: boolean; + }>(ctx, 'sobjects/Campaign', { method: 'POST', body: flattenFields(input) }); + + await logEventFromContext( + ctx, + 'salesforce.campaign.create', + input, + 'completed', + ); + return response; +}; + +export const getCampaign: SalesforceEndpoints['getCampaign'] = async ( + ctx, + input, +) => { + const response = await salesforceCall<{ Id: string }>( + ctx, + `sobjects/Campaign/${input.id}`, + { method: 'GET' }, + ); + + await logEventFromContext(ctx, 'salesforce.campaign.get', input, 'completed'); + return response; +}; + +export const listCampaigns: SalesforceEndpoints['listCampaigns'] = async ( + ctx, + input, +) => { + const limit = input.limit ?? 200; + const queryClause = soqlWhere(input.query); + const whereStr = queryClause ? ` WHERE ${queryClause}` : ''; + const q = `SELECT Id, Name, Type, Status, StartDate, EndDate, IsActive FROM Campaign${whereStr} LIMIT ${limit}`; + + const response = await salesforceCall<{ + totalSize: number; + done: boolean; + records: Array>; + }>(ctx, 'query', { method: 'GET', query: { q } }); + + await logEventFromContext( + ctx, + 'salesforce.campaign.list', + input, + 'completed', + ); + return response; +}; + +export const deleteCampaign: SalesforceEndpoints['deleteCampaign'] = async ( + ctx, + input, +) => { + await salesforceCall(ctx, `sobjects/Campaign/${input.id}`, { + method: 'DELETE', + }); + + await logEventFromContext( + ctx, + 'salesforce.campaign.delete', + input, + 'completed', + ); + return { success: true }; +}; + +export const addContactToCampaign: SalesforceEndpoints['addContactToCampaign'] = + async (ctx, input) => { + const response = await salesforceCall<{ id: string }>( + ctx, + 'sobjects/CampaignMember', + { + method: 'POST', + body: { + CampaignId: input.campaignId, + ContactId: input.contactId, + Status: input.status, + }, + }, + ); + + await logEventFromContext( + ctx, + 'salesforce.campaign.add_contact', + input, + 'completed', + ); + return response; + }; + +export const addLeadToCampaign: SalesforceEndpoints['addLeadToCampaign'] = + async (ctx, input) => { + const response = await salesforceCall<{ id: string }>( + ctx, + 'sobjects/CampaignMember', + { + method: 'POST', + body: { + CampaignId: input.campaign_id, + LeadId: input.lead_id, + Status: input.status, + }, + }, + ); + + await logEventFromContext( + ctx, + 'salesforce.campaign.add_lead', + input, + 'completed', + ); + return response; + }; + +export const removeFromCampaign: SalesforceEndpoints['removeFromCampaign'] = + async (ctx, input) => { + let memberIdToDelete = input.campaign_member_id; + + if (!memberIdToDelete && input.member_id) { + if (!input.campaign_id) { + throw new Error( + 'campaign_id is required when looking up CampaignMember by member_id', + ); + } + const safeMemberId = escapeSoql(input.member_id); + const safeCampaignId = escapeSoql(input.campaign_id); + const res = await salesforceCall<{ + records: Array<{ Id: string }>; + }>(ctx, 'query', { + method: 'GET', + query: { + q: `SELECT Id FROM CampaignMember WHERE CampaignId = '${safeCampaignId}' AND (ContactId = '${safeMemberId}' OR LeadId = '${safeMemberId}') LIMIT 1`, + }, + }); + memberIdToDelete = res.records?.[0]?.Id; + if (!memberIdToDelete) { + throw new Error( + 'No CampaignMember found for the requested campaign and member', + ); + } + } + + if (!memberIdToDelete) { + throw new Error( + 'Either campaign_member_id or valid member_id (Contact/Lead ID) must be provided', + ); + } + + await salesforceCall( + ctx, + `sobjects/CampaignMember/${memberIdToDelete}`, + { method: 'DELETE' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.campaign.remove_member', + input, + 'completed', + ); + return { success: true }; + }; + +export const searchCampaigns: SalesforceEndpoints['searchCampaigns'] = async ( + ctx, + input, +) => { + const terms: string[] = []; + if (input.name) terms.push(`Name LIKE '%${escapeSoql(input.name)}%'`); + if (input.type) terms.push(`Type = '${escapeSoql(input.type)}'`); + if (input.status) terms.push(`Status = '${escapeSoql(input.status)}'`); + + const whereStr = terms.length > 0 ? ` WHERE ${terms.join(' AND ')}` : ''; + const q = `SELECT Id, Name, Type, Status, StartDate, EndDate FROM Campaign${whereStr} LIMIT ${input.limit ?? 50}`; + + const response = await salesforceCall<{ + records: Array>; + }>(ctx, 'query', { method: 'GET', query: { q } }); + + await logEventFromContext( + ctx, + 'salesforce.campaign.search', + input, + 'completed', + ); + return { records: response.records ?? [] }; +}; + +/** @deprecated */ +export const createCampaignRecordViaPost: SalesforceEndpoints['createCampaignRecordViaPost'] = + async (ctx, input) => { + const response = await salesforceCall<{ id: string }>( + ctx, + 'sobjects/Campaign', + { method: 'POST', body: input }, + ); + + await logEventFromContext( + ctx, + 'salesforce.campaign.create_record_deprecated', + input, + 'completed', + ); + return response; + }; + +/** @deprecated */ +export const removeCampaignObjectById: SalesforceEndpoints['removeCampaignObjectById'] = + async (ctx, input) => { + await salesforceCall(ctx, `sobjects/Campaign/${input.id}`, { + method: 'DELETE', + }); + + await logEventFromContext( + ctx, + 'salesforce.campaign.remove_deprecated', + input, + 'completed', + ); + return { success: true }; + }; + +/** @deprecated */ +export const retrieveCampaignDataWithErrorHandling: SalesforceEndpoints['retrieveCampaignDataWithErrorHandling'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + 'sobjects/Campaign/describe', + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.campaign.retrieve_data_deprecated', + input, + 'completed', + ); + return { metadata: response }; + }; + +/** @deprecated */ +export const retrieveSpecificCampaignObjectDetails: SalesforceEndpoints['retrieveSpecificCampaignObjectDetails'] = + async (ctx, input) => { + const response = await salesforceCall<{ Id: string }>( + ctx, + `sobjects/Campaign/${input.id}`, + { + method: 'GET', + query: input.fields ? { fields: input.fields.join(',') } : undefined, + }, + ); + + await logEventFromContext( + ctx, + 'salesforce.campaign.retrieve_specific_deprecated', + input, + 'completed', + ); + return response; + }; + +export const updateCampaign: SalesforceEndpoints['updateCampaign'] = async ( + ctx, + input, +) => { + const { id, ...fields } = input; + const body = flattenFields(fields); + await salesforceCall(ctx, `sobjects/Campaign/${id}`, { + method: 'PATCH', + body, + }); + await logEventFromContext( + ctx, + 'salesforce.campaign.update', + { id }, + 'completed', + ); + return { success: true }; +}; + +export const updateCampaignByIdWithJson: SalesforceEndpoints['updateCampaignByIdWithJson'] = + updateCampaign as unknown as SalesforceEndpoints['updateCampaignByIdWithJson']; diff --git a/packages/salesforce/endpoints/composite.ts b/packages/salesforce/endpoints/composite.ts new file mode 100644 index 000000000..cd4aa0fde --- /dev/null +++ b/packages/salesforce/endpoints/composite.ts @@ -0,0 +1,205 @@ +import { logEventFromContext } from 'corsair/core'; +import type { SalesforceEndpoints } from '..'; +import { salesforceCall } from './shared'; + +export const postCompositeSobjects: SalesforceEndpoints['postCompositeSobjects'] = + async (ctx, input) => { + const response = await salesforceCall>>( + ctx, + 'composite/sobjects', + { + method: 'POST', + body: input, + }, + ); + + await logEventFromContext( + ctx, + 'salesforce.composite.post_sobjects', + input, + 'completed', + ); + return response; + }; + +export const createSobjectTree: SalesforceEndpoints['createSobjectTree'] = + async (ctx, input) => { + const response = await salesforceCall<{ + hasErrors: boolean; + results: Array>; + }>(ctx, `composite/tree/${input.sobject}`, { + method: 'POST', + body: { records: input.records }, + }); + + await logEventFromContext( + ctx, + 'salesforce.composite.tree', + input, + 'completed', + ); + return response; + }; + +export const deleteSobjectCollections: SalesforceEndpoints['deleteSobjectCollections'] = + async (ctx, input) => { + const idsQuery = input.ids.join(','); + const response = await salesforceCall>>( + ctx, + 'composite/sobjects', + { + method: 'DELETE', + query: { + ids: idsQuery, + allOrNone: input.allOrNone ? 'true' : 'false', + }, + }, + ); + + await logEventFromContext( + ctx, + 'salesforce.composite.delete_sobjects', + input, + 'completed', + ); + return response; + }; + +export const postCompositeGraph: SalesforceEndpoints['postCompositeGraph'] = + async (ctx, input) => { + const response = await salesforceCall<{ + graphs: Array>; + }>(ctx, 'composite/graph', { + method: 'POST', + body: input, + }); + + await logEventFromContext( + ctx, + 'salesforce.composite.graph', + input, + 'completed', + ); + return response; + }; + +/** @deprecated */ +export const compositeGraphAction: SalesforceEndpoints['compositeGraphAction'] = + async (ctx, input) => { + const response = await salesforceCall<{ + graphs: Array>; + }>(ctx, 'composite/graph', { + method: 'POST', + body: input, + }); + + await logEventFromContext( + ctx, + 'salesforce.composite.graph_deprecated', + input, + 'completed', + ); + return response; + }; + +export const getABatchOfRecords: SalesforceEndpoints['getABatchOfRecords'] = + async (ctx, input) => { + const response = await salesforceCall<{ + results: Array>; + }>(ctx, 'composite/sobjects', { + method: 'POST', + body: { + ids: input.ids, + fields: input.fields, + }, + }); + + await logEventFromContext( + ctx, + 'salesforce.composite.get_batch', + input, + 'completed', + ); + return response; + }; + +export const getCompositeResources: SalesforceEndpoints['getCompositeResources'] = + async (ctx, _input) => { + const response = await salesforceCall>( + ctx, + 'composite', + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.composite.resources', + {}, + 'completed', + ); + return response; + }; + +export const getCompositeSobjects: SalesforceEndpoints['getCompositeSobjects'] = + async (ctx, input) => { + const response = await salesforceCall>>( + ctx, + 'composite/sobjects', + { + method: 'POST', + body: { + ids: input.ids, + fields: input.fields, + }, + }, + ); + + await logEventFromContext( + ctx, + 'salesforce.composite.get_sobjects', + input, + 'completed', + ); + return response; + }; + +export const getSobjectCollections: SalesforceEndpoints['getSobjectCollections'] = + async (ctx, input) => { + const response = await salesforceCall>>( + ctx, + 'composite/sobjects', + { + method: 'POST', + body: { + ids: input.ids, + fields: input.fields, + }, + }, + ); + + await logEventFromContext( + ctx, + 'salesforce.composite.get_sobject_collections', + input, + 'completed', + ); + return response; + }; + +export const patchCompositeSobjects: SalesforceEndpoints['patchCompositeSobjects'] = + async (ctx, input) => { + const response = await salesforceCall(ctx, 'composite/sobjects', { + method: 'PATCH', + body: { + allOrNone: input.allOrNone, + records: input.records, + }, + }); + await logEventFromContext( + ctx, + 'salesforce.composite.patch_sobjects', + input, + 'completed', + ); + return { result: response }; + }; diff --git a/packages/salesforce/endpoints/contacts.ts b/packages/salesforce/endpoints/contacts.ts new file mode 100644 index 000000000..558035485 --- /dev/null +++ b/packages/salesforce/endpoints/contacts.ts @@ -0,0 +1,277 @@ +import { logEventFromContext } from 'corsair/core'; +import type { SalesforceEndpoints } from '..'; +import { SalesforceContactEntity } from '../schema/database'; +import { escapeSoql, soqlWhere } from '../utils'; +import { cacheEntities, cacheEntity, evictEntity } from './persist'; +import { flattenFields, salesforceCall } from './shared'; + +const LABEL = 'contact'; + +export const createContact: SalesforceEndpoints['createContact'] = async ( + ctx, + input, +) => { + const body = flattenFields(input); + + const response = await salesforceCall<{ + id: string; + success?: boolean; + }>(ctx, 'sobjects/Contact', { method: 'POST', body }); + + await cacheEntity( + ctx.db?.contact, + SalesforceContactEntity, + { + Id: response.id, + ...body, + }, + { label: LABEL }, + ); + + await logEventFromContext( + ctx, + 'salesforce.contact.create', + input, + 'completed', + ); + return response; +}; + +export const getContact: SalesforceEndpoints['getContact'] = async ( + ctx, + input, +) => { + const response = await salesforceCall<{ + Id: string; + LastName?: string; + }>(ctx, `sobjects/Contact/${input.id}`, { + method: 'GET', + query: input.fields ? { fields: input.fields.join(',') } : undefined, + }); + + await cacheEntity(ctx.db?.contact, SalesforceContactEntity, response, { + label: LABEL, + }); + + await logEventFromContext(ctx, 'salesforce.contact.get', input, 'completed'); + return response; +}; + +export const listContacts: SalesforceEndpoints['listContacts'] = async ( + ctx, + input, +) => { + const limit = input.limit ?? 200; + const offsetStr = input.offset ? ` OFFSET ${input.offset}` : ''; + const conditions: string[] = []; + if (input.accountId) + conditions.push(`AccountId = '${escapeSoql(input.accountId)}'`); + const queryClause = soqlWhere(input.query); + if (queryClause) conditions.push(queryClause); + + const whereStr = + conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : ''; + const q = `SELECT Id, FirstName, LastName, Email, Phone, AccountId FROM Contact${whereStr} LIMIT ${limit}${offsetStr}`; + + const response = await salesforceCall<{ + totalSize: number; + done: boolean; + records: Array>; + nextRecordsUrl?: string; + }>(ctx, 'query', { method: 'GET', query: { q } }); + + await cacheEntities( + ctx.db?.contact, + SalesforceContactEntity, + response.records, + { label: LABEL }, + ); + + await logEventFromContext(ctx, 'salesforce.contact.list', input, 'completed'); + return response; +}; + +export const deleteContact: SalesforceEndpoints['deleteContact'] = async ( + ctx, + input, +) => { + await salesforceCall(ctx, `sobjects/Contact/${input.id}`, { + method: 'DELETE', + }); + + await evictEntity(ctx.db?.contact, input.id, LABEL); + + await logEventFromContext( + ctx, + 'salesforce.contact.delete', + input, + 'completed', + ); + return { success: true }; +}; + +export const associateContactToAccount: SalesforceEndpoints['associateContactToAccount'] = + async (ctx, input) => { + await salesforceCall(ctx, `sobjects/Contact/${input.contactId}`, { + method: 'PATCH', + body: { AccountId: input.accountId }, + }); + + await logEventFromContext( + ctx, + 'salesforce.contact.associate_account', + input, + 'completed', + ); + return { success: true }; + }; + +/** @deprecated */ +export const createNewContactWithJsonHeader: SalesforceEndpoints['createNewContactWithJsonHeader'] = + async (ctx, input) => { + const response = await salesforceCall<{ id: string }>( + ctx, + 'sobjects/Contact', + { method: 'POST', body: input }, + ); + + await logEventFromContext( + ctx, + 'salesforce.contact.create_deprecated', + input, + 'completed', + ); + return response; + }; + +/** @deprecated */ +export const queryContactsByName: SalesforceEndpoints['queryContactsByName'] = + async (ctx, input) => { + const q = `SELECT Id, FirstName, LastName, Email FROM Contact WHERE Name LIKE '%${escapeSoql(input.name)}%'`; + const response = await salesforceCall<{ + records: Array>; + }>(ctx, 'query', { method: 'GET', query: { q } }); + + await logEventFromContext( + ctx, + 'salesforce.contact.query_by_name_deprecated', + input, + 'completed', + ); + return { records: response.records ?? [] }; + }; + +/** @deprecated */ +export const removeASpecificContactById: SalesforceEndpoints['removeASpecificContactById'] = + async (ctx, input) => { + await salesforceCall(ctx, `sobjects/Contact/${input.id}`, { + method: 'DELETE', + }); + + await logEventFromContext( + ctx, + 'salesforce.contact.remove_deprecated', + input, + 'completed', + ); + return { success: true }; + }; + +/** @deprecated */ +export const retrieveContactInfoWithStandardResponses: SalesforceEndpoints['retrieveContactInfoWithStandardResponses'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + 'sobjects/Contact/describe', + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.contact.retrieve_info_deprecated', + input, + 'completed', + ); + return { metadata: response }; + }; + +export const getContactById: SalesforceEndpoints['getContactById'] = async ( + ctx, + input, +) => { + const response = await salesforceCall<{ Id: string }>( + ctx, + `sobjects/Contact/${input.id}`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.contact.get_by_id', + input, + 'completed', + ); + return response; +}; + +export const updateContact: SalesforceEndpoints['updateContact'] = async ( + ctx, + input, +) => { + const { id, ...fields } = input; + const body = flattenFields(fields); + await salesforceCall(ctx, `sobjects/Contact/${id}`, { + method: 'PATCH', + body, + }); + await cacheEntity( + ctx.db?.contact, + SalesforceContactEntity, + { + Id: id, + ...body, + }, + { label: LABEL }, + ); + await logEventFromContext( + ctx, + 'salesforce.contact.update', + { id }, + 'completed', + ); + return { success: true }; +}; + +export const updateContactById: SalesforceEndpoints['updateContactById'] = + updateContact as unknown as SalesforceEndpoints['updateContactById']; + +export const searchContacts: SalesforceEndpoints['searchContacts'] = async ( + ctx, + input, +) => { + const terms: string[] = []; + if (input.name) terms.push(`Name LIKE '%${escapeSoql(input.name)}%'`); + if (input.email) terms.push(`Email LIKE '%${escapeSoql(input.email)}%'`); + if (input.phone) terms.push(`Phone LIKE '%${escapeSoql(input.phone)}%'`); + if (input.accountId) + terms.push(`AccountId = '${escapeSoql(input.accountId)}'`); + if (input.title) terms.push(`Title LIKE '%${escapeSoql(input.title)}%'`); + const whereStr = terms.length > 0 ? ` WHERE ${terms.join(' AND ')}` : ''; + const q = `SELECT Id, FirstName, LastName, Email, Phone, AccountId, Title FROM Contact${whereStr} LIMIT ${input.limit ?? 50}`; + const response = await salesforceCall<{ + records: Array>; + }>(ctx, 'query', { method: 'GET', query: { q } }); + await cacheEntities( + ctx.db?.contact, + SalesforceContactEntity, + response.records, + { label: LABEL }, + ); + await logEventFromContext( + ctx, + 'salesforce.contact.search', + input, + 'completed', + ); + return { records: response.records ?? [] }; +}; diff --git a/packages/salesforce/endpoints/files.ts b/packages/salesforce/endpoints/files.ts new file mode 100644 index 000000000..9c5840461 --- /dev/null +++ b/packages/salesforce/endpoints/files.ts @@ -0,0 +1,100 @@ +import { logEventFromContext } from 'corsair/core'; +import type { SalesforceEndpoints } from '..'; +import { escapeSoql } from '../utils'; +import { salesforceCall } from './shared'; + +export const getFileContent: SalesforceEndpoints['getFileContent'] = async ( + ctx, + input, +) => { + const bytes = await salesforceCall( + ctx, + `sobjects/ContentVersion/${input.fileId}/VersionData`, + { method: 'GET', responseType: 'binary' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.files.get_content', + input, + 'completed', + ); + return { content: Buffer.from(bytes).toString('base64') }; +}; + +export const getFileInformation: SalesforceEndpoints['getFileInformation'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + `sobjects/ContentDocument/${input.fileId}`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.files.get_information', + input, + 'completed', + ); + return response; + }; + +export const getFileShares: SalesforceEndpoints['getFileShares'] = async ( + ctx, + input, +) => { + const safeFileId = escapeSoql(input.fileId); + const response = await salesforceCall<{ + records: Array>; + }>(ctx, 'query', { + method: 'GET', + query: { + q: `SELECT Id, ContentDocumentId, LinkedEntityId, ShareType FROM ContentDocumentLink WHERE ContentDocumentId = '${safeFileId}'`, + }, + }); + + await logEventFromContext( + ctx, + 'salesforce.files.get_shares', + input, + 'completed', + ); + return { shares: response.records ?? [] }; +}; + +export const deleteFile: SalesforceEndpoints['deleteFile'] = async ( + ctx, + input, +) => { + await salesforceCall(ctx, `sobjects/ContentDocument/${input.fileId}`, { + method: 'DELETE', + }); + + await logEventFromContext(ctx, 'salesforce.files.delete', input, 'completed'); + return { success: true }; +}; + +export const uploadFile: SalesforceEndpoints['uploadFile'] = async ( + ctx, + input, +) => { + const response = await salesforceCall<{ + id: string; + success?: boolean; + }>(ctx, 'sobjects/ContentVersion', { + method: 'POST', + body: { + Title: input.title, + PathOnClient: input.pathOnClient ?? input.title, + VersionData: input.versionData, + FirstPublishLocationId: input.firstPublishLocationId, + }, + }); + await logEventFromContext( + ctx, + 'salesforce.files.upload', + { id: response.id, title: input.title }, + 'completed', + ); + return response; +}; diff --git a/packages/salesforce/endpoints/index.ts b/packages/salesforce/endpoints/index.ts new file mode 100644 index 000000000..db67f96f0 --- /dev/null +++ b/packages/salesforce/endpoints/index.ts @@ -0,0 +1,31 @@ +import * as accounts from './accounts'; +import * as analyticsReports from './analytics-reports'; +import * as campaigns from './campaigns'; +import * as composite from './composite'; +import * as contacts from './contacts'; +import * as files from './files'; +import * as jobs from './jobs'; +import * as leads from './leads'; +import * as metadata from './metadata'; +import * as notes from './notes'; +import * as opportunities from './opportunities'; +import * as soqlSosl from './soql-sosl'; +import * as tasks from './tasks'; +import * as uiApi from './ui-api'; + +export const Accounts = accounts; +export const Contacts = contacts; +export const Leads = leads; +export const Opportunities = opportunities; +export const Campaigns = campaigns; +export const Notes = notes; +export const Tasks = tasks; +export const Jobs = jobs; +export const SoqlSosl = soqlSosl; +export const Composite = composite; +export const Metadata = metadata; +export const UiApi = uiApi; +export const Files = files; +export const AnalyticsReports = analyticsReports; + +export * from './types'; diff --git a/packages/salesforce/endpoints/jobs.ts b/packages/salesforce/endpoints/jobs.ts new file mode 100644 index 000000000..1bf03ea04 --- /dev/null +++ b/packages/salesforce/endpoints/jobs.ts @@ -0,0 +1,160 @@ +import { logEventFromContext } from 'corsair/core'; +import type { SalesforceEndpoints } from '..'; +import { parseCsvRecords } from '../utils'; +import { salesforceCall } from './shared'; + +export const closeOrAbortJob: SalesforceEndpoints['closeOrAbortJob'] = async ( + ctx, + input, +) => { + const response = await salesforceCall<{ id: string; state: string }>( + ctx, + `jobs/ingest/${input.jobId}`, + { + method: 'PATCH', + body: { state: input.state }, + }, + ); + + await logEventFromContext( + ctx, + 'salesforce.job.close_or_abort', + input, + 'completed', + ); + return response; +}; + +export const deleteJobQuery: SalesforceEndpoints['deleteJobQuery'] = async ( + ctx, + input, +) => { + await salesforceCall(ctx, `jobs/query/${input.jobId}`, { + method: 'DELETE', + }); + + await logEventFromContext( + ctx, + 'salesforce.job.delete_query', + input, + 'completed', + ); + return { success: true }; +}; + +export const getJobFailedRecordResults: SalesforceEndpoints['getJobFailedRecordResults'] = + async (ctx, input) => { + const response = await salesforceCall( + ctx, + `jobs/ingest/${input.jobId}/failedResults`, + { method: 'GET', responseType: 'text' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.job.failed_results', + input, + 'completed', + ); + return { records: parseCsvRecords(response) }; + }; + +export const getQueryJobInfo: SalesforceEndpoints['getQueryJobInfo'] = async ( + ctx, + input, +) => { + const response = await salesforceCall<{ id: string; state: string }>( + ctx, + `jobs/query/${input.jobId}`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.job.query_info', + input, + 'completed', + ); + return response; +}; + +export const getQueryJobResults: SalesforceEndpoints['getQueryJobResults'] = + async (ctx, input) => { + const response = await salesforceCall( + ctx, + `jobs/query/${input.jobId}/results`, + { + method: 'GET', + query: { + maxRecords: input.maxRecords, + locator: input.locator, + }, + responseType: 'text', + }, + ); + + await logEventFromContext( + ctx, + 'salesforce.job.query_results', + input, + 'completed', + ); + return { + data: + typeof response === 'string' + ? response + : JSON.stringify(response ?? ''), + }; + }; + +export const getJobSuccessfulRecordResults: SalesforceEndpoints['getJobSuccessfulRecordResults'] = + async (ctx, input) => { + const response = await salesforceCall( + ctx, + `jobs/ingest/${input.jobId}/successfulResults`, + { method: 'GET', responseType: 'text' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.job.successful_results', + input, + 'completed', + ); + return { records: parseCsvRecords(response) }; + }; + +export const getJobUnprocessedRecordResults: SalesforceEndpoints['getJobUnprocessedRecordResults'] = + async (ctx, input) => { + const response = await salesforceCall( + ctx, + `jobs/ingest/${input.jobId}/unprocessedrecords`, + { method: 'GET', responseType: 'text' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.job.unprocessed_results', + input, + 'completed', + ); + return { records: parseCsvRecords(response) }; + }; + +export const uploadJobData: SalesforceEndpoints['uploadJobData'] = async ( + ctx, + input, +) => { + await salesforceCall(ctx, `jobs/ingest/${input.jobId}/batches`, { + method: 'PUT', + body: input.csv, + mediaType: 'text/csv', + }); + await logEventFromContext( + ctx, + 'salesforce.job.upload_data', + { jobId: input.jobId }, + 'completed', + ); + return { success: true }; +}; diff --git a/packages/salesforce/endpoints/leads.ts b/packages/salesforce/endpoints/leads.ts new file mode 100644 index 000000000..617c309d8 --- /dev/null +++ b/packages/salesforce/endpoints/leads.ts @@ -0,0 +1,232 @@ +import { logEventFromContext } from 'corsair/core'; +import type { SalesforceEndpoints } from '..'; +import { SalesforceLeadEntity } from '../schema/database'; +import { escapeSoql, soqlWhere } from '../utils'; +import { cacheEntities, cacheEntity, evictEntity } from './persist'; +import { flattenFields, salesforceCall } from './shared'; + +const LABEL = 'lead'; + +export const createLead: SalesforceEndpoints['createLead'] = async ( + ctx, + input, +) => { + const body = flattenFields(input); + + const response = await salesforceCall<{ + id: string; + success?: boolean; + }>(ctx, 'sobjects/Lead', { method: 'POST', body }); + + await cacheEntity( + ctx.db?.lead, + SalesforceLeadEntity, + { + Id: response.id, + ...body, + }, + { label: LABEL }, + ); + + await logEventFromContext(ctx, 'salesforce.lead.create', input, 'completed'); + return response; +}; + +export const getLead: SalesforceEndpoints['getLead'] = async (ctx, input) => { + const response = await salesforceCall<{ Id: string }>( + ctx, + `sobjects/Lead/${input.id}`, + { method: 'GET' }, + ); + + await cacheEntity(ctx.db?.lead, SalesforceLeadEntity, response, { + label: LABEL, + }); + + await logEventFromContext(ctx, 'salesforce.lead.get', input, 'completed'); + return response; +}; + +export const listLeads: SalesforceEndpoints['listLeads'] = async ( + ctx, + input, +) => { + const limit = input.limit ?? 200; + const offsetStr = input.offset ? ` OFFSET ${input.offset}` : ''; + const queryClause = soqlWhere(input.query); + const whereStr = queryClause ? ` WHERE ${queryClause}` : ''; + const q = `SELECT Id, FirstName, LastName, Company, Email, Status FROM Lead${whereStr} LIMIT ${limit}${offsetStr}`; + + const response = await salesforceCall<{ + totalSize: number; + done: boolean; + records: Array>; + nextRecordsUrl?: string; + }>(ctx, 'query', { method: 'GET', query: { q } }); + + await cacheEntities(ctx.db?.lead, SalesforceLeadEntity, response.records, { + label: LABEL, + }); + + await logEventFromContext(ctx, 'salesforce.lead.list', input, 'completed'); + return response; +}; + +export const deleteLead: SalesforceEndpoints['deleteLead'] = async ( + ctx, + input, +) => { + await salesforceCall(ctx, `sobjects/Lead/${input.id}`, { + method: 'DELETE', + }); + + await evictEntity(ctx.db?.lead, input.id, LABEL); + + await logEventFromContext(ctx, 'salesforce.lead.delete', input, 'completed'); + return { success: true }; +}; + +export const applyLeadAssignmentRules: SalesforceEndpoints['applyLeadAssignmentRules'] = + async (ctx, input) => { + const headers: Record = {}; + if (input.assignmentRuleId) { + headers['Sforce-Auto-Assign'] = input.assignmentRuleId; + } else { + headers['Sforce-Auto-Assign'] = 'TRUE'; + } + + await salesforceCall(ctx, `sobjects/Lead/${input.leadId}`, { + method: 'PATCH', + body: {}, + headers, + }); + + await logEventFromContext( + ctx, + 'salesforce.lead.apply_assignment_rules', + input, + 'completed', + ); + return { success: true }; + }; + +/** @deprecated */ +export const createLeadWithSpecifiedContentType: SalesforceEndpoints['createLeadWithSpecifiedContentType'] = + async (ctx, input) => { + const response = await salesforceCall<{ id: string }>( + ctx, + 'sobjects/Lead', + { method: 'POST', body: input }, + ); + + await logEventFromContext( + ctx, + 'salesforce.lead.create_deprecated', + input, + 'completed', + ); + return response; + }; + +/** @deprecated */ +export const deleteALeadObjectByItsId: SalesforceEndpoints['deleteALeadObjectByItsId'] = + async (ctx, input) => { + await salesforceCall(ctx, `sobjects/Lead/${input.id}`, { + method: 'DELETE', + }); + + await logEventFromContext( + ctx, + 'salesforce.lead.delete_deprecated', + input, + 'completed', + ); + return { success: true }; + }; + +export const retrieveLeadById: SalesforceEndpoints['retrieveLeadById'] = async ( + ctx, + input, +) => { + const response = await salesforceCall<{ Id: string }>( + ctx, + `sobjects/Lead/${input.id}`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.lead.retrieve_by_id', + input, + 'completed', + ); + return response; +}; + +/** @deprecated */ +export const retrieveLeadDataWithVariousResponses: SalesforceEndpoints['retrieveLeadDataWithVariousResponses'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + input.id ? `sobjects/Lead/${input.id}` : 'sobjects/Lead/describe', + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.lead.retrieve_various_deprecated', + input, + 'completed', + ); + return { records: [response] }; + }; + +export const updateLead: SalesforceEndpoints['updateLead'] = async ( + ctx, + input, +) => { + const { id, ...fields } = input; + const body = flattenFields(fields); + await salesforceCall(ctx, `sobjects/Lead/${id}`, { + method: 'PATCH', + body, + }); + await cacheEntity( + ctx.db?.lead, + SalesforceLeadEntity, + { + Id: id, + ...body, + }, + { label: LABEL }, + ); + await logEventFromContext(ctx, 'salesforce.lead.update', { id }, 'completed'); + return { success: true }; +}; + +export const updateLeadByIdWithJsonPayload: SalesforceEndpoints['updateLeadByIdWithJsonPayload'] = + updateLead as unknown as SalesforceEndpoints['updateLeadByIdWithJsonPayload']; + +export const searchLeads: SalesforceEndpoints['searchLeads'] = async ( + ctx, + input, +) => { + const terms: string[] = []; + if (input.name) terms.push(`Name LIKE '%${escapeSoql(input.name)}%'`); + if (input.email) terms.push(`Email LIKE '%${escapeSoql(input.email)}%'`); + if (input.phone) terms.push(`Phone LIKE '%${escapeSoql(input.phone)}%'`); + if (input.company) + terms.push(`Company LIKE '%${escapeSoql(input.company)}%'`); + if (input.status) terms.push(`Status = '${escapeSoql(input.status)}'`); + if (input.title) terms.push(`Title LIKE '%${escapeSoql(input.title)}%'`); + const whereStr = terms.length > 0 ? ` WHERE ${terms.join(' AND ')}` : ''; + const q = `SELECT Id, FirstName, LastName, Company, Email, Status, Phone FROM Lead${whereStr} LIMIT ${input.limit ?? 50}`; + const response = await salesforceCall<{ + records: Array>; + }>(ctx, 'query', { method: 'GET', query: { q } }); + await cacheEntities(ctx.db?.lead, SalesforceLeadEntity, response.records, { + label: LABEL, + }); + await logEventFromContext(ctx, 'salesforce.lead.search', input, 'completed'); + return { records: response.records ?? [] }; +}; diff --git a/packages/salesforce/endpoints/metadata.ts b/packages/salesforce/endpoints/metadata.ts new file mode 100644 index 000000000..31d22973e --- /dev/null +++ b/packages/salesforce/endpoints/metadata.ts @@ -0,0 +1,962 @@ +import { logEventFromContext } from 'corsair/core'; +import type { SalesforceEndpoints } from '..'; +import { + assertSobjectName, + cloneableFields, + createableNames, + escapeSoql, +} from '../utils'; +import { salesforceCall } from './shared'; + +export const createSObjectRecord: SalesforceEndpoints['createSObjectRecord'] = + async (ctx, input) => { + const response = await salesforceCall<{ + id: string; + success?: boolean; + }>(ctx, `sobjects/${input.sobject}`, { + method: 'POST', + body: input.fields, + }); + + await logEventFromContext( + ctx, + 'salesforce.metadata.create_sobject', + input, + 'completed', + ); + return response; + }; + +export const cloneRecord: SalesforceEndpoints['cloneRecord'] = async ( + ctx, + input, +) => { + const orig = await salesforceCall>( + ctx, + `sobjects/${input.sobject}/${input.recordId}`, + { method: 'GET' }, + ); + const describe = await salesforceCall<{ + fields?: Array<{ name?: string; createable?: boolean }>; + }>(ctx, `sobjects/${input.sobject}/describe`, { method: 'GET' }); + const allowed = createableNames(describe); + const body = { + ...cloneableFields(orig, allowed), + ...cloneableFields(input.overrides ?? {}, allowed), + }; + + const response = await salesforceCall<{ id: string }>( + ctx, + `sobjects/${input.sobject}`, + { method: 'POST', body }, + ); + + await logEventFromContext( + ctx, + 'salesforce.metadata.clone_record', + input, + 'completed', + ); + return { id: response.id }; +}; + +export const createCustomField: SalesforceEndpoints['createCustomField'] = + async (ctx, input) => { + const body = { + FullName: `${input.sobject}.${input.developerName}__c`, + Metadata: { + label: input.label, + type: input.type, + length: input.length, + }, + }; + + const response = await salesforceCall<{ + id: string; + success?: boolean; + }>(ctx, 'tooling/sobjects/CustomField', { method: 'POST', body }); + + await logEventFromContext( + ctx, + 'salesforce.metadata.create_custom_field', + input, + 'completed', + ); + return response; + }; + +export const createCustomObject: SalesforceEndpoints['createCustomObject'] = + async (ctx, input) => { + const body = { + FullName: `${input.developerName}__c`, + Metadata: { + label: input.label, + pluralLabel: input.pluralLabel, + nameField: { + type: 'Text', + label: `${input.label} Name`, + }, + deploymentStatus: 'Deployed', + sharingModel: 'ReadWrite', + }, + }; + + const response = await salesforceCall<{ + id: string; + success?: boolean; + }>(ctx, 'tooling/sobjects/CustomObject', { method: 'POST', body }); + + await logEventFromContext( + ctx, + 'salesforce.metadata.create_custom_object', + input, + 'completed', + ); + return response; + }; + +export const deleteSobject: SalesforceEndpoints['deleteSobject'] = async ( + ctx, + input, +) => { + await salesforceCall(ctx, `sobjects/${input.sobject}/${input.id}`, { + method: 'DELETE', + }); + + await logEventFromContext( + ctx, + 'salesforce.metadata.delete_sobject', + input, + 'completed', + ); + return { success: true }; +}; + +export const deleteSobjectRows: SalesforceEndpoints['deleteSobjectRows'] = + async (ctx, input) => { + await salesforceCall(ctx, `sobjects/${input.sobject}/${input.id}`, { + method: 'DELETE', + }); + + await logEventFromContext( + ctx, + 'salesforce.metadata.delete_sobject_rows', + input, + 'completed', + ); + return { success: true }; + }; + +export const getSobjects: SalesforceEndpoints['getSobjects'] = async ( + ctx, + _input, +) => { + const response = await salesforceCall<{ + encoding?: string; + maxBatchSize?: number; + sobjects: Array>; + }>(ctx, 'sobjects', { method: 'GET' }); + + await logEventFromContext( + ctx, + 'salesforce.metadata.get_sobjects', + {}, + 'completed', + ); + return response; +}; + +export const executeSobjectQuickAction: SalesforceEndpoints['executeSobjectQuickAction'] = + async (ctx, input) => { + const endpoint = input.contextId + ? `sobjects/${input.sobject}/quickActions/${input.actionName}/${input.contextId}` + : `sobjects/${input.sobject}/quickActions/${input.actionName}`; + + const response = await salesforceCall<{ + success: boolean; + recordId?: string; + }>(ctx, endpoint, { + method: 'POST', + body: input.record ?? {}, + }); + + await logEventFromContext( + ctx, + 'salesforce.metadata.execute_quick_action', + input, + 'completed', + ); + return response; + }; + +export const getApi: SalesforceEndpoints['getApi'] = async (ctx, input) => { + const endpoint = input.version ? `v${input.version}` : ''; + const response = await salesforceCall>( + ctx, + endpoint, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.metadata.get_api', + input, + 'completed', + ); + return response; +}; + +export const getChatterResources: SalesforceEndpoints['getChatterResources'] = + async (ctx, _input) => { + const response = await salesforceCall>( + ctx, + 'chatter', + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.metadata.chatter_resources', + {}, + 'completed', + ); + return response; + }; + +export const getSobjectPlatformaction: SalesforceEndpoints['getSobjectPlatformaction'] = + async (ctx, _input) => { + const response = await salesforceCall>( + ctx, + 'sobjects/PlatformAction/describe', + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.metadata.platformaction', + {}, + 'completed', + ); + return response; + }; + +export const headQuickActions: SalesforceEndpoints['headQuickActions'] = async ( + ctx, + _input, +) => { + await salesforceCall(ctx, 'quickActions', { method: 'HEAD' }); + + await logEventFromContext( + ctx, + 'salesforce.metadata.head_quick_actions', + {}, + 'completed', + ); + return { status: 200 }; +}; + +export const headSobjectsUserPassword: SalesforceEndpoints['headSobjectsUserPassword'] = + async (ctx, input) => { + await salesforceCall(ctx, `sobjects/User/${input.userId}/password`, { + method: 'HEAD', + }); + + await logEventFromContext( + ctx, + 'salesforce.metadata.head_user_password', + input, + 'completed', + ); + return { status: 200 }; + }; + +export const getPicklistValuesByRecordType: SalesforceEndpoints['getPicklistValuesByRecordType'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + `ui-api/object-info/${input.sobject}/picklist-values/${input.recordTypeId}`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.metadata.picklist_values', + input, + 'completed', + ); + return response; + }; + +export const getAllFieldsForObject: SalesforceEndpoints['getAllFieldsForObject'] = + async (ctx, input) => { + const response = await salesforceCall<{ + fields: Array>; + }>(ctx, `sobjects/${input.sobject}/describe`, { method: 'GET' }); + + await logEventFromContext( + ctx, + 'salesforce.metadata.all_fields', + input, + 'completed', + ); + return { fields: response.fields ?? [] }; + }; + +export const getAllCustomObjects: SalesforceEndpoints['getAllCustomObjects'] = + async (ctx, _input) => { + const response = await salesforceCall<{ + sobjects: Array>; + }>(ctx, 'sobjects', { method: 'GET' }); + + const customObjects = (response.sobjects ?? []).filter( + (obj) => obj.custom === true, + ); + + await logEventFromContext( + ctx, + 'salesforce.metadata.all_custom_objects', + {}, + 'completed', + ); + return { sobjects: customObjects }; + }; + +export const getSobjectsSobjectDescribeApprovallayouts: SalesforceEndpoints['getSobjectsSobjectDescribeApprovallayouts'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + `sobjects/${input.sobject}/describe/approvalLayouts`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.metadata.approval_layouts_describe', + input, + 'completed', + ); + return response; + }; + +export const getSobjectApprovalLayouts: SalesforceEndpoints['getSobjectApprovalLayouts'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + `sobjects/${input.sobject}/approvalLayouts`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.metadata.approval_layouts', + input, + 'completed', + ); + return response; + }; + +export const getChildRecords: SalesforceEndpoints['getChildRecords'] = async ( + ctx, + input, +) => { + const response = await salesforceCall<{ + records: Array>; + }>(ctx, `sobjects/Account/${input.parentId}/${input.relationshipName}`, { + method: 'GET', + }); + + await logEventFromContext( + ctx, + 'salesforce.metadata.child_records', + input, + 'completed', + ); + return { records: response.records ?? [] }; +}; + +export const getConsentAction: SalesforceEndpoints['getConsentAction'] = async ( + ctx, + input, +) => { + const response = await salesforceCall>( + ctx, + 'consent/action', + { + method: 'GET', + query: { + action: input.action, + ids: input.ids.join(','), + }, + }, + ); + + await logEventFromContext( + ctx, + 'salesforce.metadata.consent_action', + input, + 'completed', + ); + return response; +}; + +export const headActionsCustom: SalesforceEndpoints['headActionsCustom'] = + async (ctx, _input) => { + await salesforceCall(ctx, 'actions/custom', { + method: 'HEAD', + }); + + await logEventFromContext( + ctx, + 'salesforce.metadata.head_custom_actions', + {}, + 'completed', + ); + return { status: 200 }; + }; + +export const listCustomInvocableActions: SalesforceEndpoints['listCustomInvocableActions'] = + async (ctx, _input) => { + const response = await salesforceCall<{ + actions: Array>; + }>(ctx, 'actions/custom', { method: 'GET' }); + + await logEventFromContext( + ctx, + 'salesforce.metadata.custom_invocable_actions', + {}, + 'completed', + ); + return { actions: response.actions ?? [] }; + }; + +export const getSupportedObjectsDirectory: SalesforceEndpoints['getSupportedObjectsDirectory'] = + async (ctx, _input) => { + const response = await salesforceCall>( + ctx, + 'ui-api/object-info', + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.metadata.supported_objects_dir', + {}, + 'completed', + ); + return response; + }; + +export const getGlobalActions: SalesforceEndpoints['getGlobalActions'] = async ( + ctx, + _input, +) => { + const response = await salesforceCall<{ + actions: Array>; + }>(ctx, 'quickActions', { method: 'GET' }); + + await logEventFromContext( + ctx, + 'salesforce.metadata.global_actions', + {}, + 'completed', + ); + return { actions: response.actions ?? [] }; +}; + +export const headSobjectsGlobalDescribeLayouts: SalesforceEndpoints['headSobjectsGlobalDescribeLayouts'] = + async (ctx, _input) => { + await salesforceCall(ctx, 'sobjects/Global/describe/layouts', { + method: 'HEAD', + }); + + await logEventFromContext( + ctx, + 'salesforce.metadata.head_global_describe_layouts', + {}, + 'completed', + ); + return { status: 200 }; + }; + +export const getSObjectsDescribeLayoutsRecordTypeId: SalesforceEndpoints['getSObjectsDescribeLayoutsRecordTypeId'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + `sobjects/${input.sobject}/describe/layouts/${input.recordTypeId}`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.metadata.layouts_record_type_id', + input, + 'completed', + ); + return response; + }; + +export const getOrgLimits: SalesforceEndpoints['getOrgLimits'] = async ( + ctx, + _input, +) => { + const response = await salesforceCall>( + ctx, + 'limits', + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.metadata.org_limits', + {}, + 'completed', + ); + return response; +}; + +export const headProcessRulesSObject: SalesforceEndpoints['headProcessRulesSObject'] = + async (ctx, input) => { + await salesforceCall(ctx, `process/rules/${input.sobject}`, { + method: 'HEAD', + }); + + await logEventFromContext( + ctx, + 'salesforce.metadata.head_process_rules', + input, + 'completed', + ); + return { status: 200 }; + }; + +export const headSobjectQuickActionDefaultValues: SalesforceEndpoints['headSobjectQuickActionDefaultValues'] = + async (ctx, input) => { + const endpoint = input.contextId + ? `sobjects/${input.sobject}/quickActions/${input.actionName}/defaultValues/${input.contextId}` + : `sobjects/${input.sobject}/quickActions/${input.actionName}/defaultValues`; + + await salesforceCall(ctx, endpoint, { method: 'HEAD' }); + + await logEventFromContext( + ctx, + 'salesforce.metadata.head_quick_action_defaults', + input, + 'completed', + ); + return { status: 200 }; + }; + +export const getQuickActions: SalesforceEndpoints['getQuickActions'] = async ( + ctx, + _input, +) => { + const response = await salesforceCall<{ + actions: Array>; + }>(ctx, 'quickActions', { method: 'GET' }); + + await logEventFromContext( + ctx, + 'salesforce.metadata.get_quick_actions', + {}, + 'completed', + ); + return { actions: response.actions ?? [] }; +}; + +export const getRecordCounts: SalesforceEndpoints['getRecordCounts'] = async ( + ctx, + input, +) => { + const response = await salesforceCall<{ + sObjects: Array>; + }>(ctx, 'limits/recordCount', { + method: 'GET', + query: { sObjects: input.sobjects.join(',') }, + }); + + await logEventFromContext( + ctx, + 'salesforce.metadata.record_counts', + input, + 'completed', + ); + return response; +}; + +export const getSobjectRelationship: SalesforceEndpoints['getSobjectRelationship'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + `sobjects/${input.sobject}/${input.id}/${input.fieldName}`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.metadata.sobject_relationship', + input, + 'completed', + ); + return response; + }; + +export const getSobjectQuickActionDefaultValues: SalesforceEndpoints['getSobjectQuickActionDefaultValues'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + `sobjects/${input.sobject}/quickActions/${input.actionName}/defaultValues`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.metadata.quick_action_default_values', + input, + 'completed', + ); + return response; + }; + +export const getSObjectQuickActionDefaultValues: SalesforceEndpoints['getSObjectQuickActionDefaultValues'] = + async (ctx, input) => { + const endpoint = input.contextId + ? `sobjects/${input.sobject}/quickActions/${input.actionName}/defaultValues/${input.contextId}` + : `sobjects/${input.sobject}/quickActions/${input.actionName}/defaultValues`; + + const response = await salesforceCall>( + ctx, + endpoint, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.metadata.quick_action_default_values_context', + input, + 'completed', + ); + return response; + }; + +export const getSobjectByExternalId: SalesforceEndpoints['getSobjectByExternalId'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + `sobjects/${input.sobject}/${input.fieldName}/${encodeURIComponent(input.fieldValue)}`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.metadata.sobject_external_id', + input, + 'completed', + ); + return response; + }; + +export const headSobjectsQuickAction: SalesforceEndpoints['headSobjectsQuickAction'] = + async (ctx, input) => { + await salesforceCall( + ctx, + `sobjects/${input.sobject}/quickActions/${input.actionName}`, + { method: 'HEAD' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.metadata.head_sobject_quick_action', + input, + 'completed', + ); + return { status: 200 }; + }; + +export const getSObjectRecord: SalesforceEndpoints['getSObjectRecord'] = async ( + ctx, + input, +) => { + const response = await salesforceCall>( + ctx, + `sobjects/${input.sobject}/${input.id}`, + { + method: 'GET', + query: input.fields ? { fields: input.fields.join(',') } : undefined, + }, + ); + + await logEventFromContext( + ctx, + 'salesforce.metadata.get_sobject_record', + input, + 'completed', + ); + return response; +}; + +export const headActionsStandard: SalesforceEndpoints['headActionsStandard'] = + async (ctx, _input) => { + await salesforceCall(ctx, 'actions/standard', { + method: 'HEAD', + }); + + await logEventFromContext( + ctx, + 'salesforce.metadata.head_standard_actions', + {}, + 'completed', + ); + return { status: 200 }; + }; + +export const listStandardInvocableActions: SalesforceEndpoints['listStandardInvocableActions'] = + async (ctx, _input) => { + const response = await salesforceCall<{ + actions: Array>; + }>(ctx, 'actions/standard', { method: 'GET' }); + + await logEventFromContext( + ctx, + 'salesforce.metadata.standard_invocable_actions', + {}, + 'completed', + ); + return { actions: response.actions ?? [] }; + }; + +export const getSupport: SalesforceEndpoints['getSupport'] = async ( + ctx, + _input, +) => { + const response = await salesforceCall>( + ctx, + 'support/data', + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.metadata.support', + {}, + 'completed', + ); + return response; +}; + +export const getSupportKnowledgeArticles: SalesforceEndpoints['getSupportKnowledgeArticles'] = + async (ctx, _input) => { + const response = await salesforceCall>( + ctx, + 'support/knowledgeArticles', + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.metadata.support_articles', + {}, + 'completed', + ); + return response; + }; + +export const getTheme: SalesforceEndpoints['getTheme'] = async ( + ctx, + _input, +) => { + const response = await salesforceCall>(ctx, 'theme', { + method: 'GET', + }); + + await logEventFromContext(ctx, 'salesforce.metadata.theme', {}, 'completed'); + return response; +}; + +export const getSObjectsUpdated: SalesforceEndpoints['getSObjectsUpdated'] = + async (ctx, input) => { + const response = await salesforceCall<{ + ids: string[]; + latestDateCovered: string; + }>(ctx, `sobjects/${input.sobject}/updated`, { + method: 'GET', + query: { + start: input.start, + end: input.end, + }, + }); + + await logEventFromContext( + ctx, + 'salesforce.metadata.sobjects_updated', + input, + 'completed', + ); + return response; + }; + +export const getUserInfo: SalesforceEndpoints['getUserInfo'] = async ( + ctx, + input, +) => { + const endpoint = input.userId + ? `sobjects/User/${input.userId}` + : 'chatter/users/me'; + const response = await salesforceCall>( + ctx, + endpoint, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.metadata.user_info', + input, + 'completed', + ); + return response; +}; + +export const sobjectUserPassword: SalesforceEndpoints['sobjectUserPassword'] = + async (ctx, input) => { + const response = await salesforceCall<{ isExpired?: boolean }>( + ctx, + `sobjects/User/${input.userId}/password`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.metadata.user_password_expiration', + input, + 'completed', + ); + return response; + }; + +export const massTransferOwnership: SalesforceEndpoints['massTransferOwnership'] = + async (ctx, input) => { + const sobject = assertSobjectName(input.sobject); + const recordIds = input.recordIds ? [...input.recordIds] : []; + if (recordIds.length === 0) { + const safeFromUserId = escapeSoql(input.fromUserId); + let endpoint = 'query'; + let query: Record | undefined = { + q: `SELECT Id FROM ${sobject} WHERE OwnerId = '${safeFromUserId}' LIMIT 200`, + }; + while (true) { + const queryRes = await salesforceCall<{ + records?: Array<{ Id: string }>; + done?: boolean; + nextRecordsUrl?: string; + }>(ctx, endpoint, { method: 'GET', query }); + for (const r of queryRes.records ?? []) { + recordIds.push(r.Id); + } + if (queryRes.done !== false || !queryRes.nextRecordsUrl) break; + endpoint = queryRes.nextRecordsUrl; + query = undefined; + } + } + + const failed: Array<{ id?: string; errors?: unknown }> = []; + let transferred = 0; + for (let i = 0; i < recordIds.length; i += 200) { + const chunk = recordIds.slice(i, i + 200); + const result = await salesforceCall(ctx, 'composite/sobjects', { + method: 'PATCH', + body: { + records: chunk.map((id) => ({ + attributes: { type: sobject }, + Id: id, + OwnerId: input.toUserId, + })), + }, + }); + const rows = Array.isArray(result) + ? result + : Array.isArray((result as { results?: unknown }).results) + ? (result as { results: unknown[] }).results + : []; + if (rows.length === 0) { + transferred += chunk.length; + continue; + } + for (let j = 0; j < chunk.length; j++) { + const row = rows[j] as + | { success?: boolean; id?: string; errors?: unknown } + | undefined; + if (row && row.success === false) { + failed.push({ id: row.id ?? chunk[j], errors: row.errors }); + } else { + transferred += 1; + } + } + } + + await logEventFromContext( + ctx, + 'salesforce.metadata.mass_transfer_ownership', + input, + 'completed', + ); + return { success: failed.length === 0, transferred, failed }; + }; + +export const updateSobject: SalesforceEndpoints['updateSobject'] = async ( + ctx, + input, +) => { + await salesforceCall(ctx, `sobjects/${input.sobject}/${input.id}`, { + method: 'PATCH', + body: input.fields, + }); + await logEventFromContext( + ctx, + 'salesforce.metadata.update_sobject', + input, + 'completed', + ); + return { success: true }; +}; + +export const sobjectRowsUpdate: SalesforceEndpoints['sobjectRowsUpdate'] = + updateSobject as unknown as SalesforceEndpoints['sobjectRowsUpdate']; + +export const upsertSobjectByExternalId: SalesforceEndpoints['upsertSobjectByExternalId'] = + async (ctx, input) => { + const response = await salesforceCall<{ + id?: string; + created?: boolean; + success?: boolean; + }>( + ctx, + `sobjects/${input.sobject}/${input.fieldName}/${encodeURIComponent(input.fieldValue)}`, + { method: 'PATCH', body: input.fields }, + ); + await logEventFromContext( + ctx, + 'salesforce.metadata.upsert_by_external_id', + input, + 'completed', + ); + return response; + }; + +export const setUserPassword: SalesforceEndpoints['setUserPassword'] = async ( + ctx, + input, +) => { + const body = input.password ? { NewPassword: input.password } : {}; + const response = await salesforceCall( + ctx, + `sobjects/User/${input.userId}/password`, + { method: 'POST', body }, + ); + await logEventFromContext( + ctx, + 'salesforce.metadata.set_user_password', + { userId: input.userId }, + 'completed', + ); + return { result: response }; +}; diff --git a/packages/salesforce/endpoints/notes.ts b/packages/salesforce/endpoints/notes.ts new file mode 100644 index 000000000..f07588b10 --- /dev/null +++ b/packages/salesforce/endpoints/notes.ts @@ -0,0 +1,170 @@ +import { logEventFromContext } from 'corsair/core'; +import type { SalesforceEndpoints } from '..'; +import { escapeSoql, soqlWhere } from '../utils'; +import { flattenFields, salesforceCall } from './shared'; + +export const createNote: SalesforceEndpoints['createNote'] = async ( + ctx, + input, +) => { + const response = await salesforceCall<{ + id: string; + success?: boolean; + }>(ctx, 'sobjects/Note', { method: 'POST', body: flattenFields(input) }); + + await logEventFromContext(ctx, 'salesforce.note.create', input, 'completed'); + return response; +}; + +export const getNote: SalesforceEndpoints['getNote'] = async (ctx, input) => { + const response = await salesforceCall<{ + Id: string; + Title?: string; + Body?: string; + }>(ctx, `sobjects/Note/${input.id}`, { method: 'GET' }); + + await logEventFromContext(ctx, 'salesforce.note.get', input, 'completed'); + return response; +}; + +export const listNotes: SalesforceEndpoints['listNotes'] = async ( + ctx, + input, +) => { + const limit = input.limit ?? 200; + const conditions: string[] = []; + if (input.parentId) + conditions.push(`ParentId = '${escapeSoql(input.parentId)}'`); + const queryClause = soqlWhere(input.query); + if (queryClause) conditions.push(queryClause); + + const whereStr = + conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : ''; + const q = `SELECT Id, Title, Body, ParentId, CreatedDate FROM Note${whereStr} LIMIT ${limit}`; + + const response = await salesforceCall<{ + records: Array>; + }>(ctx, 'query', { method: 'GET', query: { q } }); + + await logEventFromContext(ctx, 'salesforce.note.list', input, 'completed'); + return { records: response.records ?? [] }; +}; + +export const deleteNote: SalesforceEndpoints['deleteNote'] = async ( + ctx, + input, +) => { + await salesforceCall(ctx, `sobjects/Note/${input.id}`, { + method: 'DELETE', + }); + + await logEventFromContext(ctx, 'salesforce.note.delete', input, 'completed'); + return { success: true }; +}; + +/** @deprecated */ +export const createNoteRecordWithContentTypeHeader: SalesforceEndpoints['createNoteRecordWithContentTypeHeader'] = + async (ctx, input) => { + const response = await salesforceCall<{ id: string }>( + ctx, + 'sobjects/Note', + { method: 'POST', body: input }, + ); + + await logEventFromContext( + ctx, + 'salesforce.note.create_record_deprecated', + input, + 'completed', + ); + return response; + }; + +/** @deprecated */ +export const removeNoteObjectById: SalesforceEndpoints['removeNoteObjectById'] = + async (ctx, input) => { + await salesforceCall(ctx, `sobjects/Note/${input.id}`, { + method: 'DELETE', + }); + + await logEventFromContext( + ctx, + 'salesforce.note.remove_deprecated', + input, + 'completed', + ); + return { success: true }; + }; + +/** @deprecated */ +export const getNoteByIdWithFields: SalesforceEndpoints['getNoteByIdWithFields'] = + async (ctx, input) => { + const response = await salesforceCall<{ Id: string }>( + ctx, + `sobjects/Note/${input.id}`, + { + method: 'GET', + query: input.fields ? { fields: input.fields.join(',') } : undefined, + }, + ); + + await logEventFromContext( + ctx, + 'salesforce.note.get_by_id_with_fields_deprecated', + input, + 'completed', + ); + return response; + }; + +/** @deprecated */ +export const retrieveNoteObjectInformation: SalesforceEndpoints['retrieveNoteObjectInformation'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + input.id ? `sobjects/Note/${input.id}` : 'sobjects/Note/describe', + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.note.retrieve_info_deprecated', + input, + 'completed', + ); + return { metadata: response }; + }; + +export const updateNote: SalesforceEndpoints['updateNote'] = async ( + ctx, + input, +) => { + const { id, ...fields } = input; + const body = flattenFields(fields); + await salesforceCall(ctx, `sobjects/Note/${id}`, { + method: 'PATCH', + body, + }); + await logEventFromContext(ctx, 'salesforce.note.update', { id }, 'completed'); + return { success: true }; +}; + +export const updateSpecificNoteById: SalesforceEndpoints['updateSpecificNoteById'] = + updateNote as unknown as SalesforceEndpoints['updateSpecificNoteById']; + +export const searchNotes: SalesforceEndpoints['searchNotes'] = async ( + ctx, + input, +) => { + const terms: string[] = []; + if (input.title) terms.push(`Title LIKE '%${escapeSoql(input.title)}%'`); + if (input.body) terms.push(`Body LIKE '%${escapeSoql(input.body)}%'`); + if (input.parentId) terms.push(`ParentId = '${escapeSoql(input.parentId)}'`); + const whereStr = terms.length > 0 ? ` WHERE ${terms.join(' AND ')}` : ''; + const q = `SELECT Id, Title, Body, ParentId, OwnerId FROM Note${whereStr} LIMIT ${input.limit ?? 50}`; + const response = await salesforceCall<{ + records: Array>; + }>(ctx, 'query', { method: 'GET', query: { q } }); + await logEventFromContext(ctx, 'salesforce.note.search', input, 'completed'); + return { records: response.records ?? [] }; +}; diff --git a/packages/salesforce/endpoints/opportunities.ts b/packages/salesforce/endpoints/opportunities.ts new file mode 100644 index 000000000..89442ff1c --- /dev/null +++ b/packages/salesforce/endpoints/opportunities.ts @@ -0,0 +1,369 @@ +import { logEventFromContext } from 'corsair/core'; +import type { SalesforceEndpoints } from '..'; +import { SalesforceOpportunityEntity } from '../schema/database'; +import { + cloneableFields, + createableNames, + escapeSoql, + soqlWhere, +} from '../utils'; +import { cacheEntities, cacheEntity, evictEntity } from './persist'; +import { flattenFields, salesforceCall } from './shared'; + +const LABEL = 'opportunity'; + +export const createOpportunity: SalesforceEndpoints['createOpportunity'] = + async (ctx, input) => { + const body = flattenFields(input); + + const response = await salesforceCall<{ + id: string; + success?: boolean; + }>(ctx, 'sobjects/Opportunity', { method: 'POST', body }); + + await cacheEntity( + ctx.db?.opportunity, + SalesforceOpportunityEntity, + { + Id: response.id, + ...body, + }, + { label: LABEL }, + ); + + await logEventFromContext( + ctx, + 'salesforce.opportunity.create', + input, + 'completed', + ); + return response; + }; + +export const getOpportunity: SalesforceEndpoints['getOpportunity'] = async ( + ctx, + input, +) => { + const response = await salesforceCall<{ Id: string }>( + ctx, + `sobjects/Opportunity/${input.id}`, + { method: 'GET' }, + ); + + await cacheEntity( + ctx.db?.opportunity, + SalesforceOpportunityEntity, + response, + { + label: LABEL, + }, + ); + + await logEventFromContext( + ctx, + 'salesforce.opportunity.get', + input, + 'completed', + ); + return response; +}; + +export const listOpportunities: SalesforceEndpoints['listOpportunities'] = + async (ctx, input) => { + const limit = input.limit ?? 200; + const offsetStr = input.offset ? ` OFFSET ${input.offset}` : ''; + const queryClause = soqlWhere(input.query); + const whereStr = queryClause ? ` WHERE ${queryClause}` : ''; + const q = `SELECT Id, Name, StageName, CloseDate, Amount, AccountId FROM Opportunity${whereStr} LIMIT ${limit}${offsetStr}`; + + const response = await salesforceCall<{ + totalSize: number; + done: boolean; + records: Array>; + nextRecordsUrl?: string; + }>(ctx, 'query', { method: 'GET', query: { q } }); + + await cacheEntities( + ctx.db?.opportunity, + SalesforceOpportunityEntity, + response.records, + { label: LABEL }, + ); + + await logEventFromContext( + ctx, + 'salesforce.opportunity.list', + input, + 'completed', + ); + return response; + }; + +export const deleteOpportunity: SalesforceEndpoints['deleteOpportunity'] = + async (ctx, input) => { + await salesforceCall(ctx, `sobjects/Opportunity/${input.id}`, { + method: 'DELETE', + }); + + await evictEntity(ctx.db?.opportunity, input.id, LABEL); + + await logEventFromContext( + ctx, + 'salesforce.opportunity.delete', + input, + 'completed', + ); + return { success: true }; + }; + +export const addOpportunityLineItem: SalesforceEndpoints['addOpportunityLineItem'] = + async (ctx, input) => { + const response = await salesforceCall<{ id: string }>( + ctx, + 'sobjects/OpportunityLineItem', + { + method: 'POST', + body: input, + }, + ); + + await logEventFromContext( + ctx, + 'salesforce.opportunity.add_line_item', + input, + 'completed', + ); + return response; + }; + +export const cloneOpportunityWithProducts: SalesforceEndpoints['cloneOpportunityWithProducts'] = + async (ctx, input) => { + const orig = await salesforceCall>( + ctx, + `sobjects/Opportunity/${input.opportunityId}`, + { method: 'GET' }, + ); + const describe = await salesforceCall<{ + fields?: Array<{ name?: string; createable?: boolean }>; + }>(ctx, 'sobjects/Opportunity/describe', { method: 'GET' }); + const fieldsToClone = cloneableFields(orig, createableNames(describe)); + + if (input.name) fieldsToClone.Name = input.name; + + const created = await salesforceCall<{ id: string }>( + ctx, + 'sobjects/Opportunity', + { method: 'POST', body: fieldsToClone }, + ); + + if (input.cloneProducts) { + const lineItemsRes = await salesforceCall<{ + records: Array>; + }>(ctx, 'query', { + method: 'GET', + query: { + q: `SELECT PricebookEntryId, Quantity, UnitPrice FROM OpportunityLineItem WHERE OpportunityId = '${escapeSoql(input.opportunityId)}'`, + }, + }); + + for (const item of lineItemsRes.records ?? []) { + await salesforceCall(ctx, 'sobjects/OpportunityLineItem', { + method: 'POST', + body: { + OpportunityId: created.id, + PricebookEntryId: item.PricebookEntryId, + Quantity: item.Quantity, + UnitPrice: item.UnitPrice, + }, + }); + } + } + + await logEventFromContext( + ctx, + 'salesforce.opportunity.clone', + input, + 'completed', + ); + return { id: created.id }; + }; + +export const listPricebookEntries: SalesforceEndpoints['listPricebookEntries'] = + async (ctx, input) => { + const limit = input.limit ?? 200; + const conditions = ['IsActive = true']; + if (input.pricebookId) + conditions.push(`Pricebook2Id = '${escapeSoql(input.pricebookId)}'`); + const queryClause = soqlWhere(input.query); + if (queryClause) conditions.push(queryClause); + + const whereStr = ` WHERE ${conditions.join(' AND ')}`; + const q = `SELECT Id, Name, Pricebook2Id, Product2Id, UnitPrice, IsActive FROM PricebookEntry${whereStr} LIMIT ${limit}`; + + const response = await salesforceCall<{ + records: Array>; + }>(ctx, 'query', { method: 'GET', query: { q } }); + + await logEventFromContext( + ctx, + 'salesforce.pricebook_entries.list', + input, + 'completed', + ); + return { records: response.records ?? [] }; + }; + +export const listPricebooks: SalesforceEndpoints['listPricebooks'] = async ( + ctx, + input, +) => { + const limit = input.limit ?? 200; + const queryClause = soqlWhere(input.query); + const whereStr = queryClause ? ` WHERE ${queryClause}` : ''; + const q = `SELECT Id, Name, IsActive, IsStandard FROM Pricebook2${whereStr} LIMIT ${limit}`; + + const response = await salesforceCall<{ + records: Array>; + }>(ctx, 'query', { method: 'GET', query: { q } }); + + await logEventFromContext( + ctx, + 'salesforce.pricebooks.list', + input, + 'completed', + ); + return { records: response.records ?? [] }; +}; + +/** @deprecated */ +export const createOpportunityRecord: SalesforceEndpoints['createOpportunityRecord'] = + async (ctx, input) => { + const response = await salesforceCall<{ id: string }>( + ctx, + 'sobjects/Opportunity', + { method: 'POST', body: input }, + ); + + await logEventFromContext( + ctx, + 'salesforce.opportunity.create_deprecated', + input, + 'completed', + ); + return response; + }; + +/** @deprecated */ +export const removeOpportunityById: SalesforceEndpoints['removeOpportunityById'] = + async (ctx, input) => { + await salesforceCall(ctx, `sobjects/Opportunity/${input.id}`, { + method: 'DELETE', + }); + + await logEventFromContext( + ctx, + 'salesforce.opportunity.remove_deprecated', + input, + 'completed', + ); + return { success: true }; + }; + +export const retrieveOpportunitiesData: SalesforceEndpoints['retrieveOpportunitiesData'] = + async (ctx, input) => { + const queryClause = soqlWhere(input.query); + const whereStr = queryClause ? ` WHERE ${queryClause}` : ''; + const q = `SELECT Id, Name, StageName, Amount FROM Opportunity${whereStr} LIMIT ${input.limit ?? 200}`; + + const response = await salesforceCall<{ + records: Array>; + }>(ctx, 'query', { method: 'GET', query: { q } }); + + await logEventFromContext( + ctx, + 'salesforce.opportunity.retrieve_data', + input, + 'completed', + ); + return { records: response.records ?? [] }; + }; + +/** @deprecated */ +export const retrieveOpportunityByIdWithOptionalFields: SalesforceEndpoints['retrieveOpportunityByIdWithOptionalFields'] = + async (ctx, input) => { + const response = await salesforceCall<{ Id: string }>( + ctx, + `sobjects/Opportunity/${input.id}`, + { + method: 'GET', + query: input.fields ? { fields: input.fields.join(',') } : undefined, + }, + ); + + await logEventFromContext( + ctx, + 'salesforce.opportunity.retrieve_by_id_deprecated', + input, + 'completed', + ); + return response; + }; + +export const updateOpportunity: SalesforceEndpoints['updateOpportunity'] = + async (ctx, input) => { + const { id, ...fields } = input; + const body = flattenFields(fields); + await salesforceCall(ctx, `sobjects/Opportunity/${id}`, { + method: 'PATCH', + body, + }); + await cacheEntity( + ctx.db?.opportunity, + SalesforceOpportunityEntity, + { + Id: id, + ...body, + }, + { label: LABEL }, + ); + await logEventFromContext( + ctx, + 'salesforce.opportunity.update', + { id }, + 'completed', + ); + return { success: true }; + }; + +export const updateOpportunityById: SalesforceEndpoints['updateOpportunityById'] = + updateOpportunity as unknown as SalesforceEndpoints['updateOpportunityById']; + +export const searchOpportunities: SalesforceEndpoints['searchOpportunities'] = + async (ctx, input) => { + const terms: string[] = []; + if (input.name) terms.push(`Name LIKE '%${escapeSoql(input.name)}%'`); + if (input.accountId) + terms.push(`AccountId = '${escapeSoql(input.accountId)}'`); + if (input.stageName) + terms.push(`StageName = '${escapeSoql(input.stageName)}'`); + if (input.isClosed !== undefined) + terms.push(`IsClosed = ${input.isClosed}`); + const whereStr = terms.length > 0 ? ` WHERE ${terms.join(' AND ')}` : ''; + const q = `SELECT Id, Name, StageName, CloseDate, Amount, AccountId, IsClosed FROM Opportunity${whereStr} LIMIT ${input.limit ?? 50}`; + const response = await salesforceCall<{ + records: Array>; + }>(ctx, 'query', { method: 'GET', query: { q } }); + await cacheEntities( + ctx.db?.opportunity, + SalesforceOpportunityEntity, + response.records, + { label: LABEL }, + ); + await logEventFromContext( + ctx, + 'salesforce.opportunity.search', + input, + 'completed', + ); + return { records: response.records ?? [] }; + }; diff --git a/packages/salesforce/endpoints/persist.ts b/packages/salesforce/endpoints/persist.ts new file mode 100644 index 000000000..8cf458938 --- /dev/null +++ b/packages/salesforce/endpoints/persist.ts @@ -0,0 +1,101 @@ +import type { z } from 'zod'; + +type EntityStore = { + upsertByEntityId: (entityId: string, data: T) => Promise; +}; + +type EntityEvictor = { + deleteByEntityId: (entityId: string) => Promise; +}; + +function asStore(store: unknown): EntityStore | undefined { + if (!store || typeof store !== 'object') return undefined; + const upsert = (store as EntityStore).upsertByEntityId; + return typeof upsert === 'function' ? (store as EntityStore) : undefined; +} + +function asEvictor(store: unknown): EntityEvictor | undefined { + if (!store || typeof store !== 'object') return undefined; + const del = (store as EntityEvictor).deleteByEntityId; + return typeof del === 'function' ? (store as EntityEvictor) : undefined; +} + +async function safely(operation: () => Promise, what: string) { + try { + await operation(); + } catch (error) { + console.warn(`[SALESFORCE] ${what}:`, error); + } +} + +const CACHE_WRITE_CONCURRENCY = 16; + +type EntityIdOf = (parsed: T) => string | undefined; + +const defaultEntityId = (parsed: T): string | undefined => { + const id = + (parsed as { Id?: unknown; id?: unknown }).Id ?? + (parsed as { id?: unknown }).id; + return typeof id === 'string' && id.length > 0 ? id : undefined; +}; + +/** + * Mirrors one Salesforce record into the local cache after schema validation. + */ +export async function cacheEntity( + store: unknown, + schema: Schema, + record: unknown, + options: { label: string; entityId?: EntityIdOf> }, +): Promise { + if (record == null) return; + const entityStore = asStore>(store); + if (!entityStore) return; + + const parsed = schema.safeParse(record); + if (!parsed.success) { + console.warn( + `[SALESFORCE] skipped caching a ${options.label} that does not match its schema:`, + parsed.error.issues, + ); + return; + } + + const entityId = (options.entityId ?? defaultEntityId)(parsed.data); + if (!entityId) return; + + await safely( + () => entityStore.upsertByEntityId(entityId, parsed.data), + `failed to cache ${options.label} ${entityId}`, + ); +} + +export async function evictEntity( + store: unknown, + entityId: string | undefined | null, + label: string, +): Promise { + const entityStore = asEvictor(store); + if (!entityStore || entityId == null) return; + + await safely( + () => entityStore.deleteByEntityId(entityId), + `failed to evict ${label} ${entityId}`, + ); +} + +export async function cacheEntities( + store: unknown, + schema: Schema, + records: readonly unknown[] | undefined | null, + options: { label: string; entityId?: EntityIdOf> }, +): Promise { + if (!records || records.length === 0) return; + + for (let i = 0; i < records.length; i += CACHE_WRITE_CONCURRENCY) { + const batch = records.slice(i, i + CACHE_WRITE_CONCURRENCY); + await Promise.all( + batch.map((record) => cacheEntity(store, schema, record, options)), + ); + } +} diff --git a/packages/salesforce/endpoints/shared.ts b/packages/salesforce/endpoints/shared.ts new file mode 100644 index 000000000..eae38535b --- /dev/null +++ b/packages/salesforce/endpoints/shared.ts @@ -0,0 +1,98 @@ +import type { SalesforceRequestOptions } from '../client'; +import { + discoverSalesforceInstanceUrl, + makeSalesforceRequest, +} from '../client'; +import { soqlWhere } from '../utils'; + +/** + * Minimal structural view of the plugin context the endpoints need. + */ +export type SalesforceCallContext = { + key: string; + options: { instanceUrl?: string | undefined; loginUrl?: string | undefined }; + keys?: unknown; + db?: unknown; +}; + +/** + * Resolves the org instance URL for a call. + * + * Salesforce REST must be called on the org host from the OAuth token + * (`instance_url`), not login.salesforce.com. + * Docs: https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_web_server_flow.htm + */ +export async function resolveInstanceUrl( + ctx: SalesforceCallContext, +): Promise { + if (ctx.options.instanceUrl) return ctx.options.instanceUrl; + + const stored = await ( + ctx.keys as { get_instance_url?: () => Promise } + )?.get_instance_url?.(); + if (stored) { + ctx.options.instanceUrl = stored; + return stored; + } + + const fromEnv = process.env.SALESFORCE_INSTANCE_URL; + if (fromEnv) { + ctx.options.instanceUrl = fromEnv; + return fromEnv; + } + + const discovered = await discoverSalesforceInstanceUrl( + ctx.key, + ctx.options.loginUrl, + ); + ctx.options.instanceUrl = discovered; + return discovered; +} + +export async function salesforceCall( + ctx: SalesforceCallContext, + endpoint: string, + options: Omit = {}, +): Promise { + const instanceUrl = await resolveInstanceUrl(ctx); + return await makeSalesforceRequest(endpoint, ctx.key, { + ...options, + instanceUrl, + }); +} + +/** + * Salesforce create/update bodies are a flat map of API field names. A + * `CustomFields` bag is a plugin convenience and must be spread, not nested. + */ +export function flattenFields(input: object): Record { + const { CustomFields, ...rest } = input as { + CustomFields?: Record; + } & Record; + const body: Record = {}; + for (const [key, value] of Object.entries(rest)) { + if (value !== undefined) body[key] = value; + } + if (CustomFields) { + for (const [key, value] of Object.entries(CustomFields)) { + if (value !== undefined) body[key] = value; + } + } + return body; +} + +export function soqlList( + sobject: string, + fields: string[], + input: { query?: string; limit?: number; offset?: number }, + extraWhere?: string[], +): string { + const limit = input.limit ?? 200; + const offsetStr = input.offset ? ` OFFSET ${input.offset}` : ''; + const conditions = [...(extraWhere ?? [])]; + const queryClause = soqlWhere(input.query); + if (queryClause) conditions.push(queryClause); + const whereStr = + conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : ''; + return `SELECT ${fields.join(', ')} FROM ${sobject}${whereStr} LIMIT ${limit}${offsetStr}`; +} diff --git a/packages/salesforce/endpoints/soql-sosl.ts b/packages/salesforce/endpoints/soql-sosl.ts new file mode 100644 index 000000000..0b0740022 --- /dev/null +++ b/packages/salesforce/endpoints/soql-sosl.ts @@ -0,0 +1,218 @@ +import { logEventFromContext } from 'corsair/core'; +import type { SalesforceEndpoints } from '..'; +import { salesforceCall } from './shared'; + +export const runSoqlQuery: SalesforceEndpoints['runSoqlQuery'] = async ( + ctx, + input, +) => { + const response = await salesforceCall<{ + totalSize: number; + done: boolean; + records: Array>; + nextRecordsUrl?: string; + }>(ctx, 'query', { method: 'GET', query: { q: input.q } }); + + await logEventFromContext( + ctx, + 'salesforce.soql.run_query', + input, + 'completed', + ); + return response; +}; + +export const queryAll: SalesforceEndpoints['queryAll'] = async (ctx, input) => { + const response = await salesforceCall<{ + totalSize: number; + done: boolean; + records: Array>; + }>(ctx, 'queryAll', { method: 'GET', query: { q: input.q } }); + + await logEventFromContext( + ctx, + 'salesforce.soql.query_all', + input, + 'completed', + ); + return response; +}; + +export const search: SalesforceEndpoints['search'] = async (ctx, input) => { + const response = await salesforceCall<{ + searchRecords: Array>; + }>(ctx, 'search', { method: 'GET', query: { q: input.q } }); + + await logEventFromContext(ctx, 'salesforce.sosl.search', input, 'completed'); + return { searchRecords: response.searchRecords ?? [] }; +}; + +export const executeSoslSearch: SalesforceEndpoints['executeSoslSearch'] = + async (ctx, input) => { + const response = await salesforceCall<{ + searchRecords: Array>; + }>(ctx, 'search', { method: 'GET', query: { q: input.q } }); + + await logEventFromContext( + ctx, + 'salesforce.sosl.execute_search', + input, + 'completed', + ); + return { searchRecords: response.searchRecords ?? [] }; + }; + +export const toolingQuery: SalesforceEndpoints['toolingQuery'] = async ( + ctx, + input, +) => { + const response = await salesforceCall<{ + totalSize: number; + done: boolean; + records: Array>; + }>(ctx, 'tooling/query', { method: 'GET', query: { q: input.q } }); + + await logEventFromContext( + ctx, + 'salesforce.tooling.query', + input, + 'completed', + ); + return response; +}; + +export const parameterizedSearch: SalesforceEndpoints['parameterizedSearch'] = + async (ctx, input) => { + const isPost = Boolean(input.sobjects); + const response = await salesforceCall<{ + searchRecords: Array>; + }>(ctx, 'parameterizedSearch', { + method: isPost ? 'POST' : 'GET', + query: isPost ? undefined : { q: input.q }, + body: isPost ? { q: input.q, sobjects: input.sobjects } : undefined, + }); + + await logEventFromContext( + ctx, + 'salesforce.search.parameterized', + input, + 'completed', + ); + return { searchRecords: response.searchRecords ?? [] }; + }; + +export const postParameterizedSearch: SalesforceEndpoints['postParameterizedSearch'] = + async (ctx, input) => { + const response = await salesforceCall<{ + searchRecords: Array>; + }>(ctx, 'parameterizedSearch', { + method: 'POST', + body: input, + }); + + await logEventFromContext( + ctx, + 'salesforce.search.post_parameterized', + input, + 'completed', + ); + return { searchRecords: response.searchRecords ?? [] }; + }; + +export const getSearchLayout: SalesforceEndpoints['getSearchLayout'] = async ( + ctx, + input, +) => { + const response = await salesforceCall>>( + ctx, + 'search/layout', + { + method: 'GET', + query: { q: input.sobjects }, + }, + ); + + await logEventFromContext( + ctx, + 'salesforce.search.layout', + input, + 'completed', + ); + return response; +}; + +/** @deprecated */ +export const query: SalesforceEndpoints['query'] = async (ctx, input) => { + const response = await salesforceCall<{ + totalSize: number; + records: Array>; + }>(ctx, 'query', { method: 'GET', query: { q: input.q } }); + + await logEventFromContext( + ctx, + 'salesforce.soql.query_deprecated', + input, + 'completed', + ); + return response; +}; + +/** @deprecated */ +export const executeSoqlQuery: SalesforceEndpoints['executeSoqlQuery'] = async ( + ctx, + input, +) => { + const response = await salesforceCall<{ + totalSize: number; + records: Array>; + }>(ctx, 'query', { method: 'GET', query: { q: input.q } }); + + await logEventFromContext( + ctx, + 'salesforce.soql.execute_query_deprecated', + input, + 'completed', + ); + return response; +}; + +export const getSearchSuggestions: SalesforceEndpoints['getSearchSuggestions'] = + async (ctx, input) => { + const response = await salesforceCall( + ctx, + 'search/suggestTitleMatches', + { + method: 'GET', + query: { q: input.q, sobject: input.sobject }, + }, + ); + await logEventFromContext( + ctx, + 'salesforce.search.suggestions', + input, + 'completed', + ); + return { result: response }; + }; + +export const searchKnowledgeArticles: SalesforceEndpoints['searchKnowledgeArticles'] = + async (ctx, input) => { + const response = await salesforceCall( + ctx, + 'search/suggestTitleMatches', + { + method: 'GET', + query: { q: input.q, sobject: 'KnowledgeArticleVersion' }, + }, + ); + await logEventFromContext( + ctx, + 'salesforce.search.knowledge', + input, + 'completed', + ); + return { result: response }; + }; + +export const getParameterizedSearch: SalesforceEndpoints['getParameterizedSearch'] = + parameterizedSearch as unknown as SalesforceEndpoints['getParameterizedSearch']; diff --git a/packages/salesforce/endpoints/tasks.ts b/packages/salesforce/endpoints/tasks.ts new file mode 100644 index 000000000..e8d931fff --- /dev/null +++ b/packages/salesforce/endpoints/tasks.ts @@ -0,0 +1,226 @@ +import { logEventFromContext } from 'corsair/core'; +import type { SalesforceEndpoints } from '..'; +import { escapeSoql } from '../utils'; +import { flattenFields, salesforceCall } from './shared'; + +export const createTask: SalesforceEndpoints['createTask'] = async ( + ctx, + input, +) => { + const response = await salesforceCall<{ + id: string; + success?: boolean; + }>(ctx, 'sobjects/Task', { method: 'POST', body: flattenFields(input) }); + + await logEventFromContext(ctx, 'salesforce.task.create', input, 'completed'); + return response; +}; + +export const completeTask: SalesforceEndpoints['completeTask'] = async ( + ctx, + input, +) => { + const body: Record = { Status: 'Completed' }; + if (input.completionNotes) { + const current = await salesforceCall<{ Description?: string | null }>( + ctx, + `sobjects/Task/${input.taskId}`, + { method: 'GET', query: { fields: 'Description' } }, + ); + const existing = + typeof current.Description === 'string' ? current.Description : ''; + body.Description = existing + ? `${existing}\n${input.completionNotes}` + : input.completionNotes; + } + + await salesforceCall(ctx, `sobjects/Task/${input.taskId}`, { + method: 'PATCH', + body, + }); + + await logEventFromContext( + ctx, + 'salesforce.task.complete', + input, + 'completed', + ); + return { success: true }; +}; + +export const logCall: SalesforceEndpoints['logCall'] = async (ctx, input) => { + const body = { + Subject: input.Subject, + Status: 'Completed', + TaskSubtype: 'Call', + CallDurationInSeconds: input.CallDurationInSeconds, + CallType: input.CallType, + CallDisposition: input.CallDisposition, + Description: input.Description, + WhoId: input.WhoId, + WhatId: input.WhatId, + }; + + const response = await salesforceCall<{ id: string }>(ctx, 'sobjects/Task', { + method: 'POST', + body, + }); + + await logEventFromContext( + ctx, + 'salesforce.task.log_call', + input, + 'completed', + ); + return response; +}; + +export const logEmailActivity: SalesforceEndpoints['logEmailActivity'] = async ( + ctx, + input, +) => { + const body = { + Subject: input.Subject, + TextBody: input.TextBody, + HtmlBody: input.HtmlBody, + FromAddress: input.FromAddress, + ToAddress: input.ToAddress, + RelatedToId: input.RelatedToId, + }; + + const response = await salesforceCall<{ id: string }>( + ctx, + 'sobjects/EmailMessage', + { method: 'POST', body }, + ); + + await logEventFromContext( + ctx, + 'salesforce.task.log_email', + input, + 'completed', + ); + return response; +}; + +export const updateTask: SalesforceEndpoints['updateTask'] = async ( + ctx, + input, +) => { + const { id, ...fields } = input; + const body = flattenFields(fields); + await salesforceCall(ctx, `sobjects/Task/${id}`, { + method: 'PATCH', + body, + }); + await logEventFromContext(ctx, 'salesforce.task.update', { id }, 'completed'); + return { success: true }; +}; + +export const searchTasks: SalesforceEndpoints['searchTasks'] = async ( + ctx, + input, +) => { + const terms: string[] = []; + if (input.subject) + terms.push(`Subject LIKE '%${escapeSoql(input.subject)}%'`); + if (input.status) terms.push(`Status = '${escapeSoql(input.status)}'`); + if (input.priority) terms.push(`Priority = '${escapeSoql(input.priority)}'`); + if (input.whoId) terms.push(`WhoId = '${escapeSoql(input.whoId)}'`); + if (input.whatId) terms.push(`WhatId = '${escapeSoql(input.whatId)}'`); + const whereStr = terms.length > 0 ? ` WHERE ${terms.join(' AND ')}` : ''; + const q = `SELECT Id, Subject, Status, Priority, WhoId, WhatId, ActivityDate FROM Task${whereStr} LIMIT ${input.limit ?? 50}`; + const response = await salesforceCall<{ + records: Array>; + }>(ctx, 'query', { method: 'GET', query: { q } }); + await logEventFromContext(ctx, 'salesforce.task.search', input, 'completed'); + return { records: response.records ?? [] }; +}; + +export const sendEmail: SalesforceEndpoints['sendEmail'] = async ( + ctx, + input, +) => { + const response = await salesforceCall( + ctx, + 'actions/standard/emailSimple', + { + method: 'POST', + body: { + inputs: [ + { + emailAddresses: input.toAddresses?.join(','), + emailSubject: input.subject, + emailBody: input.body, + senderType: input.senderType ?? 'CurrentUser', + }, + ], + }, + }, + ); + await logEventFromContext(ctx, 'salesforce.email.send', input, 'completed'); + return { result: response }; +}; + +export const sendEmailFromTemplate: SalesforceEndpoints['sendEmailFromTemplate'] = + async (ctx, input) => { + const response = await salesforceCall( + ctx, + 'actions/standard/emailSimple', + { + method: 'POST', + body: { + inputs: [ + { + emailAddresses: input.toAddresses?.join(','), + emailTemplateId: input.templateId, + senderType: input.senderType ?? 'CurrentUser', + targetObjectId: input.targetObjectId, + }, + ], + }, + }, + ); + await logEventFromContext( + ctx, + 'salesforce.email.send_from_template', + input, + 'completed', + ); + return { result: response }; + }; + +export const sendMassEmail: SalesforceEndpoints['sendMassEmail'] = async ( + ctx, + input, +) => { + const addresses = input.toAddresses ?? []; + const item: Record = { + emailAddresses: addresses.join(','), + }; + if (input.templateId) { + item.emailTemplateId = input.templateId; + const recipient = addresses.find((a) => /^[a-zA-Z0-9]{15,18}$/.test(a)); + if (recipient) item.recipientId = recipient; + } else { + item.emailSubject = input.subject; + item.emailBody = input.body; + } + const response = await salesforceCall( + ctx, + 'actions/standard/emailSimple', + { + method: 'POST', + body: { + inputs: [item], + }, + }, + ); + await logEventFromContext( + ctx, + 'salesforce.email.send_mass', + input, + 'completed', + ); + return { result: response }; +}; diff --git a/packages/salesforce/endpoints/types.ts b/packages/salesforce/endpoints/types.ts new file mode 100644 index 000000000..d2fb9d7b3 --- /dev/null +++ b/packages/salesforce/endpoints/types.ts @@ -0,0 +1,3723 @@ +import { z } from 'zod'; + +// Accounts +export const CreateAccountInputSchema = z.object({ + Name: z.string(), + Type: z.string().optional(), + Industry: z.string().optional(), + Phone: z.string().optional(), + Website: z.string().optional(), + BillingStreet: z.string().optional(), + BillingCity: z.string().optional(), + BillingState: z.string().optional(), + BillingPostalCode: z.string().optional(), + BillingCountry: z.string().optional(), + CustomFields: z.record(z.string(), z.unknown()).optional(), +}); +export type CreateAccountInput = z.infer; + +export const CreateAccountResponseSchema = z + .object({ + id: z.string(), + success: z.boolean().optional(), + errors: z.array(z.unknown()).optional(), + }) + .passthrough(); +export type CreateAccountResponse = z.infer; + +export const GetAccountInputSchema = z.object({ + id: z.string(), + fields: z.array(z.string()).optional(), +}); +export type GetAccountInput = z.infer; + +export const GetAccountResponseSchema = z + .object({ + Id: z.string(), + Name: z.string().optional(), + }) + .passthrough(); +export type GetAccountResponse = z.infer; + +export const ListAccountsInputSchema = z.object({ + query: z.string().optional(), + limit: z.number().optional(), + offset: z.number().optional(), + fields: z.array(z.string()).optional(), +}); +export type ListAccountsInput = z.infer; + +export const ListAccountsResponseSchema = z + .object({ + totalSize: z.number(), + done: z.boolean(), + records: z.array(z.record(z.string(), z.unknown())), + nextRecordsUrl: z.string().optional(), + }) + .passthrough(); +export type ListAccountsResponse = z.infer; + +export const SearchAccountsInputSchema = z.object({ + name: z.string().optional(), + industry: z.string().optional(), + type: z.string().optional(), + phone: z.string().optional(), + limit: z.number().optional(), +}); +export type SearchAccountsInput = z.infer; + +export const SearchAccountsResponseSchema = z + .object({ + records: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type SearchAccountsResponse = z.infer< + typeof SearchAccountsResponseSchema +>; + +export const DeleteAccountInputSchema = z.object({ + id: z.string(), +}); +export type DeleteAccountInput = z.infer; + +export const DeleteAccountResponseSchema = z + .object({ + success: z.boolean(), + }) + .passthrough(); +export type DeleteAccountResponse = z.infer; + +export const AccountCreationWithContentTypeOptionInputSchema = z + .object({ + Name: z.string(), + }) + .passthrough(); +export type AccountCreationWithContentTypeOptionInput = z.infer< + typeof AccountCreationWithContentTypeOptionInputSchema +>; + +export const AccountCreationWithContentTypeOptionResponseSchema = z + .object({ + id: z.string(), + success: z.boolean().optional(), + }) + .passthrough(); +export type AccountCreationWithContentTypeOptionResponse = z.infer< + typeof AccountCreationWithContentTypeOptionResponseSchema +>; + +export const FetchAccountByIdWithQueryInputSchema = z.object({ + id: z.string(), + fields: z.string().optional(), +}); +export type FetchAccountByIdWithQueryInput = z.infer< + typeof FetchAccountByIdWithQueryInputSchema +>; + +export const FetchAccountByIdWithQueryResponseSchema = z + .object({ + Id: z.string(), + }) + .passthrough(); +export type FetchAccountByIdWithQueryResponse = z.infer< + typeof FetchAccountByIdWithQueryResponseSchema +>; + +export const RemoveAccountByUniqueIdentifierInputSchema = z.object({ + id: z.string(), +}); +export type RemoveAccountByUniqueIdentifierInput = z.infer< + typeof RemoveAccountByUniqueIdentifierInputSchema +>; + +export const RemoveAccountByUniqueIdentifierResponseSchema = z + .object({ + success: z.boolean(), + }) + .passthrough(); +export type RemoveAccountByUniqueIdentifierResponse = z.infer< + typeof RemoveAccountByUniqueIdentifierResponseSchema +>; + +export const RetrieveAccountDataAndErrorResponsesInputSchema = z.object({ + id: z.string().optional(), +}); +export type RetrieveAccountDataAndErrorResponsesInput = z.infer< + typeof RetrieveAccountDataAndErrorResponsesInputSchema +>; + +export const RetrieveAccountDataAndErrorResponsesResponseSchema = z + .object({ + objectDescribe: z.record(z.string(), z.unknown()).optional(), + }) + .passthrough(); +export type RetrieveAccountDataAndErrorResponsesResponse = z.infer< + typeof RetrieveAccountDataAndErrorResponsesResponseSchema +>; + +// Contacts +export const CreateContactInputSchema = z.object({ + LastName: z.string(), + FirstName: z.string().optional(), + Email: z.string().optional(), + Phone: z.string().optional(), + AccountId: z.string().optional(), + Title: z.string().optional(), + CustomFields: z.record(z.string(), z.unknown()).optional(), +}); +export type CreateContactInput = z.infer; + +export const CreateContactResponseSchema = z + .object({ + id: z.string(), + success: z.boolean().optional(), + }) + .passthrough(); +export type CreateContactResponse = z.infer; + +export const GetContactInputSchema = z.object({ + id: z.string(), + fields: z.array(z.string()).optional(), +}); +export type GetContactInput = z.infer; + +export const GetContactResponseSchema = z + .object({ + Id: z.string(), + LastName: z.string().optional(), + }) + .passthrough(); +export type GetContactResponse = z.infer; + +export const ListContactsInputSchema = z.object({ + query: z.string().optional(), + limit: z.number().optional(), + offset: z.number().optional(), + accountId: z.string().optional(), +}); +export type ListContactsInput = z.infer; + +export const ListContactsResponseSchema = z + .object({ + totalSize: z.number(), + done: z.boolean(), + records: z.array(z.record(z.string(), z.unknown())), + nextRecordsUrl: z.string().optional(), + }) + .passthrough(); +export type ListContactsResponse = z.infer; + +export const DeleteContactInputSchema = z.object({ + id: z.string(), +}); +export type DeleteContactInput = z.infer; + +export const DeleteContactResponseSchema = z + .object({ + success: z.boolean(), + }) + .passthrough(); +export type DeleteContactResponse = z.infer; + +export const AssociateContactToAccountInputSchema = z.object({ + contactId: z.string(), + accountId: z.string(), +}); +export type AssociateContactToAccountInput = z.infer< + typeof AssociateContactToAccountInputSchema +>; + +export const AssociateContactToAccountResponseSchema = z + .object({ + success: z.boolean(), + }) + .passthrough(); +export type AssociateContactToAccountResponse = z.infer< + typeof AssociateContactToAccountResponseSchema +>; + +export const CreateNewContactWithJsonHeaderInputSchema = z + .object({ + LastName: z.string(), + AccountId: z.string().optional(), + }) + .passthrough(); +export type CreateNewContactWithJsonHeaderInput = z.infer< + typeof CreateNewContactWithJsonHeaderInputSchema +>; + +export const CreateNewContactWithJsonHeaderResponseSchema = z + .object({ + id: z.string(), + }) + .passthrough(); +export type CreateNewContactWithJsonHeaderResponse = z.infer< + typeof CreateNewContactWithJsonHeaderResponseSchema +>; + +export const QueryContactsByNameInputSchema = z.object({ + name: z.string(), +}); +export type QueryContactsByNameInput = z.infer< + typeof QueryContactsByNameInputSchema +>; + +export const QueryContactsByNameResponseSchema = z + .object({ + records: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type QueryContactsByNameResponse = z.infer< + typeof QueryContactsByNameResponseSchema +>; + +export const RemoveASpecificContactByIdInputSchema = z.object({ + id: z.string(), +}); +export type RemoveASpecificContactByIdInput = z.infer< + typeof RemoveASpecificContactByIdInputSchema +>; + +export const RemoveASpecificContactByIdResponseSchema = z + .object({ + success: z.boolean(), + }) + .passthrough(); +export type RemoveASpecificContactByIdResponse = z.infer< + typeof RemoveASpecificContactByIdResponseSchema +>; + +export const RetrieveContactInfoWithStandardResponsesInputSchema = z.object({ + id: z.string().optional(), +}); +export type RetrieveContactInfoWithStandardResponsesInput = z.infer< + typeof RetrieveContactInfoWithStandardResponsesInputSchema +>; + +export const RetrieveContactInfoWithStandardResponsesResponseSchema = z + .object({ + metadata: z.record(z.string(), z.unknown()), + }) + .passthrough(); +export type RetrieveContactInfoWithStandardResponsesResponse = z.infer< + typeof RetrieveContactInfoWithStandardResponsesResponseSchema +>; + +export const GetContactByIdInputSchema = z.object({ + id: z.string(), +}); +export type GetContactByIdInput = z.infer; + +export const GetContactByIdResponseSchema = z + .object({ + Id: z.string(), + }) + .passthrough(); +export type GetContactByIdResponse = z.infer< + typeof GetContactByIdResponseSchema +>; + +// Leads +export const CreateLeadInputSchema = z.object({ + LastName: z.string(), + Company: z.string(), + FirstName: z.string().optional(), + Email: z.string().optional(), + Phone: z.string().optional(), + Status: z.string().optional(), + Title: z.string().optional(), + CustomFields: z.record(z.string(), z.unknown()).optional(), +}); +export type CreateLeadInput = z.infer; + +export const CreateLeadResponseSchema = z + .object({ + id: z.string(), + success: z.boolean().optional(), + }) + .passthrough(); +export type CreateLeadResponse = z.infer; + +export const GetLeadInputSchema = z.object({ + id: z.string(), +}); +export type GetLeadInput = z.infer; + +export const GetLeadResponseSchema = z + .object({ + Id: z.string(), + }) + .passthrough(); +export type GetLeadResponse = z.infer; + +export const ListLeadsInputSchema = z.object({ + query: z.string().optional(), + limit: z.number().optional(), + offset: z.number().optional(), +}); +export type ListLeadsInput = z.infer; + +export const ListLeadsResponseSchema = z + .object({ + totalSize: z.number(), + done: z.boolean(), + records: z.array(z.record(z.string(), z.unknown())), + nextRecordsUrl: z.string().optional(), + }) + .passthrough(); +export type ListLeadsResponse = z.infer; + +export const DeleteLeadInputSchema = z.object({ + id: z.string(), +}); +export type DeleteLeadInput = z.infer; + +export const DeleteLeadResponseSchema = z + .object({ + success: z.boolean(), + }) + .passthrough(); +export type DeleteLeadResponse = z.infer; + +export const ApplyLeadAssignmentRulesInputSchema = z.object({ + leadId: z.string(), + assignmentRuleId: z.string().optional(), +}); +export type ApplyLeadAssignmentRulesInput = z.infer< + typeof ApplyLeadAssignmentRulesInputSchema +>; + +export const ApplyLeadAssignmentRulesResponseSchema = z + .object({ + success: z.boolean(), + }) + .passthrough(); +export type ApplyLeadAssignmentRulesResponse = z.infer< + typeof ApplyLeadAssignmentRulesResponseSchema +>; + +export const CreateLeadWithSpecifiedContentTypeInputSchema = z + .object({ + LastName: z.string(), + Company: z.string(), + }) + .passthrough(); +export type CreateLeadWithSpecifiedContentTypeInput = z.infer< + typeof CreateLeadWithSpecifiedContentTypeInputSchema +>; + +export const CreateLeadWithSpecifiedContentTypeResponseSchema = z + .object({ + id: z.string(), + }) + .passthrough(); +export type CreateLeadWithSpecifiedContentTypeResponse = z.infer< + typeof CreateLeadWithSpecifiedContentTypeResponseSchema +>; + +export const DeleteALeadObjectByItsIdInputSchema = z.object({ + id: z.string(), +}); +export type DeleteALeadObjectByItsIdInput = z.infer< + typeof DeleteALeadObjectByItsIdInputSchema +>; + +export const DeleteALeadObjectByItsIdResponseSchema = z + .object({ + success: z.boolean(), + }) + .passthrough(); +export type DeleteALeadObjectByItsIdResponse = z.infer< + typeof DeleteALeadObjectByItsIdResponseSchema +>; + +export const RetrieveLeadByIdInputSchema = z.object({ + id: z.string(), +}); +export type RetrieveLeadByIdInput = z.infer; + +export const RetrieveLeadByIdResponseSchema = z + .object({ + Id: z.string(), + }) + .passthrough(); +export type RetrieveLeadByIdResponse = z.infer< + typeof RetrieveLeadByIdResponseSchema +>; + +export const RetrieveLeadDataWithVariousResponsesInputSchema = z.object({ + id: z.string().optional(), +}); +export type RetrieveLeadDataWithVariousResponsesInput = z.infer< + typeof RetrieveLeadDataWithVariousResponsesInputSchema +>; + +export const RetrieveLeadDataWithVariousResponsesResponseSchema = z + .object({ + records: z.array(z.record(z.string(), z.unknown())).optional(), + }) + .passthrough(); +export type RetrieveLeadDataWithVariousResponsesResponse = z.infer< + typeof RetrieveLeadDataWithVariousResponsesResponseSchema +>; + +// Opportunities +export const CreateOpportunityInputSchema = z.object({ + Name: z.string(), + StageName: z.string(), + CloseDate: z.string(), + AccountId: z.string().optional(), + Amount: z.number().optional(), + Probability: z.number().optional(), + CustomFields: z.record(z.string(), z.unknown()).optional(), +}); +export type CreateOpportunityInput = z.infer< + typeof CreateOpportunityInputSchema +>; + +export const CreateOpportunityResponseSchema = z + .object({ + id: z.string(), + success: z.boolean().optional(), + }) + .passthrough(); +export type CreateOpportunityResponse = z.infer< + typeof CreateOpportunityResponseSchema +>; + +export const GetOpportunityInputSchema = z.object({ + id: z.string(), +}); +export type GetOpportunityInput = z.infer; + +export const GetOpportunityResponseSchema = z + .object({ + Id: z.string(), + }) + .passthrough(); +export type GetOpportunityResponse = z.infer< + typeof GetOpportunityResponseSchema +>; + +export const ListOpportunitiesInputSchema = z.object({ + query: z.string().optional(), + limit: z.number().optional(), + offset: z.number().optional(), +}); +export type ListOpportunitiesInput = z.infer< + typeof ListOpportunitiesInputSchema +>; + +export const ListOpportunitiesResponseSchema = z + .object({ + totalSize: z.number(), + done: z.boolean(), + records: z.array(z.record(z.string(), z.unknown())), + nextRecordsUrl: z.string().optional(), + }) + .passthrough(); +export type ListOpportunitiesResponse = z.infer< + typeof ListOpportunitiesResponseSchema +>; + +export const DeleteOpportunityInputSchema = z.object({ + id: z.string(), +}); +export type DeleteOpportunityInput = z.infer< + typeof DeleteOpportunityInputSchema +>; + +export const DeleteOpportunityResponseSchema = z + .object({ + success: z.boolean(), + }) + .passthrough(); +export type DeleteOpportunityResponse = z.infer< + typeof DeleteOpportunityResponseSchema +>; + +export const AddOpportunityLineItemInputSchema = z.object({ + OpportunityId: z.string(), + PricebookEntryId: z.string(), + Quantity: z.number(), + UnitPrice: z.number().optional(), + TotalPrice: z.number().optional(), +}); +export type AddOpportunityLineItemInput = z.infer< + typeof AddOpportunityLineItemInputSchema +>; + +export const AddOpportunityLineItemResponseSchema = z + .object({ + id: z.string(), + }) + .passthrough(); +export type AddOpportunityLineItemResponse = z.infer< + typeof AddOpportunityLineItemResponseSchema +>; + +export const CloneOpportunityWithProductsInputSchema = z.object({ + opportunityId: z.string(), + name: z.string().optional(), + cloneProducts: z.boolean().optional(), +}); +export type CloneOpportunityWithProductsInput = z.infer< + typeof CloneOpportunityWithProductsInputSchema +>; + +export const CloneOpportunityWithProductsResponseSchema = z + .object({ + id: z.string(), + }) + .passthrough(); +export type CloneOpportunityWithProductsResponse = z.infer< + typeof CloneOpportunityWithProductsResponseSchema +>; + +export const ListPricebookEntriesInputSchema = z.object({ + pricebookId: z.string().optional(), + query: z.string().optional(), + limit: z.number().optional(), +}); +export type ListPricebookEntriesInput = z.infer< + typeof ListPricebookEntriesInputSchema +>; + +export const ListPricebookEntriesResponseSchema = z + .object({ + records: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type ListPricebookEntriesResponse = z.infer< + typeof ListPricebookEntriesResponseSchema +>; + +export const ListPricebooksInputSchema = z.object({ + query: z.string().optional(), + limit: z.number().optional(), +}); +export type ListPricebooksInput = z.infer; + +export const ListPricebooksResponseSchema = z + .object({ + records: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type ListPricebooksResponse = z.infer< + typeof ListPricebooksResponseSchema +>; + +export const CreateOpportunityRecordInputSchema = z + .object({ + Name: z.string(), + StageName: z.string(), + CloseDate: z.string(), + }) + .passthrough(); +export type CreateOpportunityRecordInput = z.infer< + typeof CreateOpportunityRecordInputSchema +>; + +export const CreateOpportunityRecordResponseSchema = z + .object({ + id: z.string(), + }) + .passthrough(); +export type CreateOpportunityRecordResponse = z.infer< + typeof CreateOpportunityRecordResponseSchema +>; + +export const RemoveOpportunityByIdInputSchema = z.object({ + id: z.string(), +}); +export type RemoveOpportunityByIdInput = z.infer< + typeof RemoveOpportunityByIdInputSchema +>; + +export const RemoveOpportunityByIdResponseSchema = z + .object({ + success: z.boolean(), + }) + .passthrough(); +export type RemoveOpportunityByIdResponse = z.infer< + typeof RemoveOpportunityByIdResponseSchema +>; + +export const RetrieveOpportunitiesDataInputSchema = z.object({ + query: z.string().optional(), + limit: z.number().int().positive().optional(), +}); +export type RetrieveOpportunitiesDataInput = z.infer< + typeof RetrieveOpportunitiesDataInputSchema +>; + +export const RetrieveOpportunitiesDataResponseSchema = z + .object({ + records: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type RetrieveOpportunitiesDataResponse = z.infer< + typeof RetrieveOpportunitiesDataResponseSchema +>; + +export const RetrieveOpportunityByIdWithOptionalFieldsInputSchema = z.object({ + id: z.string(), + fields: z.array(z.string()).optional(), +}); +export type RetrieveOpportunityByIdWithOptionalFieldsInput = z.infer< + typeof RetrieveOpportunityByIdWithOptionalFieldsInputSchema +>; + +export const RetrieveOpportunityByIdWithOptionalFieldsResponseSchema = z + .object({ + Id: z.string(), + }) + .passthrough(); +export type RetrieveOpportunityByIdWithOptionalFieldsResponse = z.infer< + typeof RetrieveOpportunityByIdWithOptionalFieldsResponseSchema +>; + +// Campaigns +export const CreateCampaignInputSchema = z.object({ + Name: z.string(), + Type: z.string().optional(), + Status: z.string().optional(), + StartDate: z.string().optional(), + EndDate: z.string().optional(), + IsActive: z.boolean().optional(), + ParentId: z.string().optional(), +}); +export type CreateCampaignInput = z.infer; + +export const CreateCampaignResponseSchema = z + .object({ + id: z.string(), + success: z.boolean().optional(), + }) + .passthrough(); +export type CreateCampaignResponse = z.infer< + typeof CreateCampaignResponseSchema +>; + +export const GetCampaignInputSchema = z.object({ + id: z.string(), +}); +export type GetCampaignInput = z.infer; + +export const GetCampaignResponseSchema = z + .object({ + Id: z.string(), + }) + .passthrough(); +export type GetCampaignResponse = z.infer; + +export const ListCampaignsInputSchema = z.object({ + query: z.string().optional(), + limit: z.number().optional(), +}); +export type ListCampaignsInput = z.infer; + +export const ListCampaignsResponseSchema = z + .object({ + totalSize: z.number(), + done: z.boolean(), + records: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type ListCampaignsResponse = z.infer; + +export const DeleteCampaignInputSchema = z.object({ + id: z.string(), +}); +export type DeleteCampaignInput = z.infer; + +export const DeleteCampaignResponseSchema = z + .object({ + success: z.boolean(), + }) + .passthrough(); +export type DeleteCampaignResponse = z.infer< + typeof DeleteCampaignResponseSchema +>; + +export const AddContactToCampaignInputSchema = z.object({ + campaignId: z.string(), + contactId: z.string(), + status: z.string().optional(), +}); +export type AddContactToCampaignInput = z.infer< + typeof AddContactToCampaignInputSchema +>; + +export const AddContactToCampaignResponseSchema = z + .object({ + id: z.string(), + }) + .passthrough(); +export type AddContactToCampaignResponse = z.infer< + typeof AddContactToCampaignResponseSchema +>; + +export const AddLeadToCampaignInputSchema = z.object({ + campaign_id: z.string(), + lead_id: z.string(), + status: z.string().optional(), +}); +export type AddLeadToCampaignInput = z.infer< + typeof AddLeadToCampaignInputSchema +>; + +export const AddLeadToCampaignResponseSchema = z + .object({ + id: z.string(), + }) + .passthrough(); +export type AddLeadToCampaignResponse = z.infer< + typeof AddLeadToCampaignResponseSchema +>; + +export const RemoveFromCampaignInputSchema = z.object({ + member_id: z.string().optional(), + campaign_member_id: z.string().optional(), + campaign_id: z.string().optional(), +}); +export type RemoveFromCampaignInput = z.infer< + typeof RemoveFromCampaignInputSchema +>; + +export const RemoveFromCampaignResponseSchema = z + .object({ + success: z.boolean(), + }) + .passthrough(); +export type RemoveFromCampaignResponse = z.infer< + typeof RemoveFromCampaignResponseSchema +>; + +export const SearchCampaignsInputSchema = z.object({ + name: z.string().optional(), + type: z.string().optional(), + status: z.string().optional(), + limit: z.number().int().positive().optional(), +}); +export type SearchCampaignsInput = z.infer; + +export const SearchCampaignsResponseSchema = z + .object({ + records: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type SearchCampaignsResponse = z.infer< + typeof SearchCampaignsResponseSchema +>; + +export const CreateCampaignRecordViaPostInputSchema = z + .object({ + Name: z.string(), + ParentId: z.string().optional(), + OwnerId: z.string().optional(), + }) + .passthrough(); +export type CreateCampaignRecordViaPostInput = z.infer< + typeof CreateCampaignRecordViaPostInputSchema +>; + +export const CreateCampaignRecordViaPostResponseSchema = z + .object({ + id: z.string(), + }) + .passthrough(); +export type CreateCampaignRecordViaPostResponse = z.infer< + typeof CreateCampaignRecordViaPostResponseSchema +>; + +export const RemoveCampaignObjectByIdInputSchema = z.object({ + id: z.string(), +}); +export type RemoveCampaignObjectByIdInput = z.infer< + typeof RemoveCampaignObjectByIdInputSchema +>; + +export const RemoveCampaignObjectByIdResponseSchema = z + .object({ + success: z.boolean(), + }) + .passthrough(); +export type RemoveCampaignObjectByIdResponse = z.infer< + typeof RemoveCampaignObjectByIdResponseSchema +>; + +export const RetrieveCampaignDataWithErrorHandlingInputSchema = z.object({ + id: z.string().optional(), +}); +export type RetrieveCampaignDataWithErrorHandlingInput = z.infer< + typeof RetrieveCampaignDataWithErrorHandlingInputSchema +>; + +export const RetrieveCampaignDataWithErrorHandlingResponseSchema = z + .object({ + metadata: z.record(z.string(), z.unknown()), + }) + .passthrough(); +export type RetrieveCampaignDataWithErrorHandlingResponse = z.infer< + typeof RetrieveCampaignDataWithErrorHandlingResponseSchema +>; + +export const RetrieveSpecificCampaignObjectDetailsInputSchema = z.object({ + id: z.string(), + fields: z.array(z.string()).optional(), +}); +export type RetrieveSpecificCampaignObjectDetailsInput = z.infer< + typeof RetrieveSpecificCampaignObjectDetailsInputSchema +>; + +export const RetrieveSpecificCampaignObjectDetailsResponseSchema = z + .object({ + Id: z.string(), + }) + .passthrough(); +export type RetrieveSpecificCampaignObjectDetailsResponse = z.infer< + typeof RetrieveSpecificCampaignObjectDetailsResponseSchema +>; + +// Notes +export const CreateNoteInputSchema = z.object({ + Title: z.string(), + Body: z.string().optional(), + ParentId: z.string().optional(), + IsPrivate: z.boolean().optional(), +}); +export type CreateNoteInput = z.infer; + +export const CreateNoteResponseSchema = z + .object({ + id: z.string(), + success: z.boolean().optional(), + }) + .passthrough(); +export type CreateNoteResponse = z.infer; + +export const GetNoteInputSchema = z.object({ + id: z.string(), +}); +export type GetNoteInput = z.infer; + +export const GetNoteResponseSchema = z + .object({ + Id: z.string(), + Title: z.string().optional(), + Body: z.string().optional(), + }) + .passthrough(); +export type GetNoteResponse = z.infer; + +export const ListNotesInputSchema = z.object({ + parentId: z.string().optional(), + query: z.string().optional(), + limit: z.number().optional(), +}); +export type ListNotesInput = z.infer; + +export const ListNotesResponseSchema = z + .object({ + records: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type ListNotesResponse = z.infer; + +export const DeleteNoteInputSchema = z.object({ + id: z.string(), +}); +export type DeleteNoteInput = z.infer; + +export const DeleteNoteResponseSchema = z + .object({ + success: z.boolean(), + }) + .passthrough(); +export type DeleteNoteResponse = z.infer; + +export const CreateNoteRecordWithContentTypeHeaderInputSchema = z + .object({ + Title: z.string(), + ParentId: z.string(), + Body: z.string().optional(), + }) + .passthrough(); +export type CreateNoteRecordWithContentTypeHeaderInput = z.infer< + typeof CreateNoteRecordWithContentTypeHeaderInputSchema +>; + +export const CreateNoteRecordWithContentTypeHeaderResponseSchema = z + .object({ + id: z.string(), + }) + .passthrough(); +export type CreateNoteRecordWithContentTypeHeaderResponse = z.infer< + typeof CreateNoteRecordWithContentTypeHeaderResponseSchema +>; + +export const RemoveNoteObjectByIdInputSchema = z.object({ + id: z.string(), +}); +export type RemoveNoteObjectByIdInput = z.infer< + typeof RemoveNoteObjectByIdInputSchema +>; + +export const RemoveNoteObjectByIdResponseSchema = z + .object({ + success: z.boolean(), + }) + .passthrough(); +export type RemoveNoteObjectByIdResponse = z.infer< + typeof RemoveNoteObjectByIdResponseSchema +>; + +export const GetNoteByIdWithFieldsInputSchema = z.object({ + id: z.string(), + fields: z.array(z.string()).optional(), +}); +export type GetNoteByIdWithFieldsInput = z.infer< + typeof GetNoteByIdWithFieldsInputSchema +>; + +export const GetNoteByIdWithFieldsResponseSchema = z + .object({ + Id: z.string(), + }) + .passthrough(); +export type GetNoteByIdWithFieldsResponse = z.infer< + typeof GetNoteByIdWithFieldsResponseSchema +>; + +export const RetrieveNoteObjectInformationInputSchema = z.object({ + id: z.string().optional(), +}); +export type RetrieveNoteObjectInformationInput = z.infer< + typeof RetrieveNoteObjectInformationInputSchema +>; + +export const RetrieveNoteObjectInformationResponseSchema = z + .object({ + metadata: z.record(z.string(), z.unknown()), + }) + .passthrough(); +export type RetrieveNoteObjectInformationResponse = z.infer< + typeof RetrieveNoteObjectInformationResponseSchema +>; + +// Tasks +export const CreateTaskInputSchema = z.object({ + Subject: z.string(), + Status: z.string().optional(), + Priority: z.string().optional(), + WhoId: z.string().optional(), + WhatId: z.string().optional(), + OwnerId: z.string().optional(), + ActivityDate: z.string().optional(), + Description: z.string().optional(), +}); +export type CreateTaskInput = z.infer; + +export const CreateTaskResponseSchema = z + .object({ + id: z.string(), + success: z.boolean().optional(), + }) + .passthrough(); +export type CreateTaskResponse = z.infer; + +export const CompleteTaskInputSchema = z.object({ + taskId: z.string(), + completionNotes: z.string().optional(), +}); +export type CompleteTaskInput = z.infer; + +export const CompleteTaskResponseSchema = z + .object({ + success: z.boolean(), + }) + .passthrough(); +export type CompleteTaskResponse = z.infer; + +export const LogCallInputSchema = z.object({ + Subject: z.string(), + CallDurationInSeconds: z.number().optional(), + CallType: z.string().optional(), + CallDisposition: z.string().optional(), + Description: z.string().optional(), + WhoId: z.string().optional(), + WhatId: z.string().optional(), +}); +export type LogCallInput = z.infer; + +export const LogCallResponseSchema = z + .object({ + id: z.string(), + }) + .passthrough(); +export type LogCallResponse = z.infer; + +export const LogEmailActivityInputSchema = z.object({ + Subject: z.string(), + TextBody: z.string().optional(), + HtmlBody: z.string().optional(), + FromAddress: z.string().optional(), + ToAddress: z.string().optional(), + RelatedToId: z.string().optional(), +}); +export type LogEmailActivityInput = z.infer; + +export const LogEmailActivityResponseSchema = z + .object({ + id: z.string(), + }) + .passthrough(); +export type LogEmailActivityResponse = z.infer< + typeof LogEmailActivityResponseSchema +>; + +// Jobs +export const CloseOrAbortJobInputSchema = z.object({ + jobId: z.string(), + state: z.enum(['UploadComplete', 'Aborted']), +}); +export type CloseOrAbortJobInput = z.infer; + +export const CloseOrAbortJobResponseSchema = z + .object({ + id: z.string(), + state: z.string(), + }) + .passthrough(); +export type CloseOrAbortJobResponse = z.infer< + typeof CloseOrAbortJobResponseSchema +>; + +export const DeleteJobQueryInputSchema = z.object({ + jobId: z.string(), +}); +export type DeleteJobQueryInput = z.infer; + +export const DeleteJobQueryResponseSchema = z + .object({ + success: z.boolean(), + }) + .passthrough(); +export type DeleteJobQueryResponse = z.infer< + typeof DeleteJobQueryResponseSchema +>; + +export const GetJobFailedRecordResultsInputSchema = z.object({ + jobId: z.string(), +}); +export type GetJobFailedRecordResultsInput = z.infer< + typeof GetJobFailedRecordResultsInputSchema +>; + +export const GetJobFailedRecordResultsResponseSchema = z + .object({ + records: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type GetJobFailedRecordResultsResponse = z.infer< + typeof GetJobFailedRecordResultsResponseSchema +>; + +export const GetQueryJobInfoInputSchema = z.object({ + jobId: z.string(), +}); +export type GetQueryJobInfoInput = z.infer; + +export const GetQueryJobInfoResponseSchema = z + .object({ + id: z.string(), + state: z.string(), + }) + .passthrough(); +export type GetQueryJobInfoResponse = z.infer< + typeof GetQueryJobInfoResponseSchema +>; + +export const GetQueryJobResultsInputSchema = z.object({ + jobId: z.string(), + maxRecords: z.number().optional(), + locator: z.string().optional(), +}); +export type GetQueryJobResultsInput = z.infer< + typeof GetQueryJobResultsInputSchema +>; + +export const GetQueryJobResultsResponseSchema = z + .object({ + data: z.string().or(z.array(z.record(z.string(), z.unknown()))), + }) + .passthrough(); +export type GetQueryJobResultsResponse = z.infer< + typeof GetQueryJobResultsResponseSchema +>; + +export const GetJobSuccessfulRecordResultsInputSchema = z.object({ + jobId: z.string(), +}); +export type GetJobSuccessfulRecordResultsInput = z.infer< + typeof GetJobSuccessfulRecordResultsInputSchema +>; + +export const GetJobSuccessfulRecordResultsResponseSchema = z + .object({ + records: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type GetJobSuccessfulRecordResultsResponse = z.infer< + typeof GetJobSuccessfulRecordResultsResponseSchema +>; + +export const GetJobUnprocessedRecordResultsInputSchema = z.object({ + jobId: z.string(), +}); +export type GetJobUnprocessedRecordResultsInput = z.infer< + typeof GetJobUnprocessedRecordResultsInputSchema +>; + +export const GetJobUnprocessedRecordResultsResponseSchema = z + .object({ + records: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type GetJobUnprocessedRecordResultsResponse = z.infer< + typeof GetJobUnprocessedRecordResultsResponseSchema +>; + +// SOQL / SOSL +export const RunSoqlQueryInputSchema = z.object({ + q: z.string(), +}); +export type RunSoqlQueryInput = z.infer; + +export const RunSoqlQueryResponseSchema = z + .object({ + totalSize: z.number(), + done: z.boolean(), + records: z.array(z.record(z.string(), z.unknown())), + nextRecordsUrl: z.string().optional(), + }) + .passthrough(); +export type RunSoqlQueryResponse = z.infer; + +export const QueryAllInputSchema = z.object({ + q: z.string(), +}); +export type QueryAllInput = z.infer; + +export const QueryAllResponseSchema = z + .object({ + totalSize: z.number(), + done: z.boolean(), + records: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type QueryAllResponse = z.infer; + +export const SearchInputSchema = z.object({ + q: z.string(), +}); +export type SearchInput = z.infer; + +export const SearchResponseSchema = z + .object({ + searchRecords: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type SearchResponse = z.infer; + +export const ExecuteSoslSearchInputSchema = z.object({ + q: z.string(), +}); +export type ExecuteSoslSearchInput = z.infer< + typeof ExecuteSoslSearchInputSchema +>; + +export const ExecuteSoslSearchResponseSchema = z + .object({ + searchRecords: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type ExecuteSoslSearchResponse = z.infer< + typeof ExecuteSoslSearchResponseSchema +>; + +export const ToolingQueryInputSchema = z.object({ + q: z.string(), +}); +export type ToolingQueryInput = z.infer; + +export const ToolingQueryResponseSchema = z + .object({ + totalSize: z.number(), + done: z.boolean(), + records: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type ToolingQueryResponse = z.infer; + +export const ParameterizedSearchInputSchema = z.object({ + q: z.string().optional(), + sobjects: z.array(z.record(z.string(), z.unknown())).optional(), +}); +export type ParameterizedSearchInput = z.infer< + typeof ParameterizedSearchInputSchema +>; + +export const ParameterizedSearchResponseSchema = z + .object({ + searchRecords: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type ParameterizedSearchResponse = z.infer< + typeof ParameterizedSearchResponseSchema +>; + +export const PostParameterizedSearchInputSchema = z + .object({ + q: z.string(), + fields: z.array(z.string()).optional(), + }) + .passthrough(); +export type PostParameterizedSearchInput = z.infer< + typeof PostParameterizedSearchInputSchema +>; + +export const PostParameterizedSearchResponseSchema = z + .object({ + searchRecords: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type PostParameterizedSearchResponse = z.infer< + typeof PostParameterizedSearchResponseSchema +>; + +export const GetSearchLayoutInputSchema = z.object({ + sobjects: z.string(), +}); +export type GetSearchLayoutInput = z.infer; + +export const GetSearchLayoutResponseSchema = z.array( + z.record(z.string(), z.unknown()), +); +export type GetSearchLayoutResponse = z.infer< + typeof GetSearchLayoutResponseSchema +>; + +export const QueryInputSchema = z.object({ + q: z.string(), +}); +export type QueryInput = z.infer; + +export const QueryResponseSchema = z + .object({ + totalSize: z.number(), + records: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type QueryResponse = z.infer; + +export const ExecuteSoqlQueryInputSchema = z.object({ + q: z.string(), +}); +export type ExecuteSoqlQueryInput = z.infer; + +export const ExecuteSoqlQueryResponseSchema = z + .object({ + totalSize: z.number(), + records: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type ExecuteSoqlQueryResponse = z.infer< + typeof ExecuteSoqlQueryResponseSchema +>; + +// Composite +export const PostCompositeSobjectsInputSchema = z.object({ + allOrNone: z.boolean().optional(), + records: z.array(z.record(z.string(), z.unknown())), +}); +export type PostCompositeSobjectsInput = z.infer< + typeof PostCompositeSobjectsInputSchema +>; + +export const PostCompositeSobjectsResponseSchema = z.array( + z.record(z.string(), z.unknown()), +); +export type PostCompositeSobjectsResponse = z.infer< + typeof PostCompositeSobjectsResponseSchema +>; + +export const CreateSobjectTreeInputSchema = z.object({ + sobject: z.string(), + records: z.array(z.record(z.string(), z.unknown())), +}); +export type CreateSobjectTreeInput = z.infer< + typeof CreateSobjectTreeInputSchema +>; + +export const CreateSobjectTreeResponseSchema = z + .object({ + hasErrors: z.boolean(), + results: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type CreateSobjectTreeResponse = z.infer< + typeof CreateSobjectTreeResponseSchema +>; + +export const DeleteSobjectCollectionsInputSchema = z.object({ + ids: z.array(z.string()), + allOrNone: z.boolean().optional(), +}); +export type DeleteSobjectCollectionsInput = z.infer< + typeof DeleteSobjectCollectionsInputSchema +>; + +export const DeleteSobjectCollectionsResponseSchema = z.array( + z.record(z.string(), z.unknown()), +); +export type DeleteSobjectCollectionsResponse = z.infer< + typeof DeleteSobjectCollectionsResponseSchema +>; + +export const PostCompositeGraphInputSchema = z.object({ + graphs: z.array(z.record(z.string(), z.unknown())), +}); +export type PostCompositeGraphInput = z.infer< + typeof PostCompositeGraphInputSchema +>; + +export const PostCompositeGraphResponseSchema = z + .object({ + graphs: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type PostCompositeGraphResponse = z.infer< + typeof PostCompositeGraphResponseSchema +>; + +export const CompositeGraphActionInputSchema = z + .object({ + graphs: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type CompositeGraphActionInput = z.infer< + typeof CompositeGraphActionInputSchema +>; + +export const CompositeGraphActionResponseSchema = z + .object({ + graphs: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type CompositeGraphActionResponse = z.infer< + typeof CompositeGraphActionResponseSchema +>; + +export const GetABatchOfRecordsInputSchema = z.object({ + ids: z.array(z.string()), + fields: z.array(z.string()).optional(), +}); +export type GetABatchOfRecordsInput = z.infer< + typeof GetABatchOfRecordsInputSchema +>; + +export const GetABatchOfRecordsResponseSchema = z + .object({ + results: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type GetABatchOfRecordsResponse = z.infer< + typeof GetABatchOfRecordsResponseSchema +>; + +export const GetCompositeResourcesInputSchema = z.object({}); +export type GetCompositeResourcesInput = z.infer< + typeof GetCompositeResourcesInputSchema +>; + +export const GetCompositeResourcesResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetCompositeResourcesResponse = z.infer< + typeof GetCompositeResourcesResponseSchema +>; + +export const GetCompositeSobjectsInputSchema = z.object({ + ids: z.array(z.string()), + fields: z.array(z.string()).optional(), +}); +export type GetCompositeSobjectsInput = z.infer< + typeof GetCompositeSobjectsInputSchema +>; + +export const GetCompositeSobjectsResponseSchema = z.array( + z.record(z.string(), z.unknown()), +); +export type GetCompositeSobjectsResponse = z.infer< + typeof GetCompositeSobjectsResponseSchema +>; + +export const GetSobjectCollectionsInputSchema = z.object({ + ids: z.array(z.string()), + fields: z.array(z.string()).optional(), +}); +export type GetSobjectCollectionsInput = z.infer< + typeof GetSobjectCollectionsInputSchema +>; + +export const GetSobjectCollectionsResponseSchema = z.array( + z.record(z.string(), z.unknown()), +); +export type GetSobjectCollectionsResponse = z.infer< + typeof GetSobjectCollectionsResponseSchema +>; + +// Metadata & Tooling +export const CreateSObjectRecordInputSchema = z.object({ + sobject: z.string(), + fields: z.record(z.string(), z.unknown()), +}); +export type CreateSObjectRecordInput = z.infer< + typeof CreateSObjectRecordInputSchema +>; + +export const CreateSObjectRecordResponseSchema = z + .object({ + id: z.string(), + success: z.boolean().optional(), + }) + .passthrough(); +export type CreateSObjectRecordResponse = z.infer< + typeof CreateSObjectRecordResponseSchema +>; + +export const CloneRecordInputSchema = z.object({ + sobject: z.string(), + recordId: z.string(), + overrides: z.record(z.string(), z.unknown()).optional(), +}); +export type CloneRecordInput = z.infer; + +export const CloneRecordResponseSchema = z + .object({ + id: z.string(), + }) + .passthrough(); +export type CloneRecordResponse = z.infer; + +export const CreateCustomFieldInputSchema = z.object({ + sobject: z.string(), + developerName: z.string(), + label: z.string(), + type: z.string(), + length: z.number().optional(), +}); +export type CreateCustomFieldInput = z.infer< + typeof CreateCustomFieldInputSchema +>; + +export const CreateCustomFieldResponseSchema = z + .object({ + id: z.string(), + success: z.boolean().optional(), + }) + .passthrough(); +export type CreateCustomFieldResponse = z.infer< + typeof CreateCustomFieldResponseSchema +>; + +export const CreateCustomObjectInputSchema = z.object({ + developerName: z.string(), + label: z.string(), + pluralLabel: z.string(), +}); +export type CreateCustomObjectInput = z.infer< + typeof CreateCustomObjectInputSchema +>; + +export const CreateCustomObjectResponseSchema = z + .object({ + id: z.string(), + success: z.boolean().optional(), + }) + .passthrough(); +export type CreateCustomObjectResponse = z.infer< + typeof CreateCustomObjectResponseSchema +>; + +export const DeleteSobjectInputSchema = z.object({ + sobject: z.string(), + id: z.string(), +}); +export type DeleteSobjectInput = z.infer; + +export const DeleteSobjectResponseSchema = z + .object({ + success: z.boolean(), + }) + .passthrough(); +export type DeleteSobjectResponse = z.infer; + +export const DeleteSobjectRowsInputSchema = z.object({ + sobject: z.string(), + id: z.string(), +}); +export type DeleteSobjectRowsInput = z.infer< + typeof DeleteSobjectRowsInputSchema +>; + +export const DeleteSobjectRowsResponseSchema = z + .object({ + success: z.boolean(), + }) + .passthrough(); +export type DeleteSobjectRowsResponse = z.infer< + typeof DeleteSobjectRowsResponseSchema +>; + +export const GetSobjectsInputSchema = z.object({}); +export type GetSobjectsInput = z.infer; + +export const GetSobjectsResponseSchema = z + .object({ + encoding: z.string().optional(), + maxBatchSize: z.number().optional(), + sobjects: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type GetSobjectsResponse = z.infer; + +export const ExecuteSobjectQuickActionInputSchema = z.object({ + sobject: z.string(), + actionName: z.string(), + contextId: z.string().optional(), + record: z.record(z.string(), z.unknown()).optional(), +}); +export type ExecuteSobjectQuickActionInput = z.infer< + typeof ExecuteSobjectQuickActionInputSchema +>; + +export const ExecuteSobjectQuickActionResponseSchema = z + .object({ + success: z.boolean(), + recordId: z.string().optional(), + }) + .passthrough(); +export type ExecuteSobjectQuickActionResponse = z.infer< + typeof ExecuteSobjectQuickActionResponseSchema +>; + +export const GetApiInputSchema = z.object({ + version: z.string().optional(), +}); +export type GetApiInput = z.infer; + +export const GetApiResponseSchema = z.record(z.string(), z.unknown()); +export type GetApiResponse = z.infer; + +export const GetChatterResourcesInputSchema = z.object({}); +export type GetChatterResourcesInput = z.infer< + typeof GetChatterResourcesInputSchema +>; + +export const GetChatterResourcesResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetChatterResourcesResponse = z.infer< + typeof GetChatterResourcesResponseSchema +>; + +export const GetSobjectPlatformactionInputSchema = z.object({}); +export type GetSobjectPlatformactionInput = z.infer< + typeof GetSobjectPlatformactionInputSchema +>; + +export const GetSobjectPlatformactionResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetSobjectPlatformactionResponse = z.infer< + typeof GetSobjectPlatformactionResponseSchema +>; + +export const HeadQuickActionsInputSchema = z.object({}); +export type HeadQuickActionsInput = z.infer; + +export const HeadQuickActionsResponseSchema = z + .object({ + status: z.number().optional(), + }) + .passthrough(); +export type HeadQuickActionsResponse = z.infer< + typeof HeadQuickActionsResponseSchema +>; + +export const HeadSobjectsUserPasswordInputSchema = z.object({ + userId: z.string(), +}); +export type HeadSobjectsUserPasswordInput = z.infer< + typeof HeadSobjectsUserPasswordInputSchema +>; + +export const HeadSobjectsUserPasswordResponseSchema = z + .object({ + status: z.number().optional(), + }) + .passthrough(); +export type HeadSobjectsUserPasswordResponse = z.infer< + typeof HeadSobjectsUserPasswordResponseSchema +>; + +export const GetPicklistValuesByRecordTypeInputSchema = z.object({ + sobject: z.string(), + recordTypeId: z.string(), +}); +export type GetPicklistValuesByRecordTypeInput = z.infer< + typeof GetPicklistValuesByRecordTypeInputSchema +>; + +export const GetPicklistValuesByRecordTypeResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetPicklistValuesByRecordTypeResponse = z.infer< + typeof GetPicklistValuesByRecordTypeResponseSchema +>; + +export const GetAllFieldsForObjectInputSchema = z.object({ + sobject: z.string(), +}); +export type GetAllFieldsForObjectInput = z.infer< + typeof GetAllFieldsForObjectInputSchema +>; + +export const GetAllFieldsForObjectResponseSchema = z + .object({ + fields: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type GetAllFieldsForObjectResponse = z.infer< + typeof GetAllFieldsForObjectResponseSchema +>; + +export const GetAllCustomObjectsInputSchema = z.object({}); +export type GetAllCustomObjectsInput = z.infer< + typeof GetAllCustomObjectsInputSchema +>; + +export const GetAllCustomObjectsResponseSchema = z + .object({ + sobjects: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type GetAllCustomObjectsResponse = z.infer< + typeof GetAllCustomObjectsResponseSchema +>; + +export const GetSobjectsSobjectDescribeApprovallayoutsInputSchema = z.object({ + sobject: z.string(), +}); +export type GetSobjectsSobjectDescribeApprovallayoutsInput = z.infer< + typeof GetSobjectsSobjectDescribeApprovallayoutsInputSchema +>; + +export const GetSobjectsSobjectDescribeApprovallayoutsResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetSobjectsSobjectDescribeApprovallayoutsResponse = z.infer< + typeof GetSobjectsSobjectDescribeApprovallayoutsResponseSchema +>; + +export const GetSobjectApprovalLayoutsInputSchema = z.object({ + sobject: z.string(), +}); +export type GetSobjectApprovalLayoutsInput = z.infer< + typeof GetSobjectApprovalLayoutsInputSchema +>; + +export const GetSobjectApprovalLayoutsResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetSobjectApprovalLayoutsResponse = z.infer< + typeof GetSobjectApprovalLayoutsResponseSchema +>; + +export const GetChildRecordsInputSchema = z.object({ + parentId: z.string(), + relationshipName: z.string(), +}); +export type GetChildRecordsInput = z.infer; + +export const GetChildRecordsResponseSchema = z + .object({ + records: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type GetChildRecordsResponse = z.infer< + typeof GetChildRecordsResponseSchema +>; + +export const GetConsentActionInputSchema = z.object({ + action: z.string(), + ids: z.array(z.string()), +}); +export type GetConsentActionInput = z.infer; + +export const GetConsentActionResponseSchema = z.record(z.string(), z.unknown()); +export type GetConsentActionResponse = z.infer< + typeof GetConsentActionResponseSchema +>; + +export const HeadActionsCustomInputSchema = z.object({}); +export type HeadActionsCustomInput = z.infer< + typeof HeadActionsCustomInputSchema +>; + +export const HeadActionsCustomResponseSchema = z + .object({ + status: z.number().optional(), + }) + .passthrough(); +export type HeadActionsCustomResponse = z.infer< + typeof HeadActionsCustomResponseSchema +>; + +export const ListCustomInvocableActionsInputSchema = z.object({}); +export type ListCustomInvocableActionsInput = z.infer< + typeof ListCustomInvocableActionsInputSchema +>; + +export const ListCustomInvocableActionsResponseSchema = z + .object({ + actions: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type ListCustomInvocableActionsResponse = z.infer< + typeof ListCustomInvocableActionsResponseSchema +>; + +export const GetSupportedObjectsDirectoryInputSchema = z.object({}); +export type GetSupportedObjectsDirectoryInput = z.infer< + typeof GetSupportedObjectsDirectoryInputSchema +>; + +export const GetSupportedObjectsDirectoryResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetSupportedObjectsDirectoryResponse = z.infer< + typeof GetSupportedObjectsDirectoryResponseSchema +>; + +export const GetGlobalActionsInputSchema = z.object({}); +export type GetGlobalActionsInput = z.infer; + +export const GetGlobalActionsResponseSchema = z + .object({ + actions: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type GetGlobalActionsResponse = z.infer< + typeof GetGlobalActionsResponseSchema +>; + +export const HeadSobjectsGlobalDescribeLayoutsInputSchema = z.object({}); +export type HeadSobjectsGlobalDescribeLayoutsInput = z.infer< + typeof HeadSobjectsGlobalDescribeLayoutsInputSchema +>; + +export const HeadSobjectsGlobalDescribeLayoutsResponseSchema = z + .object({ + status: z.number().optional(), + }) + .passthrough(); +export type HeadSobjectsGlobalDescribeLayoutsResponse = z.infer< + typeof HeadSobjectsGlobalDescribeLayoutsResponseSchema +>; + +export const GetSObjectsDescribeLayoutsRecordTypeIdInputSchema = z.object({ + sobject: z.string(), + recordTypeId: z.string(), +}); +export type GetSObjectsDescribeLayoutsRecordTypeIdInput = z.infer< + typeof GetSObjectsDescribeLayoutsRecordTypeIdInputSchema +>; + +export const GetSObjectsDescribeLayoutsRecordTypeIdResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetSObjectsDescribeLayoutsRecordTypeIdResponse = z.infer< + typeof GetSObjectsDescribeLayoutsRecordTypeIdResponseSchema +>; + +export const GetOrgLimitsInputSchema = z.object({}); +export type GetOrgLimitsInput = z.infer; + +export const GetOrgLimitsResponseSchema = z.record(z.string(), z.unknown()); +export type GetOrgLimitsResponse = z.infer; + +export const HeadProcessRulesSObjectInputSchema = z.object({ + sobject: z.string(), +}); +export type HeadProcessRulesSObjectInput = z.infer< + typeof HeadProcessRulesSObjectInputSchema +>; + +export const HeadProcessRulesSObjectResponseSchema = z + .object({ + status: z.number().optional(), + }) + .passthrough(); +export type HeadProcessRulesSObjectResponse = z.infer< + typeof HeadProcessRulesSObjectResponseSchema +>; + +export const HeadSobjectQuickActionDefaultValuesInputSchema = z.object({ + sobject: z.string(), + actionName: z.string(), + contextId: z.string().optional(), +}); +export type HeadSobjectQuickActionDefaultValuesInput = z.infer< + typeof HeadSobjectQuickActionDefaultValuesInputSchema +>; + +export const HeadSobjectQuickActionDefaultValuesResponseSchema = z + .object({ + status: z.number().optional(), + }) + .passthrough(); +export type HeadSobjectQuickActionDefaultValuesResponse = z.infer< + typeof HeadSobjectQuickActionDefaultValuesResponseSchema +>; + +export const GetQuickActionsInputSchema = z.object({}); +export type GetQuickActionsInput = z.infer; + +export const GetQuickActionsResponseSchema = z + .object({ + actions: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type GetQuickActionsResponse = z.infer< + typeof GetQuickActionsResponseSchema +>; + +export const GetRecordCountsInputSchema = z.object({ + sobjects: z.array(z.string()), +}); +export type GetRecordCountsInput = z.infer; + +export const GetRecordCountsResponseSchema = z + .object({ + sObjects: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type GetRecordCountsResponse = z.infer< + typeof GetRecordCountsResponseSchema +>; + +export const GetSobjectRelationshipInputSchema = z.object({ + sobject: z.string(), + id: z.string(), + fieldName: z.string(), +}); +export type GetSobjectRelationshipInput = z.infer< + typeof GetSobjectRelationshipInputSchema +>; + +export const GetSobjectRelationshipResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetSobjectRelationshipResponse = z.infer< + typeof GetSobjectRelationshipResponseSchema +>; + +export const GetSobjectQuickActionDefaultValuesInputSchema = z.object({ + sobject: z.string(), + actionName: z.string(), +}); +export type GetSobjectQuickActionDefaultValuesInput = z.infer< + typeof GetSobjectQuickActionDefaultValuesInputSchema +>; + +export const GetSobjectQuickActionDefaultValuesResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetSobjectQuickActionDefaultValuesResponse = z.infer< + typeof GetSobjectQuickActionDefaultValuesResponseSchema +>; + +export const GetSObjectQuickActionDefaultValuesInputSchema = z.object({ + sobject: z.string(), + actionName: z.string(), + contextId: z.string().optional(), +}); +export type GetSObjectQuickActionDefaultValuesInput = z.infer< + typeof GetSObjectQuickActionDefaultValuesInputSchema +>; + +export const GetSObjectQuickActionDefaultValuesResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetSObjectQuickActionDefaultValuesResponse = z.infer< + typeof GetSObjectQuickActionDefaultValuesResponseSchema +>; + +export const GetSobjectByExternalIdInputSchema = z.object({ + sobject: z.string(), + fieldName: z.string(), + fieldValue: z.string(), +}); +export type GetSobjectByExternalIdInput = z.infer< + typeof GetSobjectByExternalIdInputSchema +>; + +export const GetSobjectByExternalIdResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetSobjectByExternalIdResponse = z.infer< + typeof GetSobjectByExternalIdResponseSchema +>; + +export const HeadSobjectsQuickActionInputSchema = z.object({ + sobject: z.string(), + actionName: z.string(), +}); +export type HeadSobjectsQuickActionInput = z.infer< + typeof HeadSobjectsQuickActionInputSchema +>; + +export const HeadSobjectsQuickActionResponseSchema = z + .object({ + status: z.number().optional(), + }) + .passthrough(); +export type HeadSobjectsQuickActionResponse = z.infer< + typeof HeadSobjectsQuickActionResponseSchema +>; + +export const GetSObjectRecordInputSchema = z.object({ + sobject: z.string(), + id: z.string(), + fields: z.array(z.string()).optional(), +}); +export type GetSObjectRecordInput = z.infer; + +export const GetSObjectRecordResponseSchema = z.record(z.string(), z.unknown()); +export type GetSObjectRecordResponse = z.infer< + typeof GetSObjectRecordResponseSchema +>; + +export const HeadActionsStandardInputSchema = z.object({}); +export type HeadActionsStandardInput = z.infer< + typeof HeadActionsStandardInputSchema +>; + +export const HeadActionsStandardResponseSchema = z + .object({ + status: z.number().optional(), + }) + .passthrough(); +export type HeadActionsStandardResponse = z.infer< + typeof HeadActionsStandardResponseSchema +>; + +export const ListStandardInvocableActionsInputSchema = z.object({}); +export type ListStandardInvocableActionsInput = z.infer< + typeof ListStandardInvocableActionsInputSchema +>; + +export const ListStandardInvocableActionsResponseSchema = z + .object({ + actions: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type ListStandardInvocableActionsResponse = z.infer< + typeof ListStandardInvocableActionsResponseSchema +>; + +export const GetSupportInputSchema = z.object({}); +export type GetSupportInput = z.infer; + +export const GetSupportResponseSchema = z.record(z.string(), z.unknown()); +export type GetSupportResponse = z.infer; + +export const GetSupportKnowledgeArticlesInputSchema = z.object({}); +export type GetSupportKnowledgeArticlesInput = z.infer< + typeof GetSupportKnowledgeArticlesInputSchema +>; + +export const GetSupportKnowledgeArticlesResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetSupportKnowledgeArticlesResponse = z.infer< + typeof GetSupportKnowledgeArticlesResponseSchema +>; + +export const GetThemeInputSchema = z.object({}); +export type GetThemeInput = z.infer; + +export const GetThemeResponseSchema = z.record(z.string(), z.unknown()); +export type GetThemeResponse = z.infer; + +export const GetSObjectsUpdatedInputSchema = z.object({ + sobject: z.string(), + start: z.string(), + end: z.string(), +}); +export type GetSObjectsUpdatedInput = z.infer< + typeof GetSObjectsUpdatedInputSchema +>; + +export const GetSObjectsUpdatedResponseSchema = z + .object({ + ids: z.array(z.string()), + latestDateCovered: z.string(), + }) + .passthrough(); +export type GetSObjectsUpdatedResponse = z.infer< + typeof GetSObjectsUpdatedResponseSchema +>; + +export const GetUserInfoInputSchema = z.object({ + userId: z.string().optional(), +}); +export type GetUserInfoInput = z.infer; + +export const GetUserInfoResponseSchema = z.record(z.string(), z.unknown()); +export type GetUserInfoResponse = z.infer; + +export const SobjectUserPasswordInputSchema = z.object({ + userId: z.string(), +}); +export type SobjectUserPasswordInput = z.infer< + typeof SobjectUserPasswordInputSchema +>; + +export const SobjectUserPasswordResponseSchema = z + .object({ + isExpired: z.boolean().optional(), + }) + .passthrough(); +export type SobjectUserPasswordResponse = z.infer< + typeof SobjectUserPasswordResponseSchema +>; + +export const MassTransferOwnershipInputSchema = z.object({ + sobject: z.string(), + fromUserId: z.string(), + toUserId: z.string(), + recordIds: z.array(z.string()).optional(), +}); +export type MassTransferOwnershipInput = z.infer< + typeof MassTransferOwnershipInputSchema +>; + +export const MassTransferOwnershipResponseSchema = z + .object({ + success: z.boolean(), + transferred: z.number().optional(), + failed: z + .array( + z.object({ + id: z.string().optional(), + errors: z.unknown().optional(), + }), + ) + .optional(), + }) + .passthrough(); +export type MassTransferOwnershipResponse = z.infer< + typeof MassTransferOwnershipResponseSchema +>; + +// UI API +export const CreateARecordInputSchema = z.object({ + apiName: z.string(), + fields: z.record(z.string(), z.unknown()), +}); +export type CreateARecordInput = z.infer; + +export const CreateARecordResponseSchema = z + .object({ + id: z.string(), + apiName: z.string().optional(), + }) + .passthrough(); +export type CreateARecordResponse = z.infer; + +export const CreateRecordUiApiInputSchema = z.object({ + apiName: z.string(), + fields: z.record(z.string(), z.unknown()), +}); +export type CreateRecordUiApiInput = z.infer< + typeof CreateRecordUiApiInputSchema +>; + +export const CreateRecordUiApiResponseSchema = z + .object({ + id: z.string(), + apiName: z.string().optional(), + }) + .passthrough(); +export type CreateRecordUiApiResponse = z.infer< + typeof CreateRecordUiApiResponseSchema +>; + +export const GetUiapiListInfoAccountAllAccountsInputSchema = z.object({}); +export type GetUiapiListInfoAccountAllAccountsInput = z.infer< + typeof GetUiapiListInfoAccountAllAccountsInputSchema +>; + +export const GetUiapiListInfoAccountAllAccountsResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetUiapiListInfoAccountAllAccountsResponse = z.infer< + typeof GetUiapiListInfoAccountAllAccountsResponseSchema +>; + +export const GetUiapiListInfoAccountSearchResultInputSchema = z.object({}); +export type GetUiapiListInfoAccountSearchResultInput = z.infer< + typeof GetUiapiListInfoAccountSearchResultInputSchema +>; + +export const GetUiapiListInfoAccountSearchResultResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetUiapiListInfoAccountSearchResultResponse = z.infer< + typeof GetUiapiListInfoAccountSearchResultResponseSchema +>; + +export const HeadAppmenuSalesforce1InputSchema = z.object({}); +export type HeadAppmenuSalesforce1Input = z.infer< + typeof HeadAppmenuSalesforce1InputSchema +>; + +export const HeadAppmenuSalesforce1ResponseSchema = z + .object({ + status: z.number().optional(), + }) + .passthrough(); +export type HeadAppmenuSalesforce1Response = z.infer< + typeof HeadAppmenuSalesforce1ResponseSchema +>; + +export const GetCompactLayoutsInputSchema = z.object({ + sobjects: z.array(z.string()), +}); +export type GetCompactLayoutsInput = z.infer< + typeof GetCompactLayoutsInputSchema +>; + +export const GetCompactLayoutsResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetCompactLayoutsResponse = z.infer< + typeof GetCompactLayoutsResponseSchema +>; + +export const GetListViewActionsInputSchema = z.object({ + sobject: z.string(), +}); +export type GetListViewActionsInput = z.infer< + typeof GetListViewActionsInputSchema +>; + +export const GetListViewActionsResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetListViewActionsResponse = z.infer< + typeof GetListViewActionsResponseSchema +>; + +export const GetUiapiListInfoAccountRecentInputSchema = z.object({}); +export type GetUiapiListInfoAccountRecentInput = z.infer< + typeof GetUiapiListInfoAccountRecentInputSchema +>; + +export const GetUiapiListInfoAccountRecentResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetUiapiListInfoAccountRecentResponse = z.infer< + typeof GetUiapiListInfoAccountRecentResponseSchema +>; + +export const GetUiApiListInfoRecentInputSchema = z.object({ + sobject: z.string(), +}); +export type GetUiApiListInfoRecentInput = z.infer< + typeof GetUiApiListInfoRecentInputSchema +>; + +export const GetUiApiListInfoRecentResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetUiApiListInfoRecentResponse = z.infer< + typeof GetUiApiListInfoRecentResponseSchema +>; + +export const GetUiapimruListInfoAccountInputSchema = z.object({}); +export type GetUiapimruListInfoAccountInput = z.infer< + typeof GetUiapimruListInfoAccountInputSchema +>; + +export const GetUiapimruListInfoAccountResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetUiapimruListInfoAccountResponse = z.infer< + typeof GetUiapimruListInfoAccountResponseSchema +>; + +export const GetUiApiMruListRecordsAccountInputSchema = z.object({}); +export type GetUiApiMruListRecordsAccountInput = z.infer< + typeof GetUiApiMruListRecordsAccountInputSchema +>; + +export const GetUiApiMruListRecordsAccountResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetUiApiMruListRecordsAccountResponse = z.infer< + typeof GetUiApiMruListRecordsAccountResponseSchema +>; + +export const GetUiapiActionsMruListAccountInputSchema = z.object({}); +export type GetUiapiActionsMruListAccountInput = z.infer< + typeof GetUiapiActionsMruListAccountInputSchema +>; + +export const GetUiapiActionsMruListAccountResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetUiapiActionsMruListAccountResponse = z.infer< + typeof GetUiapiActionsMruListAccountResponseSchema +>; + +export const GetMruListViewMetadataInputSchema = z.object({ + sobject: z.string(), +}); +export type GetMruListViewMetadataInput = z.infer< + typeof GetMruListViewMetadataInputSchema +>; + +export const GetMruListViewMetadataResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetMruListViewMetadataResponse = z.infer< + typeof GetMruListViewMetadataResponseSchema +>; + +export const GetUiApiAppsUserNavItemsInputSchema = z.object({ + appId: z.string().optional(), +}); +export type GetUiApiAppsUserNavItemsInput = z.infer< + typeof GetUiApiAppsUserNavItemsInputSchema +>; + +export const GetUiApiAppsUserNavItemsResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetUiApiAppsUserNavItemsResponse = z.infer< + typeof GetUiApiAppsUserNavItemsResponseSchema +>; + +export const GetAllNavigationItemsInputSchema = z.object({}); +export type GetAllNavigationItemsInput = z.infer< + typeof GetAllNavigationItemsInputSchema +>; + +export const GetAllNavigationItemsResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetAllNavigationItemsResponse = z.infer< + typeof GetAllNavigationItemsResponseSchema +>; + +export const GetAppInputSchema = z.object({ + appId: z.string(), +}); +export type GetAppInput = z.infer; + +export const GetAppResponseSchema = z.record(z.string(), z.unknown()); +export type GetAppResponse = z.infer; + +export const GetAppsInputSchema = z.object({}); +export type GetAppsInput = z.infer; + +export const GetAppsResponseSchema = z.record(z.string(), z.unknown()); +export type GetAppsResponse = z.infer; + +export const GetListViewMetadataBatchInputSchema = z.object({ + listViewIds: z.array(z.string()), +}); +export type GetListViewMetadataBatchInput = z.infer< + typeof GetListViewMetadataBatchInputSchema +>; + +export const GetListViewMetadataBatchResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetListViewMetadataBatchResponse = z.infer< + typeof GetListViewMetadataBatchResponseSchema +>; + +export const GetRelatedListPreferencesBatchInputSchema = z.object({ + relatedListIds: z.array(z.string()), +}); +export type GetRelatedListPreferencesBatchInput = z.infer< + typeof GetRelatedListPreferencesBatchInputSchema +>; + +export const GetRelatedListPreferencesBatchResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetRelatedListPreferencesBatchResponse = z.infer< + typeof GetRelatedListPreferencesBatchResponseSchema +>; + +export const GetLastSelectedAppInputSchema = z.object({}); +export type GetLastSelectedAppInput = z.infer< + typeof GetLastSelectedAppInputSchema +>; + +export const GetLastSelectedAppResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetLastSelectedAppResponse = z.infer< + typeof GetLastSelectedAppResponseSchema +>; + +export const GetListViewMetadataByNameInputSchema = z.object({ + sobject: z.string(), + listViewName: z.string(), +}); +export type GetListViewMetadataByNameInput = z.infer< + typeof GetListViewMetadataByNameInputSchema +>; + +export const GetListViewMetadataByNameResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetListViewMetadataByNameResponse = z.infer< + typeof GetListViewMetadataByNameResponseSchema +>; + +export const GetListViewRecordsByNameInputSchema = z.object({ + sobject: z.string(), + listViewName: z.string(), +}); +export type GetListViewRecordsByNameInput = z.infer< + typeof GetListViewRecordsByNameInputSchema +>; + +export const GetListViewRecordsByNameResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetListViewRecordsByNameResponse = z.infer< + typeof GetListViewRecordsByNameResponseSchema +>; + +export const GetListViewRecordsByIdInputSchema = z.object({ + listViewId: z.string(), +}); +export type GetListViewRecordsByIdInput = z.infer< + typeof GetListViewRecordsByIdInputSchema +>; + +export const GetListViewRecordsByIdResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetListViewRecordsByIdResponse = z.infer< + typeof GetListViewRecordsByIdResponseSchema +>; + +export const ListViewResultsInputSchema = z.object({ + listViewId: z.string(), +}); +export type ListViewResultsInput = z.infer; + +export const ListViewResultsResponseSchema = z.record(z.string(), z.unknown()); +export type ListViewResultsResponse = z.infer< + typeof ListViewResultsResponseSchema +>; + +export const GetListViewResultsInputSchema = z.object({ + sobject: z.string(), + listViewId: z.string(), +}); +export type GetListViewResultsInput = z.infer< + typeof GetListViewResultsInputSchema +>; + +export const GetListViewResultsResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetListViewResultsResponse = z.infer< + typeof GetListViewResultsResponseSchema +>; + +export const GetObjectListViewsInputSchema = z.object({ + sobject: z.string(), +}); +export type GetObjectListViewsInput = z.infer< + typeof GetObjectListViewsInputSchema +>; + +export const GetObjectListViewsResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetObjectListViewsResponse = z.infer< + typeof GetObjectListViewsResponseSchema +>; + +export const GetSobjectListViewsInputSchema = z.object({ + sobject: z.string(), +}); +export type GetSobjectListViewsInput = z.infer< + typeof GetSobjectListViewsInputSchema +>; + +export const GetSobjectListViewsResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetSobjectListViewsResponse = z.infer< + typeof GetSobjectListViewsResponseSchema +>; + +export const GetUiApiActionsLookupAccountInputSchema = z.object({}); +export type GetUiApiActionsLookupAccountInput = z.infer< + typeof GetUiApiActionsLookupAccountInputSchema +>; + +export const GetUiApiActionsLookupAccountResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetUiApiActionsLookupAccountResponse = z.infer< + typeof GetUiApiActionsLookupAccountResponseSchema +>; + +export const GetUiapiLookupsOpportunityAccountIdInputSchema = z.object({}); +export type GetUiapiLookupsOpportunityAccountIdInput = z.infer< + typeof GetUiapiLookupsOpportunityAccountIdInputSchema +>; + +export const GetUiapiLookupsOpportunityAccountIdResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetUiapiLookupsOpportunityAccountIdResponse = z.infer< + typeof GetUiapiLookupsOpportunityAccountIdResponseSchema +>; + +export const GetLookupFieldSuggestionsInputSchema = z.object({ + sobject: z.string(), + field: z.string(), + q: z.string().optional(), +}); +export type GetLookupFieldSuggestionsInput = z.infer< + typeof GetLookupFieldSuggestionsInputSchema +>; + +export const GetLookupFieldSuggestionsResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetLookupFieldSuggestionsResponse = z.infer< + typeof GetLookupFieldSuggestionsResponseSchema +>; + +export const GetLookupSuggestionsOpportunityAccountInputSchema = z.object({ + q: z.string().optional(), +}); +export type GetLookupSuggestionsOpportunityAccountInput = z.infer< + typeof GetLookupSuggestionsOpportunityAccountInputSchema +>; + +export const GetLookupSuggestionsOpportunityAccountResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetLookupSuggestionsOpportunityAccountResponse = z.infer< + typeof GetLookupSuggestionsOpportunityAccountResponseSchema +>; + +export const GetLookupSuggestionsCaseContactInputSchema = z.object({ + q: z.string().optional(), +}); +export type GetLookupSuggestionsCaseContactInput = z.infer< + typeof GetLookupSuggestionsCaseContactInputSchema +>; + +export const GetLookupSuggestionsCaseContactResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetLookupSuggestionsCaseContactResponse = z.infer< + typeof GetLookupSuggestionsCaseContactResponseSchema +>; + +export const GetMruListViewRecordsInputSchema = z.object({ + sobject: z.string(), +}); +export type GetMruListViewRecordsInput = z.infer< + typeof GetMruListViewRecordsInputSchema +>; + +export const GetMruListViewRecordsResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetMruListViewRecordsResponse = z.infer< + typeof GetMruListViewRecordsResponseSchema +>; + +export const GetPhotoActionsInputSchema = z.object({ + pageId: z.string().optional(), +}); +export type GetPhotoActionsInput = z.infer; + +export const GetPhotoActionsResponseSchema = z.record(z.string(), z.unknown()); +export type GetPhotoActionsResponse = z.infer< + typeof GetPhotoActionsResponseSchema +>; + +export const GetRecordUiDataAndMetadataInputSchema = z.object({ + recordId: z.string(), +}); +export type GetRecordUiDataAndMetadataInput = z.infer< + typeof GetRecordUiDataAndMetadataInputSchema +>; + +export const GetRecordUiDataAndMetadataResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetRecordUiDataAndMetadataResponse = z.infer< + typeof GetRecordUiDataAndMetadataResponseSchema +>; + +export const GetRecordEditPageActionsInputSchema = z.object({ + sobject: z.string(), + recordId: z.string().optional(), +}); +export type GetRecordEditPageActionsInput = z.infer< + typeof GetRecordEditPageActionsInputSchema +>; + +export const GetRecordEditPageActionsResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetRecordEditPageActionsResponse = z.infer< + typeof GetRecordEditPageActionsResponseSchema +>; + +export const GetUiApiActionsRecordRelatedListInputSchema = z.object({ + parentRecordId: z.string(), + relationshipName: z.string(), +}); +export type GetUiApiActionsRecordRelatedListInput = z.infer< + typeof GetUiApiActionsRecordRelatedListInputSchema +>; + +export const GetUiApiActionsRecordRelatedListResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetUiApiActionsRecordRelatedListResponse = z.infer< + typeof GetUiApiActionsRecordRelatedListResponseSchema +>; + +export const GetRelatedListActionsInputSchema = z.object({ + parentRecordId: z.string(), + relationshipName: z.string(), +}); +export type GetRelatedListActionsInput = z.infer< + typeof GetRelatedListActionsInputSchema +>; + +export const GetRelatedListActionsResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetRelatedListActionsResponse = z.infer< + typeof GetRelatedListActionsResponseSchema +>; + +export const GetRelatedListRecordsContactsInputSchema = z.object({ + parentRecordId: z.string(), +}); +export type GetRelatedListRecordsContactsInput = z.infer< + typeof GetRelatedListRecordsContactsInputSchema +>; + +export const GetRelatedListRecordsContactsResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetRelatedListRecordsContactsResponse = z.infer< + typeof GetRelatedListRecordsContactsResponseSchema +>; + +export const GetUiapiRelatedListPreferencesInputSchema = z.object({ + parentRecordId: z.string(), + relationshipName: z.string(), +}); +export type GetUiapiRelatedListPreferencesInput = z.infer< + typeof GetUiapiRelatedListPreferencesInputSchema +>; + +export const GetUiapiRelatedListPreferencesResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetUiapiRelatedListPreferencesResponse = z.infer< + typeof GetUiapiRelatedListPreferencesResponseSchema +>; + +export const GetSobjectListViewInputSchema = z.object({ + sobject: z.string(), + listViewId: z.string(), +}); +export type GetSobjectListViewInput = z.infer< + typeof GetSobjectListViewInputSchema +>; + +export const GetSobjectListViewResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetSobjectListViewResponse = z.infer< + typeof GetSobjectListViewResponseSchema +>; + +// Files +export const GetFileContentInputSchema = z.object({ + fileId: z.string(), +}); +export type GetFileContentInput = z.infer; + +export const GetFileContentResponseSchema = z + .object({ + content: z.string(), + }) + .passthrough(); +export type GetFileContentResponse = z.infer< + typeof GetFileContentResponseSchema +>; + +export const GetFileInformationInputSchema = z.object({ + fileId: z.string(), +}); +export type GetFileInformationInput = z.infer< + typeof GetFileInformationInputSchema +>; + +export const GetFileInformationResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetFileInformationResponse = z.infer< + typeof GetFileInformationResponseSchema +>; + +export const GetFileSharesInputSchema = z.object({ + fileId: z.string(), +}); +export type GetFileSharesInput = z.infer; + +export const GetFileSharesResponseSchema = z + .object({ + shares: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type GetFileSharesResponse = z.infer; + +export const DeleteFileInputSchema = z.object({ + fileId: z.string(), +}); +export type DeleteFileInput = z.infer; + +export const DeleteFileResponseSchema = z + .object({ + success: z.boolean(), + }) + .passthrough(); +export type DeleteFileResponse = z.infer; + +// Analytics & Reports +export const GetDashboardInputSchema = z.object({ + dashboardId: z.string(), +}); +export type GetDashboardInput = z.infer; + +export const GetDashboardResponseSchema = z.record(z.string(), z.unknown()); +export type GetDashboardResponse = z.infer; + +export const ListDashboardsInputSchema = z.object({}); +export type ListDashboardsInput = z.infer; + +export const ListDashboardsResponseSchema = z + .object({ + dashboards: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type ListDashboardsResponse = z.infer< + typeof ListDashboardsResponseSchema +>; + +export const ListEmailTemplatesInputSchema = z.object({ + name: z.string().optional(), + developerName: z.string().optional(), + folderId: z.string().optional(), +}); +export type ListEmailTemplatesInput = z.infer< + typeof ListEmailTemplatesInputSchema +>; + +export const ListEmailTemplatesResponseSchema = z + .object({ + templates: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type ListEmailTemplatesResponse = z.infer< + typeof ListEmailTemplatesResponseSchema +>; + +export const ListReportsInputSchema = z.object({}); +export type ListReportsInput = z.infer; + +export const ListReportsResponseSchema = z + .object({ + reports: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type ListReportsResponse = z.infer; + +export const RunReportInputSchema = z.object({ + reportId: z.string(), +}); +export type RunReportInput = z.infer; + +export const RunReportResponseSchema = z.record(z.string(), z.unknown()); +export type RunReportResponse = z.infer; + +export const ListAnalyticsTemplatesInputSchema = z.object({}); +export type ListAnalyticsTemplatesInput = z.infer< + typeof ListAnalyticsTemplatesInputSchema +>; + +export const ListAnalyticsTemplatesResponseSchema = z + .object({ + templates: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +export type ListAnalyticsTemplatesResponse = z.infer< + typeof ListAnalyticsTemplatesResponseSchema +>; + +export const GetReportInstanceInputSchema = z.object({ + reportId: z.string(), + instanceId: z.string(), +}); +export type GetReportInstanceInput = z.infer< + typeof GetReportInstanceInputSchema +>; + +export const GetReportInstanceResponseSchema = z.record( + z.string(), + z.unknown(), +); +export type GetReportInstanceResponse = z.infer< + typeof GetReportInstanceResponseSchema +>; + +export const GetReportInputSchema = z.object({ + reportId: z.string(), +}); +export type GetReportInput = z.infer; + +export const GetReportResponseSchema = z.record(z.string(), z.unknown()); +export type GetReportResponse = z.infer; + +export const QueryReportInputSchema = z.object({ + id: z.string(), + reportType: z.string().optional(), +}); +export type QueryReportInput = z.infer; + +export const QueryReportResponseSchema = z.record(z.string(), z.unknown()); +export type QueryReportResponse = z.infer; + +const SuccessResponseSchema = z.object({ success: z.boolean() }).passthrough(); +const RecordsResponseSchema = z + .object({ records: z.array(z.record(z.string(), z.unknown())) }) + .passthrough(); +const ResultResponseSchema = z + .object({ result: z.unknown().optional() }) + .passthrough(); + +export const UpdateAccountInputSchema = + CreateAccountInputSchema.partial().extend({ + id: z.string(), + }); +export type UpdateAccountInput = z.infer; +export const UpdateAccountResponseSchema = SuccessResponseSchema; +export type UpdateAccountResponse = z.infer; +export const UpdateAccountObjectByIdInputSchema = UpdateAccountInputSchema; +export const UpdateAccountObjectByIdResponseSchema = SuccessResponseSchema; + +export const UpdateContactInputSchema = + CreateContactInputSchema.partial().extend({ + id: z.string(), + }); +export type UpdateContactInput = z.infer; +export const UpdateContactResponseSchema = SuccessResponseSchema; +export const UpdateContactByIdInputSchema = UpdateContactInputSchema; +export const UpdateContactByIdResponseSchema = SuccessResponseSchema; + +export const SearchContactsInputSchema = z.object({ + name: z.string().optional(), + email: z.string().optional(), + phone: z.string().optional(), + accountId: z.string().optional(), + title: z.string().optional(), + limit: z.number().optional(), +}); +export type SearchContactsInput = z.infer; +export const SearchContactsResponseSchema = RecordsResponseSchema; + +export const UpdateLeadInputSchema = CreateLeadInputSchema.partial().extend({ + id: z.string(), +}); +export type UpdateLeadInput = z.infer; +export const UpdateLeadResponseSchema = SuccessResponseSchema; +export const UpdateLeadByIdWithJsonPayloadInputSchema = UpdateLeadInputSchema; +export const UpdateLeadByIdWithJsonPayloadResponseSchema = + SuccessResponseSchema; + +export const SearchLeadsInputSchema = z.object({ + name: z.string().optional(), + email: z.string().optional(), + phone: z.string().optional(), + company: z.string().optional(), + status: z.string().optional(), + title: z.string().optional(), + limit: z.number().optional(), +}); +export type SearchLeadsInput = z.infer; +export const SearchLeadsResponseSchema = RecordsResponseSchema; + +export const UpdateOpportunityInputSchema = + CreateOpportunityInputSchema.partial().extend({ + id: z.string(), + }); +export type UpdateOpportunityInput = z.infer< + typeof UpdateOpportunityInputSchema +>; +export const UpdateOpportunityResponseSchema = SuccessResponseSchema; +export const UpdateOpportunityByIdInputSchema = UpdateOpportunityInputSchema; +export const UpdateOpportunityByIdResponseSchema = SuccessResponseSchema; + +export const SearchOpportunitiesInputSchema = z.object({ + name: z.string().optional(), + accountId: z.string().optional(), + stageName: z.string().optional(), + isClosed: z.boolean().optional(), + limit: z.number().optional(), +}); +export type SearchOpportunitiesInput = z.infer< + typeof SearchOpportunitiesInputSchema +>; +export const SearchOpportunitiesResponseSchema = RecordsResponseSchema; + +export const UpdateCampaignInputSchema = + CreateCampaignInputSchema.partial().extend({ + id: z.string(), + }); +export type UpdateCampaignInput = z.infer; +export const UpdateCampaignResponseSchema = SuccessResponseSchema; +export const UpdateCampaignByIdWithJsonInputSchema = UpdateCampaignInputSchema; +export const UpdateCampaignByIdWithJsonResponseSchema = SuccessResponseSchema; + +export const UpdateNoteInputSchema = CreateNoteInputSchema.partial().extend({ + id: z.string(), +}); +export type UpdateNoteInput = z.infer; +export const UpdateNoteResponseSchema = SuccessResponseSchema; +export const UpdateSpecificNoteByIdInputSchema = UpdateNoteInputSchema; +export const UpdateSpecificNoteByIdResponseSchema = SuccessResponseSchema; + +export const SearchNotesInputSchema = z.object({ + title: z.string().optional(), + body: z.string().optional(), + parentId: z.string().optional(), + limit: z.number().optional(), +}); +export type SearchNotesInput = z.infer; +export const SearchNotesResponseSchema = RecordsResponseSchema; + +export const UpdateTaskInputSchema = CreateTaskInputSchema.partial().extend({ + id: z.string(), +}); +export type UpdateTaskInput = z.infer; +export const UpdateTaskResponseSchema = SuccessResponseSchema; + +export const SearchTasksInputSchema = z.object({ + subject: z.string().optional(), + status: z.string().optional(), + priority: z.string().optional(), + whoId: z.string().optional(), + whatId: z.string().optional(), + limit: z.number().optional(), +}); +export type SearchTasksInput = z.infer; +export const SearchTasksResponseSchema = RecordsResponseSchema; + +export const SendEmailInputSchema = z.object({ + toAddresses: z.array(z.string()).optional(), + subject: z.string().optional(), + body: z.string().optional(), + senderType: z.string().optional(), +}); +export type SendEmailInput = z.infer; +export const SendEmailResponseSchema = ResultResponseSchema; + +export const SendEmailFromTemplateInputSchema = z.object({ + toAddresses: z.array(z.string()).optional(), + templateId: z.string(), + targetObjectId: z.string().optional(), + senderType: z.string().optional(), +}); +export type SendEmailFromTemplateInput = z.infer< + typeof SendEmailFromTemplateInputSchema +>; +export const SendEmailFromTemplateResponseSchema = ResultResponseSchema; + +export const SendMassEmailInputSchema = z.object({ + toAddresses: z.array(z.string()).optional(), + subject: z.string().optional(), + body: z.string().optional(), + templateId: z.string().optional(), +}); +export type SendMassEmailInput = z.infer; +export const SendMassEmailResponseSchema = ResultResponseSchema; + +export const UploadFileInputSchema = z.object({ + title: z.string(), + versionData: z.string(), + pathOnClient: z.string().optional(), + firstPublishLocationId: z.string().optional(), +}); +export type UploadFileInput = z.infer; +export const UploadFileResponseSchema = z + .object({ id: z.string(), success: z.boolean().optional() }) + .passthrough(); + +export const UploadJobDataInputSchema = z.object({ + jobId: z.string(), + csv: z.string(), +}); +export type UploadJobDataInput = z.infer; +export const UploadJobDataResponseSchema = SuccessResponseSchema; + +export const PatchCompositeSobjectsInputSchema = z.object({ + allOrNone: z.boolean().optional(), + records: z.array(z.record(z.string(), z.unknown())), +}); +export type PatchCompositeSobjectsInput = z.infer< + typeof PatchCompositeSobjectsInputSchema +>; +export const PatchCompositeSobjectsResponseSchema = ResultResponseSchema; + +export const UpdateSobjectInputSchema = z.object({ + sobject: z.string(), + id: z.string(), + fields: z.record(z.string(), z.unknown()), +}); +export type UpdateSobjectInput = z.infer; +export const UpdateSobjectResponseSchema = SuccessResponseSchema; +export const SobjectRowsUpdateInputSchema = UpdateSobjectInputSchema; +export const SobjectRowsUpdateResponseSchema = SuccessResponseSchema; + +export const UpsertSobjectByExternalIdInputSchema = z.object({ + sobject: z.string(), + fieldName: z.string(), + fieldValue: z.string(), + fields: z.record(z.string(), z.unknown()), +}); +export type UpsertSobjectByExternalIdInput = z.infer< + typeof UpsertSobjectByExternalIdInputSchema +>; +export const UpsertSobjectByExternalIdResponseSchema = z + .object({ + id: z.string().optional(), + created: z.boolean().optional(), + success: z.boolean().optional(), + }) + .passthrough(); + +export const SetUserPasswordInputSchema = z.object({ + userId: z.string(), + password: z.string().optional(), +}); +export type SetUserPasswordInput = z.infer; +export const SetUserPasswordResponseSchema = ResultResponseSchema; + +export const GetSearchSuggestionsInputSchema = z.object({ + q: z.string(), + sobject: z.string().optional(), +}); +export type GetSearchSuggestionsInput = z.infer< + typeof GetSearchSuggestionsInputSchema +>; +export const GetSearchSuggestionsResponseSchema = ResultResponseSchema; + +export const SearchKnowledgeArticlesInputSchema = z.object({ + q: z.string(), +}); +export type SearchKnowledgeArticlesInput = z.infer< + typeof SearchKnowledgeArticlesInputSchema +>; +export const SearchKnowledgeArticlesResponseSchema = ResultResponseSchema; + +export const GetParameterizedSearchInputSchema = ParameterizedSearchInputSchema; +export const GetParameterizedSearchResponseSchema = + ParameterizedSearchResponseSchema; + +export const UpdateRecordInputSchema = z.object({ + recordId: z.string(), + apiName: z.string(), + fields: z.record(z.string(), z.unknown()), + ifUnmodifiedSince: z.string().optional(), +}); +export type UpdateRecordInput = z.infer; +export const UpdateRecordResponseSchema = SuccessResponseSchema; + +export const UpdateFavoriteInputSchema = z.object({ + favoriteId: z.string(), + fields: z.record(z.string(), z.unknown()), +}); +export type UpdateFavoriteInput = z.infer; +export const UpdateFavoriteResponseSchema = ResultResponseSchema; + +export const UpdateRelatedListPreferencesInputSchema = z.object({ + relatedListId: z.string(), + preferences: z.record(z.string(), z.unknown()), +}); +export type UpdateRelatedListPreferencesInput = z.infer< + typeof UpdateRelatedListPreferencesInputSchema +>; +export const UpdateRelatedListPreferencesResponseSchema = ResultResponseSchema; + +export const UpdateListViewPreferencesInputSchema = z.object({ + sobject: z.string(), + listViewId: z.string(), + preferences: z.record(z.string(), z.unknown()), +}); +export type UpdateListViewPreferencesInput = z.infer< + typeof UpdateListViewPreferencesInputSchema +>; +export const UpdateListViewPreferencesResponseSchema = ResultResponseSchema; + +// Map Objects +export const SalesforceEndpointInputSchemas = { + // Accounts + createAccount: CreateAccountInputSchema, + getAccount: GetAccountInputSchema, + listAccounts: ListAccountsInputSchema, + searchAccounts: SearchAccountsInputSchema, + updateAccount: UpdateAccountInputSchema, + updateAccountObjectById: UpdateAccountObjectByIdInputSchema, + deleteAccount: DeleteAccountInputSchema, + accountCreationWithContentTypeOption: + AccountCreationWithContentTypeOptionInputSchema, + fetchAccountByIdWithQuery: FetchAccountByIdWithQueryInputSchema, + removeAccountByUniqueIdentifier: RemoveAccountByUniqueIdentifierInputSchema, + retrieveAccountDataAndErrorResponses: + RetrieveAccountDataAndErrorResponsesInputSchema, + + // Contacts + createContact: CreateContactInputSchema, + getContact: GetContactInputSchema, + listContacts: ListContactsInputSchema, + deleteContact: DeleteContactInputSchema, + associateContactToAccount: AssociateContactToAccountInputSchema, + updateContact: UpdateContactInputSchema, + updateContactById: UpdateContactByIdInputSchema, + searchContacts: SearchContactsInputSchema, + createNewContactWithJsonHeader: CreateNewContactWithJsonHeaderInputSchema, + queryContactsByName: QueryContactsByNameInputSchema, + removeASpecificContactById: RemoveASpecificContactByIdInputSchema, + retrieveContactInfoWithStandardResponses: + RetrieveContactInfoWithStandardResponsesInputSchema, + getContactById: GetContactByIdInputSchema, + + // Leads + createLead: CreateLeadInputSchema, + getLead: GetLeadInputSchema, + listLeads: ListLeadsInputSchema, + deleteLead: DeleteLeadInputSchema, + applyLeadAssignmentRules: ApplyLeadAssignmentRulesInputSchema, + updateLead: UpdateLeadInputSchema, + updateLeadByIdWithJsonPayload: UpdateLeadByIdWithJsonPayloadInputSchema, + searchLeads: SearchLeadsInputSchema, + createLeadWithSpecifiedContentType: + CreateLeadWithSpecifiedContentTypeInputSchema, + deleteALeadObjectByItsId: DeleteALeadObjectByItsIdInputSchema, + retrieveLeadById: RetrieveLeadByIdInputSchema, + retrieveLeadDataWithVariousResponses: + RetrieveLeadDataWithVariousResponsesInputSchema, + + // Opportunities + createOpportunity: CreateOpportunityInputSchema, + getOpportunity: GetOpportunityInputSchema, + listOpportunities: ListOpportunitiesInputSchema, + deleteOpportunity: DeleteOpportunityInputSchema, + addOpportunityLineItem: AddOpportunityLineItemInputSchema, + updateOpportunity: UpdateOpportunityInputSchema, + updateOpportunityById: UpdateOpportunityByIdInputSchema, + searchOpportunities: SearchOpportunitiesInputSchema, + cloneOpportunityWithProducts: CloneOpportunityWithProductsInputSchema, + listPricebookEntries: ListPricebookEntriesInputSchema, + listPricebooks: ListPricebooksInputSchema, + createOpportunityRecord: CreateOpportunityRecordInputSchema, + removeOpportunityById: RemoveOpportunityByIdInputSchema, + retrieveOpportunitiesData: RetrieveOpportunitiesDataInputSchema, + retrieveOpportunityByIdWithOptionalFields: + RetrieveOpportunityByIdWithOptionalFieldsInputSchema, + + // Campaigns + createCampaign: CreateCampaignInputSchema, + getCampaign: GetCampaignInputSchema, + listCampaigns: ListCampaignsInputSchema, + deleteCampaign: DeleteCampaignInputSchema, + addContactToCampaign: AddContactToCampaignInputSchema, + updateCampaign: UpdateCampaignInputSchema, + updateCampaignByIdWithJson: UpdateCampaignByIdWithJsonInputSchema, + addLeadToCampaign: AddLeadToCampaignInputSchema, + removeFromCampaign: RemoveFromCampaignInputSchema, + searchCampaigns: SearchCampaignsInputSchema, + createCampaignRecordViaPost: CreateCampaignRecordViaPostInputSchema, + removeCampaignObjectById: RemoveCampaignObjectByIdInputSchema, + retrieveCampaignDataWithErrorHandling: + RetrieveCampaignDataWithErrorHandlingInputSchema, + retrieveSpecificCampaignObjectDetails: + RetrieveSpecificCampaignObjectDetailsInputSchema, + + // Notes + createNote: CreateNoteInputSchema, + updateNote: UpdateNoteInputSchema, + updateSpecificNoteById: UpdateSpecificNoteByIdInputSchema, + searchNotes: SearchNotesInputSchema, + getNote: GetNoteInputSchema, + listNotes: ListNotesInputSchema, + deleteNote: DeleteNoteInputSchema, + createNoteRecordWithContentTypeHeader: + CreateNoteRecordWithContentTypeHeaderInputSchema, + removeNoteObjectById: RemoveNoteObjectByIdInputSchema, + getNoteByIdWithFields: GetNoteByIdWithFieldsInputSchema, + retrieveNoteObjectInformation: RetrieveNoteObjectInformationInputSchema, + + // Tasks + createTask: CreateTaskInputSchema, + completeTask: CompleteTaskInputSchema, + logCall: LogCallInputSchema, + logEmailActivity: LogEmailActivityInputSchema, + updateTask: UpdateTaskInputSchema, + searchTasks: SearchTasksInputSchema, + sendEmail: SendEmailInputSchema, + sendEmailFromTemplate: SendEmailFromTemplateInputSchema, + sendMassEmail: SendMassEmailInputSchema, + + // Jobs + closeOrAbortJob: CloseOrAbortJobInputSchema, + deleteJobQuery: DeleteJobQueryInputSchema, + getJobFailedRecordResults: GetJobFailedRecordResultsInputSchema, + getQueryJobInfo: GetQueryJobInfoInputSchema, + getQueryJobResults: GetQueryJobResultsInputSchema, + getJobSuccessfulRecordResults: GetJobSuccessfulRecordResultsInputSchema, + getJobUnprocessedRecordResults: GetJobUnprocessedRecordResultsInputSchema, + uploadJobData: UploadJobDataInputSchema, + + // SOQL / SOSL + runSoqlQuery: RunSoqlQueryInputSchema, + queryAll: QueryAllInputSchema, + search: SearchInputSchema, + executeSoslSearch: ExecuteSoslSearchInputSchema, + toolingQuery: ToolingQueryInputSchema, + parameterizedSearch: ParameterizedSearchInputSchema, + postParameterizedSearch: PostParameterizedSearchInputSchema, + getSearchLayout: GetSearchLayoutInputSchema, + query: QueryInputSchema, + executeSoqlQuery: ExecuteSoqlQueryInputSchema, + getSearchSuggestions: GetSearchSuggestionsInputSchema, + searchKnowledgeArticles: SearchKnowledgeArticlesInputSchema, + getParameterizedSearch: GetParameterizedSearchInputSchema, + + // Composite + postCompositeSobjects: PostCompositeSobjectsInputSchema, + createSobjectTree: CreateSobjectTreeInputSchema, + deleteSobjectCollections: DeleteSobjectCollectionsInputSchema, + postCompositeGraph: PostCompositeGraphInputSchema, + compositeGraphAction: CompositeGraphActionInputSchema, + getABatchOfRecords: GetABatchOfRecordsInputSchema, + getCompositeResources: GetCompositeResourcesInputSchema, + getCompositeSobjects: GetCompositeSobjectsInputSchema, + getSobjectCollections: GetSobjectCollectionsInputSchema, + patchCompositeSobjects: PatchCompositeSobjectsInputSchema, + + // Metadata + createSObjectRecord: CreateSObjectRecordInputSchema, + cloneRecord: CloneRecordInputSchema, + createCustomField: CreateCustomFieldInputSchema, + createCustomObject: CreateCustomObjectInputSchema, + deleteSobject: DeleteSobjectInputSchema, + deleteSobjectRows: DeleteSobjectRowsInputSchema, + getSobjects: GetSobjectsInputSchema, + executeSobjectQuickAction: ExecuteSobjectQuickActionInputSchema, + getApi: GetApiInputSchema, + getChatterResources: GetChatterResourcesInputSchema, + getSobjectPlatformaction: GetSobjectPlatformactionInputSchema, + headQuickActions: HeadQuickActionsInputSchema, + headSobjectsUserPassword: HeadSobjectsUserPasswordInputSchema, + getPicklistValuesByRecordType: GetPicklistValuesByRecordTypeInputSchema, + getAllFieldsForObject: GetAllFieldsForObjectInputSchema, + getAllCustomObjects: GetAllCustomObjectsInputSchema, + getSobjectsSobjectDescribeApprovallayouts: + GetSobjectsSobjectDescribeApprovallayoutsInputSchema, + getSobjectApprovalLayouts: GetSobjectApprovalLayoutsInputSchema, + getChildRecords: GetChildRecordsInputSchema, + getConsentAction: GetConsentActionInputSchema, + headActionsCustom: HeadActionsCustomInputSchema, + listCustomInvocableActions: ListCustomInvocableActionsInputSchema, + getSupportedObjectsDirectory: GetSupportedObjectsDirectoryInputSchema, + getGlobalActions: GetGlobalActionsInputSchema, + headSobjectsGlobalDescribeLayouts: + HeadSobjectsGlobalDescribeLayoutsInputSchema, + getSObjectsDescribeLayoutsRecordTypeId: + GetSObjectsDescribeLayoutsRecordTypeIdInputSchema, + getOrgLimits: GetOrgLimitsInputSchema, + headProcessRulesSObject: HeadProcessRulesSObjectInputSchema, + headSobjectQuickActionDefaultValues: + HeadSobjectQuickActionDefaultValuesInputSchema, + getQuickActions: GetQuickActionsInputSchema, + getRecordCounts: GetRecordCountsInputSchema, + getSobjectRelationship: GetSobjectRelationshipInputSchema, + getSobjectQuickActionDefaultValues: + GetSobjectQuickActionDefaultValuesInputSchema, + getSObjectQuickActionDefaultValues: + GetSObjectQuickActionDefaultValuesInputSchema, + getSobjectByExternalId: GetSobjectByExternalIdInputSchema, + headSobjectsQuickAction: HeadSobjectsQuickActionInputSchema, + getSObjectRecord: GetSObjectRecordInputSchema, + headActionsStandard: HeadActionsStandardInputSchema, + listStandardInvocableActions: ListStandardInvocableActionsInputSchema, + getSupport: GetSupportInputSchema, + getSupportKnowledgeArticles: GetSupportKnowledgeArticlesInputSchema, + getTheme: GetThemeInputSchema, + getSObjectsUpdated: GetSObjectsUpdatedInputSchema, + getUserInfo: GetUserInfoInputSchema, + sobjectUserPassword: SobjectUserPasswordInputSchema, + massTransferOwnership: MassTransferOwnershipInputSchema, + updateSobject: UpdateSobjectInputSchema, + sobjectRowsUpdate: SobjectRowsUpdateInputSchema, + upsertSobjectByExternalId: UpsertSobjectByExternalIdInputSchema, + setUserPassword: SetUserPasswordInputSchema, + + // UI API + createARecord: CreateARecordInputSchema, + createRecordUiApi: CreateRecordUiApiInputSchema, + getUiapiListInfoAccountAllAccounts: + GetUiapiListInfoAccountAllAccountsInputSchema, + getUiapiListInfoAccountSearchResult: + GetUiapiListInfoAccountSearchResultInputSchema, + headAppmenuSalesforce1: HeadAppmenuSalesforce1InputSchema, + getCompactLayouts: GetCompactLayoutsInputSchema, + getListViewActions: GetListViewActionsInputSchema, + getUiapiListInfoAccountRecent: GetUiapiListInfoAccountRecentInputSchema, + getUiApiListInfoRecent: GetUiApiListInfoRecentInputSchema, + getUiapimruListInfoAccount: GetUiapimruListInfoAccountInputSchema, + getUiApiMruListRecordsAccount: GetUiApiMruListRecordsAccountInputSchema, + getUiapiActionsMruListAccount: GetUiapiActionsMruListAccountInputSchema, + getMruListViewMetadata: GetMruListViewMetadataInputSchema, + getUiApiAppsUserNavItems: GetUiApiAppsUserNavItemsInputSchema, + getAllNavigationItems: GetAllNavigationItemsInputSchema, + getApp: GetAppInputSchema, + getApps: GetAppsInputSchema, + getListViewMetadataBatch: GetListViewMetadataBatchInputSchema, + getRelatedListPreferencesBatch: GetRelatedListPreferencesBatchInputSchema, + getLastSelectedApp: GetLastSelectedAppInputSchema, + getListViewMetadataByName: GetListViewMetadataByNameInputSchema, + getListViewRecordsByName: GetListViewRecordsByNameInputSchema, + getListViewRecordsById: GetListViewRecordsByIdInputSchema, + listViewResults: ListViewResultsInputSchema, + getListViewResults: GetListViewResultsInputSchema, + getObjectListViews: GetObjectListViewsInputSchema, + getSobjectListViews: GetSobjectListViewsInputSchema, + getUiApiActionsLookupAccount: GetUiApiActionsLookupAccountInputSchema, + getUiapiLookupsOpportunityAccountId: + GetUiapiLookupsOpportunityAccountIdInputSchema, + getLookupFieldSuggestions: GetLookupFieldSuggestionsInputSchema, + getLookupSuggestionsOpportunityAccount: + GetLookupSuggestionsOpportunityAccountInputSchema, + getLookupSuggestionsCaseContact: GetLookupSuggestionsCaseContactInputSchema, + getMruListViewRecords: GetMruListViewRecordsInputSchema, + getPhotoActions: GetPhotoActionsInputSchema, + getRecordUiDataAndMetadata: GetRecordUiDataAndMetadataInputSchema, + getRecordEditPageActions: GetRecordEditPageActionsInputSchema, + getUiApiActionsRecordRelatedList: GetUiApiActionsRecordRelatedListInputSchema, + getRelatedListActions: GetRelatedListActionsInputSchema, + getRelatedListRecordsContacts: GetRelatedListRecordsContactsInputSchema, + getUiapiRelatedListPreferences: GetUiapiRelatedListPreferencesInputSchema, + getSobjectListView: GetSobjectListViewInputSchema, + updateRecord: UpdateRecordInputSchema, + updateFavorite: UpdateFavoriteInputSchema, + updateRelatedListPreferences: UpdateRelatedListPreferencesInputSchema, + updateListViewPreferences: UpdateListViewPreferencesInputSchema, + + // Files + getFileContent: GetFileContentInputSchema, + getFileInformation: GetFileInformationInputSchema, + getFileShares: GetFileSharesInputSchema, + deleteFile: DeleteFileInputSchema, + uploadFile: UploadFileInputSchema, + + // Analytics & Reports + getDashboard: GetDashboardInputSchema, + listDashboards: ListDashboardsInputSchema, + listEmailTemplates: ListEmailTemplatesInputSchema, + listReports: ListReportsInputSchema, + runReport: RunReportInputSchema, + listAnalyticsTemplates: ListAnalyticsTemplatesInputSchema, + getReportInstance: GetReportInstanceInputSchema, + getReport: GetReportInputSchema, + queryReport: QueryReportInputSchema, +} as const; + +export type SalesforceEndpointInputs = { + [K in keyof typeof SalesforceEndpointInputSchemas]: z.infer< + (typeof SalesforceEndpointInputSchemas)[K] + >; +}; + +export const SalesforceEndpointOutputSchemas = { + // Accounts + createAccount: CreateAccountResponseSchema, + getAccount: GetAccountResponseSchema, + listAccounts: ListAccountsResponseSchema, + searchAccounts: SearchAccountsResponseSchema, + updateAccount: UpdateAccountResponseSchema, + updateAccountObjectById: UpdateAccountObjectByIdResponseSchema, + deleteAccount: DeleteAccountResponseSchema, + accountCreationWithContentTypeOption: + AccountCreationWithContentTypeOptionResponseSchema, + fetchAccountByIdWithQuery: FetchAccountByIdWithQueryResponseSchema, + removeAccountByUniqueIdentifier: + RemoveAccountByUniqueIdentifierResponseSchema, + retrieveAccountDataAndErrorResponses: + RetrieveAccountDataAndErrorResponsesResponseSchema, + + // Contacts + createContact: CreateContactResponseSchema, + getContact: GetContactResponseSchema, + listContacts: ListContactsResponseSchema, + deleteContact: DeleteContactResponseSchema, + associateContactToAccount: AssociateContactToAccountResponseSchema, + updateContact: UpdateContactResponseSchema, + updateContactById: UpdateContactByIdResponseSchema, + searchContacts: SearchContactsResponseSchema, + createNewContactWithJsonHeader: CreateNewContactWithJsonHeaderResponseSchema, + queryContactsByName: QueryContactsByNameResponseSchema, + removeASpecificContactById: RemoveASpecificContactByIdResponseSchema, + retrieveContactInfoWithStandardResponses: + RetrieveContactInfoWithStandardResponsesResponseSchema, + getContactById: GetContactByIdResponseSchema, + + // Leads + createLead: CreateLeadResponseSchema, + getLead: GetLeadResponseSchema, + listLeads: ListLeadsResponseSchema, + deleteLead: DeleteLeadResponseSchema, + applyLeadAssignmentRules: ApplyLeadAssignmentRulesResponseSchema, + updateLead: UpdateLeadResponseSchema, + updateLeadByIdWithJsonPayload: UpdateLeadByIdWithJsonPayloadResponseSchema, + searchLeads: SearchLeadsResponseSchema, + createLeadWithSpecifiedContentType: + CreateLeadWithSpecifiedContentTypeResponseSchema, + deleteALeadObjectByItsId: DeleteALeadObjectByItsIdResponseSchema, + retrieveLeadById: RetrieveLeadByIdResponseSchema, + retrieveLeadDataWithVariousResponses: + RetrieveLeadDataWithVariousResponsesResponseSchema, + + // Opportunities + createOpportunity: CreateOpportunityResponseSchema, + getOpportunity: GetOpportunityResponseSchema, + listOpportunities: ListOpportunitiesResponseSchema, + deleteOpportunity: DeleteOpportunityResponseSchema, + addOpportunityLineItem: AddOpportunityLineItemResponseSchema, + updateOpportunity: UpdateOpportunityResponseSchema, + updateOpportunityById: UpdateOpportunityByIdResponseSchema, + searchOpportunities: SearchOpportunitiesResponseSchema, + cloneOpportunityWithProducts: CloneOpportunityWithProductsResponseSchema, + listPricebookEntries: ListPricebookEntriesResponseSchema, + listPricebooks: ListPricebooksResponseSchema, + createOpportunityRecord: CreateOpportunityRecordResponseSchema, + removeOpportunityById: RemoveOpportunityByIdResponseSchema, + retrieveOpportunitiesData: RetrieveOpportunitiesDataResponseSchema, + retrieveOpportunityByIdWithOptionalFields: + RetrieveOpportunityByIdWithOptionalFieldsResponseSchema, + + // Campaigns + createCampaign: CreateCampaignResponseSchema, + getCampaign: GetCampaignResponseSchema, + listCampaigns: ListCampaignsResponseSchema, + deleteCampaign: DeleteCampaignResponseSchema, + addContactToCampaign: AddContactToCampaignResponseSchema, + updateCampaign: UpdateCampaignResponseSchema, + updateCampaignByIdWithJson: UpdateCampaignByIdWithJsonResponseSchema, + addLeadToCampaign: AddLeadToCampaignResponseSchema, + removeFromCampaign: RemoveFromCampaignResponseSchema, + searchCampaigns: SearchCampaignsResponseSchema, + createCampaignRecordViaPost: CreateCampaignRecordViaPostResponseSchema, + removeCampaignObjectById: RemoveCampaignObjectByIdResponseSchema, + retrieveCampaignDataWithErrorHandling: + RetrieveCampaignDataWithErrorHandlingResponseSchema, + retrieveSpecificCampaignObjectDetails: + RetrieveSpecificCampaignObjectDetailsResponseSchema, + + // Notes + createNote: CreateNoteResponseSchema, + updateNote: UpdateNoteResponseSchema, + updateSpecificNoteById: UpdateSpecificNoteByIdResponseSchema, + searchNotes: SearchNotesResponseSchema, + getNote: GetNoteResponseSchema, + listNotes: ListNotesResponseSchema, + deleteNote: DeleteNoteResponseSchema, + createNoteRecordWithContentTypeHeader: + CreateNoteRecordWithContentTypeHeaderResponseSchema, + removeNoteObjectById: RemoveNoteObjectByIdResponseSchema, + getNoteByIdWithFields: GetNoteByIdWithFieldsResponseSchema, + retrieveNoteObjectInformation: RetrieveNoteObjectInformationResponseSchema, + + // Tasks + createTask: CreateTaskResponseSchema, + completeTask: CompleteTaskResponseSchema, + logCall: LogCallResponseSchema, + logEmailActivity: LogEmailActivityResponseSchema, + updateTask: UpdateTaskResponseSchema, + searchTasks: SearchTasksResponseSchema, + sendEmail: SendEmailResponseSchema, + sendEmailFromTemplate: SendEmailFromTemplateResponseSchema, + sendMassEmail: SendMassEmailResponseSchema, + + // Jobs + closeOrAbortJob: CloseOrAbortJobResponseSchema, + deleteJobQuery: DeleteJobQueryResponseSchema, + getJobFailedRecordResults: GetJobFailedRecordResultsResponseSchema, + getQueryJobInfo: GetQueryJobInfoResponseSchema, + getQueryJobResults: GetQueryJobResultsResponseSchema, + getJobSuccessfulRecordResults: GetJobSuccessfulRecordResultsResponseSchema, + getJobUnprocessedRecordResults: GetJobUnprocessedRecordResultsResponseSchema, + uploadJobData: UploadJobDataResponseSchema, + + // SOQL / SOSL + runSoqlQuery: RunSoqlQueryResponseSchema, + queryAll: QueryAllResponseSchema, + search: SearchResponseSchema, + executeSoslSearch: ExecuteSoslSearchResponseSchema, + toolingQuery: ToolingQueryResponseSchema, + parameterizedSearch: ParameterizedSearchResponseSchema, + postParameterizedSearch: PostParameterizedSearchResponseSchema, + getSearchLayout: GetSearchLayoutResponseSchema, + query: QueryResponseSchema, + executeSoqlQuery: ExecuteSoqlQueryResponseSchema, + getSearchSuggestions: GetSearchSuggestionsResponseSchema, + searchKnowledgeArticles: SearchKnowledgeArticlesResponseSchema, + getParameterizedSearch: GetParameterizedSearchResponseSchema, + + // Composite + postCompositeSobjects: PostCompositeSobjectsResponseSchema, + createSobjectTree: CreateSobjectTreeResponseSchema, + deleteSobjectCollections: DeleteSobjectCollectionsResponseSchema, + postCompositeGraph: PostCompositeGraphResponseSchema, + compositeGraphAction: CompositeGraphActionResponseSchema, + getABatchOfRecords: GetABatchOfRecordsResponseSchema, + getCompositeResources: GetCompositeResourcesResponseSchema, + getCompositeSobjects: GetCompositeSobjectsResponseSchema, + getSobjectCollections: GetSobjectCollectionsResponseSchema, + patchCompositeSobjects: PatchCompositeSobjectsResponseSchema, + + // Metadata + createSObjectRecord: CreateSObjectRecordResponseSchema, + cloneRecord: CloneRecordResponseSchema, + createCustomField: CreateCustomFieldResponseSchema, + createCustomObject: CreateCustomObjectResponseSchema, + deleteSobject: DeleteSobjectResponseSchema, + deleteSobjectRows: DeleteSobjectRowsResponseSchema, + getSobjects: GetSobjectsResponseSchema, + executeSobjectQuickAction: ExecuteSobjectQuickActionResponseSchema, + getApi: GetApiResponseSchema, + getChatterResources: GetChatterResourcesResponseSchema, + getSobjectPlatformaction: GetSobjectPlatformactionResponseSchema, + headQuickActions: HeadQuickActionsResponseSchema, + headSobjectsUserPassword: HeadSobjectsUserPasswordResponseSchema, + getPicklistValuesByRecordType: GetPicklistValuesByRecordTypeResponseSchema, + getAllFieldsForObject: GetAllFieldsForObjectResponseSchema, + getAllCustomObjects: GetAllCustomObjectsResponseSchema, + getSobjectsSobjectDescribeApprovallayouts: + GetSobjectsSobjectDescribeApprovallayoutsResponseSchema, + getSobjectApprovalLayouts: GetSobjectApprovalLayoutsResponseSchema, + getChildRecords: GetChildRecordsResponseSchema, + getConsentAction: GetConsentActionResponseSchema, + headActionsCustom: HeadActionsCustomResponseSchema, + listCustomInvocableActions: ListCustomInvocableActionsResponseSchema, + getSupportedObjectsDirectory: GetSupportedObjectsDirectoryResponseSchema, + getGlobalActions: GetGlobalActionsResponseSchema, + headSobjectsGlobalDescribeLayouts: + HeadSobjectsGlobalDescribeLayoutsResponseSchema, + getSObjectsDescribeLayoutsRecordTypeId: + GetSObjectsDescribeLayoutsRecordTypeIdResponseSchema, + getOrgLimits: GetOrgLimitsResponseSchema, + headProcessRulesSObject: HeadProcessRulesSObjectResponseSchema, + headSobjectQuickActionDefaultValues: + HeadSobjectQuickActionDefaultValuesResponseSchema, + getQuickActions: GetQuickActionsResponseSchema, + getRecordCounts: GetRecordCountsResponseSchema, + getSobjectRelationship: GetSobjectRelationshipResponseSchema, + getSobjectQuickActionDefaultValues: + GetSobjectQuickActionDefaultValuesResponseSchema, + getSObjectQuickActionDefaultValues: + GetSObjectQuickActionDefaultValuesResponseSchema, + getSobjectByExternalId: GetSobjectByExternalIdResponseSchema, + headSobjectsQuickAction: HeadSobjectsQuickActionResponseSchema, + getSObjectRecord: GetSObjectRecordResponseSchema, + headActionsStandard: HeadActionsStandardResponseSchema, + listStandardInvocableActions: ListStandardInvocableActionsResponseSchema, + getSupport: GetSupportResponseSchema, + getSupportKnowledgeArticles: GetSupportKnowledgeArticlesResponseSchema, + getTheme: GetThemeResponseSchema, + getSObjectsUpdated: GetSObjectsUpdatedResponseSchema, + getUserInfo: GetUserInfoResponseSchema, + sobjectUserPassword: SobjectUserPasswordResponseSchema, + massTransferOwnership: MassTransferOwnershipResponseSchema, + updateSobject: UpdateSobjectResponseSchema, + sobjectRowsUpdate: SobjectRowsUpdateResponseSchema, + upsertSobjectByExternalId: UpsertSobjectByExternalIdResponseSchema, + setUserPassword: SetUserPasswordResponseSchema, + + // UI API + createARecord: CreateARecordResponseSchema, + createRecordUiApi: CreateRecordUiApiResponseSchema, + getUiapiListInfoAccountAllAccounts: + GetUiapiListInfoAccountAllAccountsResponseSchema, + getUiapiListInfoAccountSearchResult: + GetUiapiListInfoAccountSearchResultResponseSchema, + headAppmenuSalesforce1: HeadAppmenuSalesforce1ResponseSchema, + getCompactLayouts: GetCompactLayoutsResponseSchema, + getListViewActions: GetListViewActionsResponseSchema, + getUiapiListInfoAccountRecent: GetUiapiListInfoAccountRecentResponseSchema, + getUiApiListInfoRecent: GetUiApiListInfoRecentResponseSchema, + getUiapimruListInfoAccount: GetUiapimruListInfoAccountResponseSchema, + getUiApiMruListRecordsAccount: GetUiApiMruListRecordsAccountResponseSchema, + getUiapiActionsMruListAccount: GetUiapiActionsMruListAccountResponseSchema, + getMruListViewMetadata: GetMruListViewMetadataResponseSchema, + getUiApiAppsUserNavItems: GetUiApiAppsUserNavItemsResponseSchema, + getAllNavigationItems: GetAllNavigationItemsResponseSchema, + getApp: GetAppResponseSchema, + getApps: GetAppsResponseSchema, + getListViewMetadataBatch: GetListViewMetadataBatchResponseSchema, + getRelatedListPreferencesBatch: GetRelatedListPreferencesBatchResponseSchema, + getLastSelectedApp: GetLastSelectedAppResponseSchema, + getListViewMetadataByName: GetListViewMetadataByNameResponseSchema, + getListViewRecordsByName: GetListViewRecordsByNameResponseSchema, + getListViewRecordsById: GetListViewRecordsByIdResponseSchema, + listViewResults: ListViewResultsResponseSchema, + getListViewResults: GetListViewResultsResponseSchema, + getObjectListViews: GetObjectListViewsResponseSchema, + getSobjectListViews: GetSobjectListViewsResponseSchema, + getUiApiActionsLookupAccount: GetUiApiActionsLookupAccountResponseSchema, + getUiapiLookupsOpportunityAccountId: + GetUiapiLookupsOpportunityAccountIdResponseSchema, + getLookupFieldSuggestions: GetLookupFieldSuggestionsResponseSchema, + getLookupSuggestionsOpportunityAccount: + GetLookupSuggestionsOpportunityAccountResponseSchema, + getLookupSuggestionsCaseContact: + GetLookupSuggestionsCaseContactResponseSchema, + getMruListViewRecords: GetMruListViewRecordsResponseSchema, + getPhotoActions: GetPhotoActionsResponseSchema, + getRecordUiDataAndMetadata: GetRecordUiDataAndMetadataResponseSchema, + getRecordEditPageActions: GetRecordEditPageActionsResponseSchema, + getUiApiActionsRecordRelatedList: + GetUiApiActionsRecordRelatedListResponseSchema, + getRelatedListActions: GetRelatedListActionsResponseSchema, + getRelatedListRecordsContacts: GetRelatedListRecordsContactsResponseSchema, + getUiapiRelatedListPreferences: GetUiapiRelatedListPreferencesResponseSchema, + getSobjectListView: GetSobjectListViewResponseSchema, + updateRecord: UpdateRecordResponseSchema, + updateFavorite: UpdateFavoriteResponseSchema, + updateRelatedListPreferences: UpdateRelatedListPreferencesResponseSchema, + updateListViewPreferences: UpdateListViewPreferencesResponseSchema, + + // Files + getFileContent: GetFileContentResponseSchema, + getFileInformation: GetFileInformationResponseSchema, + getFileShares: GetFileSharesResponseSchema, + deleteFile: DeleteFileResponseSchema, + uploadFile: UploadFileResponseSchema, + + // Analytics & Reports + getDashboard: GetDashboardResponseSchema, + listDashboards: ListDashboardsResponseSchema, + listEmailTemplates: ListEmailTemplatesResponseSchema, + listReports: ListReportsResponseSchema, + runReport: RunReportResponseSchema, + listAnalyticsTemplates: ListAnalyticsTemplatesResponseSchema, + getReportInstance: GetReportInstanceResponseSchema, + getReport: GetReportResponseSchema, + queryReport: QueryReportResponseSchema, +} as const; + +export type SalesforceEndpointOutputs = { + [K in keyof typeof SalesforceEndpointOutputSchemas]: z.infer< + (typeof SalesforceEndpointOutputSchemas)[K] + >; +}; diff --git a/packages/salesforce/endpoints/ui-api.ts b/packages/salesforce/endpoints/ui-api.ts new file mode 100644 index 000000000..cf1487f68 --- /dev/null +++ b/packages/salesforce/endpoints/ui-api.ts @@ -0,0 +1,788 @@ +import { logEventFromContext } from 'corsair/core'; +import type { SalesforceEndpoints } from '..'; +import { salesforceCall } from './shared'; + +export const createARecord: SalesforceEndpoints['createARecord'] = async ( + ctx, + input, +) => { + const response = await salesforceCall<{ + id: string; + apiName?: string; + }>(ctx, 'ui-api/records', { method: 'POST', body: input }); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.create_record', + input, + 'completed', + ); + return response; +}; + +export const createRecordUiApi: SalesforceEndpoints['createRecordUiApi'] = + async (ctx, input) => { + const response = await salesforceCall<{ + id: string; + apiName?: string; + }>(ctx, 'ui-api/records', { method: 'POST', body: input }); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.create_record_ui', + input, + 'completed', + ); + return response; + }; + +export const getUiapiListInfoAccountAllAccounts: SalesforceEndpoints['getUiapiListInfoAccountAllAccounts'] = + async (ctx, _input) => { + const response = await salesforceCall>( + ctx, + 'ui-api/list-info/Account/AllAccounts', + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.list_info_account_all', + {}, + 'completed', + ); + return response; + }; + +export const getUiapiListInfoAccountSearchResult: SalesforceEndpoints['getUiapiListInfoAccountSearchResult'] = + async (ctx, _input) => { + const response = await salesforceCall>( + ctx, + 'ui-api/list-info/Account/__SearchResult', + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.list_info_account_search', + {}, + 'completed', + ); + return response; + }; + +export const headAppmenuSalesforce1: SalesforceEndpoints['headAppmenuSalesforce1'] = + async (ctx, _input) => { + await salesforceCall(ctx, 'appmenu/Salesforce1', { + method: 'HEAD', + }); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.head_appmenu', + {}, + 'completed', + ); + return { status: 200 }; + }; + +export const getCompactLayouts: SalesforceEndpoints['getCompactLayouts'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + `ui-api/compact-layouts/${input.sobjects.join(',')}`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.compact_layouts', + input, + 'completed', + ); + return response; + }; + +export const getListViewActions: SalesforceEndpoints['getListViewActions'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + `ui-api/actions/list-view/${input.sobject}`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.list_view_actions', + input, + 'completed', + ); + return response; + }; + +export const getUiapiListInfoAccountRecent: SalesforceEndpoints['getUiapiListInfoAccountRecent'] = + async (ctx, _input) => { + const response = await salesforceCall>( + ctx, + 'ui-api/list-info/Account/Recent', + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.list_info_account_recent', + {}, + 'completed', + ); + return response; + }; + +export const getUiApiListInfoRecent: SalesforceEndpoints['getUiApiListInfoRecent'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + `ui-api/list-info/${input.sobject}/Recent`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.list_info_recent', + input, + 'completed', + ); + return response; + }; + +/** @deprecated */ +export const getUiapimruListInfoAccount: SalesforceEndpoints['getUiapimruListInfoAccount'] = + async (ctx, _input) => { + const response = await salesforceCall>( + ctx, + 'ui-api/mru-list-info/Account', + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.mru_list_info_account_deprecated', + {}, + 'completed', + ); + return response; + }; + +/** @deprecated */ +export const getUiApiMruListRecordsAccount: SalesforceEndpoints['getUiApiMruListRecordsAccount'] = + async (ctx, _input) => { + const response = await salesforceCall>( + ctx, + 'ui-api/mru-list-records/Account', + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.mru_list_records_account_deprecated', + {}, + 'completed', + ); + return response; + }; + +export const getUiapiActionsMruListAccount: SalesforceEndpoints['getUiapiActionsMruListAccount'] = + async (ctx, _input) => { + const response = await salesforceCall>( + ctx, + 'ui-api/actions/mru-list/Account', + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.actions_mru_list_account', + {}, + 'completed', + ); + return response; + }; + +export const getMruListViewMetadata: SalesforceEndpoints['getMruListViewMetadata'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + `ui-api/mru-list-info/${input.sobject}`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.mru_list_view_metadata', + input, + 'completed', + ); + return response; + }; + +export const getUiApiAppsUserNavItems: SalesforceEndpoints['getUiApiAppsUserNavItems'] = + async (ctx, input) => { + const endpoint = input.appId + ? `ui-api/apps/${input.appId}/user-nav-items` + : 'ui-api/apps/user-nav-items'; + const response = await salesforceCall>( + ctx, + endpoint, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.apps_user_nav_items', + input, + 'completed', + ); + return response; + }; + +export const getAllNavigationItems: SalesforceEndpoints['getAllNavigationItems'] = + async (ctx, _input) => { + const response = await salesforceCall>( + ctx, + 'ui-api/nav-items', + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.all_nav_items', + {}, + 'completed', + ); + return response; + }; + +export const getApp: SalesforceEndpoints['getApp'] = async (ctx, input) => { + const response = await salesforceCall>( + ctx, + `ui-api/apps/${input.appId}`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.get_app', + input, + 'completed', + ); + return response; +}; + +export const getApps: SalesforceEndpoints['getApps'] = async (ctx, _input) => { + const response = await salesforceCall>( + ctx, + 'ui-api/apps', + { method: 'GET' }, + ); + + await logEventFromContext(ctx, 'salesforce.ui_api.get_apps', {}, 'completed'); + return response; +}; + +export const getListViewMetadataBatch: SalesforceEndpoints['getListViewMetadataBatch'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + 'ui-api/list-info/batch', + { + method: 'GET', + query: { ids: input.listViewIds.join(',') }, + }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.list_view_metadata_batch', + input, + 'completed', + ); + return response; + }; + +export const getRelatedListPreferencesBatch: SalesforceEndpoints['getRelatedListPreferencesBatch'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + `ui-api/related-list-preferences/batch/${input.relatedListIds.join(',')}`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.related_list_preferences_batch', + input, + 'completed', + ); + return response; + }; + +export const getLastSelectedApp: SalesforceEndpoints['getLastSelectedApp'] = + async (ctx, _input) => { + const response = await salesforceCall>( + ctx, + 'ui-api/apps/last-selected', + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.last_selected_app', + {}, + 'completed', + ); + return response; + }; + +export const getListViewMetadataByName: SalesforceEndpoints['getListViewMetadataByName'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + `ui-api/list-info/${input.sobject}/${input.listViewName}`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.list_view_metadata_by_name', + input, + 'completed', + ); + return response; + }; + +export const getListViewRecordsByName: SalesforceEndpoints['getListViewRecordsByName'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + `ui-api/list-records/${input.sobject}/${input.listViewName}`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.list_view_records_by_name', + input, + 'completed', + ); + return response; + }; + +export const getListViewRecordsById: SalesforceEndpoints['getListViewRecordsById'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + `ui-api/list-records/${input.listViewId}`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.list_view_records_by_id', + input, + 'completed', + ); + return response; + }; + +export const listViewResults: SalesforceEndpoints['listViewResults'] = async ( + ctx, + input, +) => { + const response = await salesforceCall>( + ctx, + `ui-api/list-records/${input.listViewId}`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.list_view_results', + input, + 'completed', + ); + return response; +}; + +export const getListViewResults: SalesforceEndpoints['getListViewResults'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + `sobjects/${input.sobject}/listviews/${input.listViewId}/results`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.get_list_view_results', + input, + 'completed', + ); + return response; + }; + +export const getObjectListViews: SalesforceEndpoints['getObjectListViews'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + `sobjects/${input.sobject}/listviews`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.object_list_views', + input, + 'completed', + ); + return response; + }; + +export const getSobjectListViews: SalesforceEndpoints['getSobjectListViews'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + `sobjects/${input.sobject}/listviews`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.sobject_list_views', + input, + 'completed', + ); + return response; + }; + +export const getUiApiActionsLookupAccount: SalesforceEndpoints['getUiApiActionsLookupAccount'] = + async (ctx, _input) => { + const response = await salesforceCall>( + ctx, + 'ui-api/actions/lookup/Account', + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.actions_lookup_account', + {}, + 'completed', + ); + return response; + }; + +export const getUiapiLookupsOpportunityAccountId: SalesforceEndpoints['getUiapiLookupsOpportunityAccountId'] = + async (ctx, _input) => { + const response = await salesforceCall>( + ctx, + 'ui-api/lookups/Opportunity/AccountId', + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.lookups_opportunity_account_id', + {}, + 'completed', + ); + return response; + }; + +export const getLookupFieldSuggestions: SalesforceEndpoints['getLookupFieldSuggestions'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + `ui-api/lookups/${input.sobject}/${input.field}`, + { + method: 'GET', + query: input.q ? { q: input.q } : undefined, + }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.lookup_field_suggestions', + input, + 'completed', + ); + return response; + }; + +export const getLookupSuggestionsOpportunityAccount: SalesforceEndpoints['getLookupSuggestionsOpportunityAccount'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + 'ui-api/lookups/Opportunity/AccountId', + { + method: 'POST', + body: input.q ? { q: input.q } : {}, + }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.lookup_suggestions_opp_acc', + input, + 'completed', + ); + return response; + }; + +export const getLookupSuggestionsCaseContact: SalesforceEndpoints['getLookupSuggestionsCaseContact'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + 'ui-api/lookups/Case/ContactId', + { + method: 'POST', + body: input.q ? { q: input.q } : {}, + }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.lookup_suggestions_case_contact', + input, + 'completed', + ); + return response; + }; + +export const getMruListViewRecords: SalesforceEndpoints['getMruListViewRecords'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + `ui-api/mru-list-records/${input.sobject}`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.mru_list_view_records', + input, + 'completed', + ); + return response; + }; + +export const getPhotoActions: SalesforceEndpoints['getPhotoActions'] = async ( + ctx, + input, +) => { + const endpoint = input.pageId + ? `ui-api/actions/photo/${input.pageId}` + : 'ui-api/actions/photo'; + const response = await salesforceCall>( + ctx, + endpoint, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.photo_actions', + input, + 'completed', + ); + return response; +}; + +export const getRecordUiDataAndMetadata: SalesforceEndpoints['getRecordUiDataAndMetadata'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + `ui-api/record-ui/${input.recordId}`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.record_ui_data_metadata', + input, + 'completed', + ); + return response; + }; + +export const getRecordEditPageActions: SalesforceEndpoints['getRecordEditPageActions'] = + async (ctx, input) => { + const endpoint = input.recordId + ? `ui-api/actions/record-edit/${input.sobject}/${input.recordId}` + : `ui-api/actions/record-edit/${input.sobject}`; + const response = await salesforceCall>( + ctx, + endpoint, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.record_edit_page_actions', + input, + 'completed', + ); + return response; + }; + +export const getUiApiActionsRecordRelatedList: SalesforceEndpoints['getUiApiActionsRecordRelatedList'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + `ui-api/actions/record-related-list/${input.parentRecordId}/${input.relationshipName}`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.actions_record_related_list', + input, + 'completed', + ); + return response; + }; + +export const getRelatedListActions: SalesforceEndpoints['getRelatedListActions'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + `ui-api/actions/related-list/${input.parentRecordId}/${input.relationshipName}`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.related_list_actions', + input, + 'completed', + ); + return response; + }; + +export const getRelatedListRecordsContacts: SalesforceEndpoints['getRelatedListRecordsContacts'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + `ui-api/related-list-records/Account/${input.parentRecordId}/Contacts`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.related_list_records_contacts', + input, + 'completed', + ); + return response; + }; + +export const getUiapiRelatedListPreferences: SalesforceEndpoints['getUiapiRelatedListPreferences'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + `ui-api/related-list-preferences/${input.parentRecordId}/${input.relationshipName}`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.related_list_preferences', + input, + 'completed', + ); + return response; + }; + +export const getSobjectListView: SalesforceEndpoints['getSobjectListView'] = + async (ctx, input) => { + const response = await salesforceCall>( + ctx, + `sobjects/${input.sobject}/listviews/${input.listViewId}`, + { method: 'GET' }, + ); + + await logEventFromContext( + ctx, + 'salesforce.ui_api.sobject_list_view_info', + input, + 'completed', + ); + return response; + }; + +export const updateRecord: SalesforceEndpoints['updateRecord'] = async ( + ctx, + input, +) => { + await salesforceCall(ctx, `ui-api/records/${input.recordId}`, { + method: 'PATCH', + body: { fields: input.fields }, + headers: input.ifUnmodifiedSince + ? { 'If-Unmodified-Since': input.ifUnmodifiedSince } + : undefined, + }); + await logEventFromContext( + ctx, + 'salesforce.ui_api.update_record', + input, + 'completed', + ); + return { success: true }; +}; + +export const updateFavorite: SalesforceEndpoints['updateFavorite'] = async ( + ctx, + input, +) => { + const response = await salesforceCall( + ctx, + `ui-api/favorites/${input.favoriteId}`, + { method: 'PATCH', body: input.fields }, + ); + await logEventFromContext( + ctx, + 'salesforce.ui_api.update_favorite', + input, + 'completed', + ); + return { result: response }; +}; + +export const updateRelatedListPreferences: SalesforceEndpoints['updateRelatedListPreferences'] = + async (ctx, input) => { + const response = await salesforceCall( + ctx, + `ui-api/related-list-preferences/${input.relatedListId}`, + { method: 'PATCH', body: input.preferences }, + ); + await logEventFromContext( + ctx, + 'salesforce.ui_api.update_related_list_preferences', + input, + 'completed', + ); + return { result: response }; + }; + +export const updateListViewPreferences: SalesforceEndpoints['updateListViewPreferences'] = + async (ctx, input) => { + const response = await salesforceCall( + ctx, + `ui-api/list-ui/${input.sobject}/${input.listViewId}/user-preferences`, + { method: 'PATCH', body: input.preferences }, + ); + await logEventFromContext( + ctx, + 'salesforce.ui_api.update_list_view_preferences', + input, + 'completed', + ); + return { result: response }; + }; diff --git a/packages/salesforce/error-handlers.ts b/packages/salesforce/error-handlers.ts new file mode 100644 index 000000000..4c30c6d36 --- /dev/null +++ b/packages/salesforce/error-handlers.ts @@ -0,0 +1,39 @@ +import type { CorsairErrorHandler } from 'corsair/core'; +import { ApiError } from 'corsair/http'; + +export const errorHandlers = { + RATE_LIMIT_ERROR: { + match: (error: Error) => { + if (error instanceof ApiError && error.status === 429) return true; + const msg = error.message.toLowerCase(); + return ( + msg.includes('rate_limited') || + msg.includes('429') || + msg.includes('request_limit_exceeded') + ); + }, + handler: async (error: Error) => { + let retryAfterMs: number | undefined; + if (error instanceof ApiError && error.retryAfter !== undefined) { + retryAfterMs = error.retryAfter; + } + return { maxRetries: 5, headersRetryAfterMs: retryAfterMs }; + }, + }, + AUTH_ERROR: { + match: (error: Error) => { + if (error instanceof ApiError && error.status === 401) return true; + const msg = error.message.toLowerCase(); + return ( + msg.includes('unauthorized') || + msg.includes('invalid_auth') || + msg.includes('invalid_session_id') + ); + }, + handler: async () => ({ maxRetries: 0 }), + }, + DEFAULT: { + match: () => true, + handler: async () => ({ maxRetries: 0 }), + }, +} satisfies CorsairErrorHandler; diff --git a/packages/salesforce/index.ts b/packages/salesforce/index.ts new file mode 100644 index 000000000..f28ea71f9 --- /dev/null +++ b/packages/salesforce/index.ts @@ -0,0 +1,2672 @@ +import type { + AuthTypes, + BindEndpoints, + BindWebhooks, + CorsairEndpoint, + CorsairErrorHandler, + CorsairPlugin, + CorsairPluginContext, + CorsairWebhook, + KeyBuilderContext, + PickAuth, + PluginAuthConfig, + PluginPermissionsConfig, + RequiredPluginEndpointMeta, + RequiredPluginEndpointSchemas, +} from 'corsair/core'; +import { AuthMissingError } from 'corsair/core'; +import { z } from 'zod'; +import { SALESFORCE_LOGIN_HOST } from './client'; +import { + Accounts, + AnalyticsReports, + Campaigns, + Composite, + Contacts, + Files, + Jobs, + Leads, + Metadata, + Notes, + Opportunities, + SoqlSosl, + Tasks, + UiApi, +} from './endpoints'; +import type { + SalesforceEndpointInputs, + SalesforceEndpointOutputs, +} from './endpoints/types'; +import { + SalesforceEndpointInputSchemas, + SalesforceEndpointOutputSchemas, +} from './endpoints/types'; +import { errorHandlers } from './error-handlers'; +import { SalesforceSchema } from './schema'; +import { resolveSalesforceOAuthWebhookTenantLink } from './webhooks/oauth-tenant-link'; +import { matchSalesforceTenantWebhook } from './webhooks/tenant-matcher'; +import { + accountCreatedOrUpdated, + contactUpdated, + genericSObjectRecordUpdated, + newContact, + newLead, + newOrUpdatedOpportunity, + taskCreatedOrCompleted, +} from './webhooks/triggers'; +import type { + SalesforceWebhookOutputs, + SalesforceWebhookPayload, +} from './webhooks/types'; +import { SalesforceWebhookPayloadSchema } from './webhooks/types'; + +export type SalesforcePluginOptions = { + authType?: PickAuth<'api_key' | 'oauth_2'>; + key?: string; + instanceUrl?: string; + loginUrl?: string; + webhookSecret?: string; + hooks?: InternalSalesforcePlugin['hooks']; + webhookHooks?: InternalSalesforcePlugin['webhookHooks']; + errorHandlers?: CorsairErrorHandler; + permissions?: PluginPermissionsConfig; +}; + +export type SalesforceContext = CorsairPluginContext< + typeof SalesforceSchema, + SalesforcePluginOptions +>; + +export type SalesforceKeyBuilderContext = + KeyBuilderContext; + +export type SalesforceBoundEndpoints = BindEndpoints< + typeof salesforceEndpointsNested +>; + +type SalesforceEndpoint = + CorsairEndpoint< + SalesforceContext, + SalesforceEndpointInputs[K], + SalesforceEndpointOutputs[K] + >; + +export type SalesforceEndpoints = { + // Accounts + createAccount: SalesforceEndpoint<'createAccount'>; + getAccount: SalesforceEndpoint<'getAccount'>; + listAccounts: SalesforceEndpoint<'listAccounts'>; + searchAccounts: SalesforceEndpoint<'searchAccounts'>; + updateAccount: SalesforceEndpoint<'updateAccount'>; + updateAccountObjectById: SalesforceEndpoint<'updateAccountObjectById'>; + deleteAccount: SalesforceEndpoint<'deleteAccount'>; + accountCreationWithContentTypeOption: SalesforceEndpoint<'accountCreationWithContentTypeOption'>; + fetchAccountByIdWithQuery: SalesforceEndpoint<'fetchAccountByIdWithQuery'>; + removeAccountByUniqueIdentifier: SalesforceEndpoint<'removeAccountByUniqueIdentifier'>; + retrieveAccountDataAndErrorResponses: SalesforceEndpoint<'retrieveAccountDataAndErrorResponses'>; + + // Contacts + createContact: SalesforceEndpoint<'createContact'>; + getContact: SalesforceEndpoint<'getContact'>; + listContacts: SalesforceEndpoint<'listContacts'>; + deleteContact: SalesforceEndpoint<'deleteContact'>; + associateContactToAccount: SalesforceEndpoint<'associateContactToAccount'>; + updateContact: SalesforceEndpoint<'updateContact'>; + updateContactById: SalesforceEndpoint<'updateContactById'>; + searchContacts: SalesforceEndpoint<'searchContacts'>; + createNewContactWithJsonHeader: SalesforceEndpoint<'createNewContactWithJsonHeader'>; + queryContactsByName: SalesforceEndpoint<'queryContactsByName'>; + removeASpecificContactById: SalesforceEndpoint<'removeASpecificContactById'>; + retrieveContactInfoWithStandardResponses: SalesforceEndpoint<'retrieveContactInfoWithStandardResponses'>; + getContactById: SalesforceEndpoint<'getContactById'>; + + // Leads + createLead: SalesforceEndpoint<'createLead'>; + getLead: SalesforceEndpoint<'getLead'>; + listLeads: SalesforceEndpoint<'listLeads'>; + deleteLead: SalesforceEndpoint<'deleteLead'>; + applyLeadAssignmentRules: SalesforceEndpoint<'applyLeadAssignmentRules'>; + updateLead: SalesforceEndpoint<'updateLead'>; + updateLeadByIdWithJsonPayload: SalesforceEndpoint<'updateLeadByIdWithJsonPayload'>; + searchLeads: SalesforceEndpoint<'searchLeads'>; + createLeadWithSpecifiedContentType: SalesforceEndpoint<'createLeadWithSpecifiedContentType'>; + deleteALeadObjectByItsId: SalesforceEndpoint<'deleteALeadObjectByItsId'>; + retrieveLeadById: SalesforceEndpoint<'retrieveLeadById'>; + retrieveLeadDataWithVariousResponses: SalesforceEndpoint<'retrieveLeadDataWithVariousResponses'>; + + // Opportunities + createOpportunity: SalesforceEndpoint<'createOpportunity'>; + getOpportunity: SalesforceEndpoint<'getOpportunity'>; + listOpportunities: SalesforceEndpoint<'listOpportunities'>; + deleteOpportunity: SalesforceEndpoint<'deleteOpportunity'>; + addOpportunityLineItem: SalesforceEndpoint<'addOpportunityLineItem'>; + updateOpportunity: SalesforceEndpoint<'updateOpportunity'>; + updateOpportunityById: SalesforceEndpoint<'updateOpportunityById'>; + searchOpportunities: SalesforceEndpoint<'searchOpportunities'>; + cloneOpportunityWithProducts: SalesforceEndpoint<'cloneOpportunityWithProducts'>; + listPricebookEntries: SalesforceEndpoint<'listPricebookEntries'>; + listPricebooks: SalesforceEndpoint<'listPricebooks'>; + createOpportunityRecord: SalesforceEndpoint<'createOpportunityRecord'>; + removeOpportunityById: SalesforceEndpoint<'removeOpportunityById'>; + retrieveOpportunitiesData: SalesforceEndpoint<'retrieveOpportunitiesData'>; + retrieveOpportunityByIdWithOptionalFields: SalesforceEndpoint<'retrieveOpportunityByIdWithOptionalFields'>; + + // Campaigns + createCampaign: SalesforceEndpoint<'createCampaign'>; + getCampaign: SalesforceEndpoint<'getCampaign'>; + listCampaigns: SalesforceEndpoint<'listCampaigns'>; + deleteCampaign: SalesforceEndpoint<'deleteCampaign'>; + addContactToCampaign: SalesforceEndpoint<'addContactToCampaign'>; + updateCampaign: SalesforceEndpoint<'updateCampaign'>; + updateCampaignByIdWithJson: SalesforceEndpoint<'updateCampaignByIdWithJson'>; + addLeadToCampaign: SalesforceEndpoint<'addLeadToCampaign'>; + removeFromCampaign: SalesforceEndpoint<'removeFromCampaign'>; + searchCampaigns: SalesforceEndpoint<'searchCampaigns'>; + createCampaignRecordViaPost: SalesforceEndpoint<'createCampaignRecordViaPost'>; + removeCampaignObjectById: SalesforceEndpoint<'removeCampaignObjectById'>; + retrieveCampaignDataWithErrorHandling: SalesforceEndpoint<'retrieveCampaignDataWithErrorHandling'>; + retrieveSpecificCampaignObjectDetails: SalesforceEndpoint<'retrieveSpecificCampaignObjectDetails'>; + + // Notes + createNote: SalesforceEndpoint<'createNote'>; + updateNote: SalesforceEndpoint<'updateNote'>; + updateSpecificNoteById: SalesforceEndpoint<'updateSpecificNoteById'>; + searchNotes: SalesforceEndpoint<'searchNotes'>; + getNote: SalesforceEndpoint<'getNote'>; + listNotes: SalesforceEndpoint<'listNotes'>; + deleteNote: SalesforceEndpoint<'deleteNote'>; + createNoteRecordWithContentTypeHeader: SalesforceEndpoint<'createNoteRecordWithContentTypeHeader'>; + removeNoteObjectById: SalesforceEndpoint<'removeNoteObjectById'>; + getNoteByIdWithFields: SalesforceEndpoint<'getNoteByIdWithFields'>; + retrieveNoteObjectInformation: SalesforceEndpoint<'retrieveNoteObjectInformation'>; + + // Tasks + createTask: SalesforceEndpoint<'createTask'>; + completeTask: SalesforceEndpoint<'completeTask'>; + logCall: SalesforceEndpoint<'logCall'>; + logEmailActivity: SalesforceEndpoint<'logEmailActivity'>; + updateTask: SalesforceEndpoint<'updateTask'>; + searchTasks: SalesforceEndpoint<'searchTasks'>; + sendEmail: SalesforceEndpoint<'sendEmail'>; + sendEmailFromTemplate: SalesforceEndpoint<'sendEmailFromTemplate'>; + sendMassEmail: SalesforceEndpoint<'sendMassEmail'>; + + // Jobs + closeOrAbortJob: SalesforceEndpoint<'closeOrAbortJob'>; + deleteJobQuery: SalesforceEndpoint<'deleteJobQuery'>; + getJobFailedRecordResults: SalesforceEndpoint<'getJobFailedRecordResults'>; + getQueryJobInfo: SalesforceEndpoint<'getQueryJobInfo'>; + getQueryJobResults: SalesforceEndpoint<'getQueryJobResults'>; + getJobSuccessfulRecordResults: SalesforceEndpoint<'getJobSuccessfulRecordResults'>; + getJobUnprocessedRecordResults: SalesforceEndpoint<'getJobUnprocessedRecordResults'>; + uploadJobData: SalesforceEndpoint<'uploadJobData'>; + + // SOQL / SOSL + runSoqlQuery: SalesforceEndpoint<'runSoqlQuery'>; + queryAll: SalesforceEndpoint<'queryAll'>; + search: SalesforceEndpoint<'search'>; + executeSoslSearch: SalesforceEndpoint<'executeSoslSearch'>; + toolingQuery: SalesforceEndpoint<'toolingQuery'>; + parameterizedSearch: SalesforceEndpoint<'parameterizedSearch'>; + postParameterizedSearch: SalesforceEndpoint<'postParameterizedSearch'>; + getSearchLayout: SalesforceEndpoint<'getSearchLayout'>; + query: SalesforceEndpoint<'query'>; + executeSoqlQuery: SalesforceEndpoint<'executeSoqlQuery'>; + getSearchSuggestions: SalesforceEndpoint<'getSearchSuggestions'>; + searchKnowledgeArticles: SalesforceEndpoint<'searchKnowledgeArticles'>; + getParameterizedSearch: SalesforceEndpoint<'getParameterizedSearch'>; + + // Composite + postCompositeSobjects: SalesforceEndpoint<'postCompositeSobjects'>; + createSobjectTree: SalesforceEndpoint<'createSobjectTree'>; + deleteSobjectCollections: SalesforceEndpoint<'deleteSobjectCollections'>; + postCompositeGraph: SalesforceEndpoint<'postCompositeGraph'>; + compositeGraphAction: SalesforceEndpoint<'compositeGraphAction'>; + getABatchOfRecords: SalesforceEndpoint<'getABatchOfRecords'>; + getCompositeResources: SalesforceEndpoint<'getCompositeResources'>; + getCompositeSobjects: SalesforceEndpoint<'getCompositeSobjects'>; + getSobjectCollections: SalesforceEndpoint<'getSobjectCollections'>; + patchCompositeSobjects: SalesforceEndpoint<'patchCompositeSobjects'>; + + // Metadata + createSObjectRecord: SalesforceEndpoint<'createSObjectRecord'>; + cloneRecord: SalesforceEndpoint<'cloneRecord'>; + createCustomField: SalesforceEndpoint<'createCustomField'>; + createCustomObject: SalesforceEndpoint<'createCustomObject'>; + deleteSobject: SalesforceEndpoint<'deleteSobject'>; + deleteSobjectRows: SalesforceEndpoint<'deleteSobjectRows'>; + getSobjects: SalesforceEndpoint<'getSobjects'>; + executeSobjectQuickAction: SalesforceEndpoint<'executeSobjectQuickAction'>; + getApi: SalesforceEndpoint<'getApi'>; + getChatterResources: SalesforceEndpoint<'getChatterResources'>; + getSobjectPlatformaction: SalesforceEndpoint<'getSobjectPlatformaction'>; + headQuickActions: SalesforceEndpoint<'headQuickActions'>; + headSobjectsUserPassword: SalesforceEndpoint<'headSobjectsUserPassword'>; + getPicklistValuesByRecordType: SalesforceEndpoint<'getPicklistValuesByRecordType'>; + getAllFieldsForObject: SalesforceEndpoint<'getAllFieldsForObject'>; + getAllCustomObjects: SalesforceEndpoint<'getAllCustomObjects'>; + getSobjectsSobjectDescribeApprovallayouts: SalesforceEndpoint<'getSobjectsSobjectDescribeApprovallayouts'>; + getSobjectApprovalLayouts: SalesforceEndpoint<'getSobjectApprovalLayouts'>; + getChildRecords: SalesforceEndpoint<'getChildRecords'>; + getConsentAction: SalesforceEndpoint<'getConsentAction'>; + headActionsCustom: SalesforceEndpoint<'headActionsCustom'>; + listCustomInvocableActions: SalesforceEndpoint<'listCustomInvocableActions'>; + getSupportedObjectsDirectory: SalesforceEndpoint<'getSupportedObjectsDirectory'>; + getGlobalActions: SalesforceEndpoint<'getGlobalActions'>; + headSobjectsGlobalDescribeLayouts: SalesforceEndpoint<'headSobjectsGlobalDescribeLayouts'>; + getSObjectsDescribeLayoutsRecordTypeId: SalesforceEndpoint<'getSObjectsDescribeLayoutsRecordTypeId'>; + getOrgLimits: SalesforceEndpoint<'getOrgLimits'>; + headProcessRulesSObject: SalesforceEndpoint<'headProcessRulesSObject'>; + headSobjectQuickActionDefaultValues: SalesforceEndpoint<'headSobjectQuickActionDefaultValues'>; + getQuickActions: SalesforceEndpoint<'getQuickActions'>; + getRecordCounts: SalesforceEndpoint<'getRecordCounts'>; + getSobjectRelationship: SalesforceEndpoint<'getSobjectRelationship'>; + getSobjectQuickActionDefaultValues: SalesforceEndpoint<'getSobjectQuickActionDefaultValues'>; + getSObjectQuickActionDefaultValues: SalesforceEndpoint<'getSObjectQuickActionDefaultValues'>; + getSobjectByExternalId: SalesforceEndpoint<'getSobjectByExternalId'>; + headSobjectsQuickAction: SalesforceEndpoint<'headSobjectsQuickAction'>; + getSObjectRecord: SalesforceEndpoint<'getSObjectRecord'>; + headActionsStandard: SalesforceEndpoint<'headActionsStandard'>; + listStandardInvocableActions: SalesforceEndpoint<'listStandardInvocableActions'>; + getSupport: SalesforceEndpoint<'getSupport'>; + getSupportKnowledgeArticles: SalesforceEndpoint<'getSupportKnowledgeArticles'>; + getTheme: SalesforceEndpoint<'getTheme'>; + getSObjectsUpdated: SalesforceEndpoint<'getSObjectsUpdated'>; + getUserInfo: SalesforceEndpoint<'getUserInfo'>; + sobjectUserPassword: SalesforceEndpoint<'sobjectUserPassword'>; + massTransferOwnership: SalesforceEndpoint<'massTransferOwnership'>; + updateSobject: SalesforceEndpoint<'updateSobject'>; + sobjectRowsUpdate: SalesforceEndpoint<'sobjectRowsUpdate'>; + upsertSobjectByExternalId: SalesforceEndpoint<'upsertSobjectByExternalId'>; + setUserPassword: SalesforceEndpoint<'setUserPassword'>; + + // UI API + createARecord: SalesforceEndpoint<'createARecord'>; + createRecordUiApi: SalesforceEndpoint<'createRecordUiApi'>; + getUiapiListInfoAccountAllAccounts: SalesforceEndpoint<'getUiapiListInfoAccountAllAccounts'>; + getUiapiListInfoAccountSearchResult: SalesforceEndpoint<'getUiapiListInfoAccountSearchResult'>; + headAppmenuSalesforce1: SalesforceEndpoint<'headAppmenuSalesforce1'>; + getCompactLayouts: SalesforceEndpoint<'getCompactLayouts'>; + getListViewActions: SalesforceEndpoint<'getListViewActions'>; + getUiapiListInfoAccountRecent: SalesforceEndpoint<'getUiapiListInfoAccountRecent'>; + getUiApiListInfoRecent: SalesforceEndpoint<'getUiApiListInfoRecent'>; + getUiapimruListInfoAccount: SalesforceEndpoint<'getUiapimruListInfoAccount'>; + getUiApiMruListRecordsAccount: SalesforceEndpoint<'getUiApiMruListRecordsAccount'>; + getUiapiActionsMruListAccount: SalesforceEndpoint<'getUiapiActionsMruListAccount'>; + getMruListViewMetadata: SalesforceEndpoint<'getMruListViewMetadata'>; + getUiApiAppsUserNavItems: SalesforceEndpoint<'getUiApiAppsUserNavItems'>; + getAllNavigationItems: SalesforceEndpoint<'getAllNavigationItems'>; + getApp: SalesforceEndpoint<'getApp'>; + getApps: SalesforceEndpoint<'getApps'>; + getListViewMetadataBatch: SalesforceEndpoint<'getListViewMetadataBatch'>; + getRelatedListPreferencesBatch: SalesforceEndpoint<'getRelatedListPreferencesBatch'>; + getLastSelectedApp: SalesforceEndpoint<'getLastSelectedApp'>; + getListViewMetadataByName: SalesforceEndpoint<'getListViewMetadataByName'>; + getListViewRecordsByName: SalesforceEndpoint<'getListViewRecordsByName'>; + getListViewRecordsById: SalesforceEndpoint<'getListViewRecordsById'>; + listViewResults: SalesforceEndpoint<'listViewResults'>; + getListViewResults: SalesforceEndpoint<'getListViewResults'>; + getObjectListViews: SalesforceEndpoint<'getObjectListViews'>; + getSobjectListViews: SalesforceEndpoint<'getSobjectListViews'>; + getUiApiActionsLookupAccount: SalesforceEndpoint<'getUiApiActionsLookupAccount'>; + getUiapiLookupsOpportunityAccountId: SalesforceEndpoint<'getUiapiLookupsOpportunityAccountId'>; + getLookupFieldSuggestions: SalesforceEndpoint<'getLookupFieldSuggestions'>; + getLookupSuggestionsOpportunityAccount: SalesforceEndpoint<'getLookupSuggestionsOpportunityAccount'>; + getLookupSuggestionsCaseContact: SalesforceEndpoint<'getLookupSuggestionsCaseContact'>; + getMruListViewRecords: SalesforceEndpoint<'getMruListViewRecords'>; + getPhotoActions: SalesforceEndpoint<'getPhotoActions'>; + getRecordUiDataAndMetadata: SalesforceEndpoint<'getRecordUiDataAndMetadata'>; + getRecordEditPageActions: SalesforceEndpoint<'getRecordEditPageActions'>; + getUiApiActionsRecordRelatedList: SalesforceEndpoint<'getUiApiActionsRecordRelatedList'>; + getRelatedListActions: SalesforceEndpoint<'getRelatedListActions'>; + getRelatedListRecordsContacts: SalesforceEndpoint<'getRelatedListRecordsContacts'>; + getUiapiRelatedListPreferences: SalesforceEndpoint<'getUiapiRelatedListPreferences'>; + getSobjectListView: SalesforceEndpoint<'getSobjectListView'>; + updateRecord: SalesforceEndpoint<'updateRecord'>; + updateFavorite: SalesforceEndpoint<'updateFavorite'>; + updateRelatedListPreferences: SalesforceEndpoint<'updateRelatedListPreferences'>; + updateListViewPreferences: SalesforceEndpoint<'updateListViewPreferences'>; + + // Files + getFileContent: SalesforceEndpoint<'getFileContent'>; + getFileInformation: SalesforceEndpoint<'getFileInformation'>; + getFileShares: SalesforceEndpoint<'getFileShares'>; + deleteFile: SalesforceEndpoint<'deleteFile'>; + uploadFile: SalesforceEndpoint<'uploadFile'>; + + // Analytics & Reports + getDashboard: SalesforceEndpoint<'getDashboard'>; + listDashboards: SalesforceEndpoint<'listDashboards'>; + listEmailTemplates: SalesforceEndpoint<'listEmailTemplates'>; + listReports: SalesforceEndpoint<'listReports'>; + runReport: SalesforceEndpoint<'runReport'>; + listAnalyticsTemplates: SalesforceEndpoint<'listAnalyticsTemplates'>; + getReportInstance: SalesforceEndpoint<'getReportInstance'>; + getReport: SalesforceEndpoint<'getReport'>; + queryReport: SalesforceEndpoint<'queryReport'>; +}; + +type SalesforceWebhook = + CorsairWebhook< + SalesforceContext, + SalesforceWebhookPayload, + SalesforceWebhookOutputs[K] + >; + +export type SalesforceWebhooks = { + accountCreatedOrUpdated: SalesforceWebhook<'accountCreatedOrUpdated'>; + contactUpdated: SalesforceWebhook<'contactUpdated'>; + newContact: SalesforceWebhook<'newContact'>; + newLead: SalesforceWebhook<'newLead'>; + newOrUpdatedOpportunity: SalesforceWebhook<'newOrUpdatedOpportunity'>; + genericSObjectRecordUpdated: SalesforceWebhook<'genericSObjectRecordUpdated'>; + taskCreatedOrCompleted: SalesforceWebhook<'taskCreatedOrCompleted'>; +}; + +export type SalesforceBoundWebhooks = BindWebhooks; + +const salesforceEndpointsNested = { + accounts: { + createAccount: Accounts.createAccount, + getAccount: Accounts.getAccount, + listAccounts: Accounts.listAccounts, + searchAccounts: Accounts.searchAccounts, + updateAccount: Accounts.updateAccount, + updateAccountObjectById: Accounts.updateAccountObjectById, + deleteAccount: Accounts.deleteAccount, + accountCreationWithContentTypeOption: + Accounts.accountCreationWithContentTypeOption, + fetchAccountByIdWithQuery: Accounts.fetchAccountByIdWithQuery, + removeAccountByUniqueIdentifier: Accounts.removeAccountByUniqueIdentifier, + retrieveAccountDataAndErrorResponses: + Accounts.retrieveAccountDataAndErrorResponses, + }, + contacts: { + createContact: Contacts.createContact, + getContact: Contacts.getContact, + listContacts: Contacts.listContacts, + deleteContact: Contacts.deleteContact, + associateContactToAccount: Contacts.associateContactToAccount, + updateContact: Contacts.updateContact, + updateContactById: Contacts.updateContactById, + searchContacts: Contacts.searchContacts, + createNewContactWithJsonHeader: Contacts.createNewContactWithJsonHeader, + queryContactsByName: Contacts.queryContactsByName, + removeASpecificContactById: Contacts.removeASpecificContactById, + retrieveContactInfoWithStandardResponses: + Contacts.retrieveContactInfoWithStandardResponses, + getContactById: Contacts.getContactById, + }, + leads: { + createLead: Leads.createLead, + getLead: Leads.getLead, + listLeads: Leads.listLeads, + deleteLead: Leads.deleteLead, + applyLeadAssignmentRules: Leads.applyLeadAssignmentRules, + updateLead: Leads.updateLead, + updateLeadByIdWithJsonPayload: Leads.updateLeadByIdWithJsonPayload, + searchLeads: Leads.searchLeads, + createLeadWithSpecifiedContentType: + Leads.createLeadWithSpecifiedContentType, + deleteALeadObjectByItsId: Leads.deleteALeadObjectByItsId, + retrieveLeadById: Leads.retrieveLeadById, + retrieveLeadDataWithVariousResponses: + Leads.retrieveLeadDataWithVariousResponses, + }, + opportunities: { + createOpportunity: Opportunities.createOpportunity, + getOpportunity: Opportunities.getOpportunity, + listOpportunities: Opportunities.listOpportunities, + deleteOpportunity: Opportunities.deleteOpportunity, + addOpportunityLineItem: Opportunities.addOpportunityLineItem, + updateOpportunity: Opportunities.updateOpportunity, + updateOpportunityById: Opportunities.updateOpportunityById, + searchOpportunities: Opportunities.searchOpportunities, + cloneOpportunityWithProducts: Opportunities.cloneOpportunityWithProducts, + listPricebookEntries: Opportunities.listPricebookEntries, + listPricebooks: Opportunities.listPricebooks, + createOpportunityRecord: Opportunities.createOpportunityRecord, + removeOpportunityById: Opportunities.removeOpportunityById, + retrieveOpportunitiesData: Opportunities.retrieveOpportunitiesData, + retrieveOpportunityByIdWithOptionalFields: + Opportunities.retrieveOpportunityByIdWithOptionalFields, + }, + campaigns: { + createCampaign: Campaigns.createCampaign, + getCampaign: Campaigns.getCampaign, + listCampaigns: Campaigns.listCampaigns, + deleteCampaign: Campaigns.deleteCampaign, + addContactToCampaign: Campaigns.addContactToCampaign, + updateCampaign: Campaigns.updateCampaign, + updateCampaignByIdWithJson: Campaigns.updateCampaignByIdWithJson, + addLeadToCampaign: Campaigns.addLeadToCampaign, + removeFromCampaign: Campaigns.removeFromCampaign, + searchCampaigns: Campaigns.searchCampaigns, + createCampaignRecordViaPost: Campaigns.createCampaignRecordViaPost, + removeCampaignObjectById: Campaigns.removeCampaignObjectById, + retrieveCampaignDataWithErrorHandling: + Campaigns.retrieveCampaignDataWithErrorHandling, + retrieveSpecificCampaignObjectDetails: + Campaigns.retrieveSpecificCampaignObjectDetails, + }, + notes: { + createNote: Notes.createNote, + updateNote: Notes.updateNote, + updateSpecificNoteById: Notes.updateSpecificNoteById, + searchNotes: Notes.searchNotes, + getNote: Notes.getNote, + listNotes: Notes.listNotes, + deleteNote: Notes.deleteNote, + createNoteRecordWithContentTypeHeader: + Notes.createNoteRecordWithContentTypeHeader, + removeNoteObjectById: Notes.removeNoteObjectById, + getNoteByIdWithFields: Notes.getNoteByIdWithFields, + retrieveNoteObjectInformation: Notes.retrieveNoteObjectInformation, + }, + tasks: { + createTask: Tasks.createTask, + completeTask: Tasks.completeTask, + logCall: Tasks.logCall, + logEmailActivity: Tasks.logEmailActivity, + updateTask: Tasks.updateTask, + searchTasks: Tasks.searchTasks, + sendEmail: Tasks.sendEmail, + sendEmailFromTemplate: Tasks.sendEmailFromTemplate, + sendMassEmail: Tasks.sendMassEmail, + }, + jobs: { + closeOrAbortJob: Jobs.closeOrAbortJob, + deleteJobQuery: Jobs.deleteJobQuery, + getJobFailedRecordResults: Jobs.getJobFailedRecordResults, + getQueryJobInfo: Jobs.getQueryJobInfo, + getQueryJobResults: Jobs.getQueryJobResults, + getJobSuccessfulRecordResults: Jobs.getJobSuccessfulRecordResults, + getJobUnprocessedRecordResults: Jobs.getJobUnprocessedRecordResults, + uploadJobData: Jobs.uploadJobData, + }, + soqlSosl: { + runSoqlQuery: SoqlSosl.runSoqlQuery, + queryAll: SoqlSosl.queryAll, + search: SoqlSosl.search, + executeSoslSearch: SoqlSosl.executeSoslSearch, + toolingQuery: SoqlSosl.toolingQuery, + parameterizedSearch: SoqlSosl.parameterizedSearch, + postParameterizedSearch: SoqlSosl.postParameterizedSearch, + getSearchLayout: SoqlSosl.getSearchLayout, + query: SoqlSosl.query, + executeSoqlQuery: SoqlSosl.executeSoqlQuery, + getSearchSuggestions: SoqlSosl.getSearchSuggestions, + searchKnowledgeArticles: SoqlSosl.searchKnowledgeArticles, + getParameterizedSearch: SoqlSosl.getParameterizedSearch, + }, + composite: { + postCompositeSobjects: Composite.postCompositeSobjects, + createSobjectTree: Composite.createSobjectTree, + deleteSobjectCollections: Composite.deleteSobjectCollections, + postCompositeGraph: Composite.postCompositeGraph, + compositeGraphAction: Composite.compositeGraphAction, + getABatchOfRecords: Composite.getABatchOfRecords, + getCompositeResources: Composite.getCompositeResources, + getCompositeSobjects: Composite.getCompositeSobjects, + getSobjectCollections: Composite.getSobjectCollections, + patchCompositeSobjects: Composite.patchCompositeSobjects, + }, + metadata: { + createSObjectRecord: Metadata.createSObjectRecord, + cloneRecord: Metadata.cloneRecord, + createCustomField: Metadata.createCustomField, + createCustomObject: Metadata.createCustomObject, + deleteSobject: Metadata.deleteSobject, + deleteSobjectRows: Metadata.deleteSobjectRows, + getSobjects: Metadata.getSobjects, + executeSobjectQuickAction: Metadata.executeSobjectQuickAction, + getApi: Metadata.getApi, + getChatterResources: Metadata.getChatterResources, + getSobjectPlatformaction: Metadata.getSobjectPlatformaction, + headQuickActions: Metadata.headQuickActions, + headSobjectsUserPassword: Metadata.headSobjectsUserPassword, + getPicklistValuesByRecordType: Metadata.getPicklistValuesByRecordType, + getAllFieldsForObject: Metadata.getAllFieldsForObject, + getAllCustomObjects: Metadata.getAllCustomObjects, + getSobjectsSobjectDescribeApprovallayouts: + Metadata.getSobjectsSobjectDescribeApprovallayouts, + getSobjectApprovalLayouts: Metadata.getSobjectApprovalLayouts, + getChildRecords: Metadata.getChildRecords, + getConsentAction: Metadata.getConsentAction, + headActionsCustom: Metadata.headActionsCustom, + listCustomInvocableActions: Metadata.listCustomInvocableActions, + getSupportedObjectsDirectory: Metadata.getSupportedObjectsDirectory, + getGlobalActions: Metadata.getGlobalActions, + headSobjectsGlobalDescribeLayouts: + Metadata.headSobjectsGlobalDescribeLayouts, + getSObjectsDescribeLayoutsRecordTypeId: + Metadata.getSObjectsDescribeLayoutsRecordTypeId, + getOrgLimits: Metadata.getOrgLimits, + headProcessRulesSObject: Metadata.headProcessRulesSObject, + headSobjectQuickActionDefaultValues: + Metadata.headSobjectQuickActionDefaultValues, + getQuickActions: Metadata.getQuickActions, + getRecordCounts: Metadata.getRecordCounts, + getSobjectRelationship: Metadata.getSobjectRelationship, + getSobjectQuickActionDefaultValues: + Metadata.getSobjectQuickActionDefaultValues, + getSObjectQuickActionDefaultValues: + Metadata.getSObjectQuickActionDefaultValues, + getSobjectByExternalId: Metadata.getSobjectByExternalId, + headSobjectsQuickAction: Metadata.headSobjectsQuickAction, + getSObjectRecord: Metadata.getSObjectRecord, + headActionsStandard: Metadata.headActionsStandard, + listStandardInvocableActions: Metadata.listStandardInvocableActions, + getSupport: Metadata.getSupport, + getSupportKnowledgeArticles: Metadata.getSupportKnowledgeArticles, + getTheme: Metadata.getTheme, + getSObjectsUpdated: Metadata.getSObjectsUpdated, + getUserInfo: Metadata.getUserInfo, + sobjectUserPassword: Metadata.sobjectUserPassword, + massTransferOwnership: Metadata.massTransferOwnership, + updateSobject: Metadata.updateSobject, + sobjectRowsUpdate: Metadata.sobjectRowsUpdate, + upsertSobjectByExternalId: Metadata.upsertSobjectByExternalId, + setUserPassword: Metadata.setUserPassword, + }, + uiApi: { + createARecord: UiApi.createARecord, + createRecordUiApi: UiApi.createRecordUiApi, + getUiapiListInfoAccountAllAccounts: + UiApi.getUiapiListInfoAccountAllAccounts, + getUiapiListInfoAccountSearchResult: + UiApi.getUiapiListInfoAccountSearchResult, + headAppmenuSalesforce1: UiApi.headAppmenuSalesforce1, + getCompactLayouts: UiApi.getCompactLayouts, + getListViewActions: UiApi.getListViewActions, + getUiapiListInfoAccountRecent: UiApi.getUiapiListInfoAccountRecent, + getUiApiListInfoRecent: UiApi.getUiApiListInfoRecent, + getUiapimruListInfoAccount: UiApi.getUiapimruListInfoAccount, + getUiApiMruListRecordsAccount: UiApi.getUiApiMruListRecordsAccount, + getUiapiActionsMruListAccount: UiApi.getUiapiActionsMruListAccount, + getMruListViewMetadata: UiApi.getMruListViewMetadata, + getUiApiAppsUserNavItems: UiApi.getUiApiAppsUserNavItems, + getAllNavigationItems: UiApi.getAllNavigationItems, + getApp: UiApi.getApp, + getApps: UiApi.getApps, + getListViewMetadataBatch: UiApi.getListViewMetadataBatch, + getRelatedListPreferencesBatch: UiApi.getRelatedListPreferencesBatch, + getLastSelectedApp: UiApi.getLastSelectedApp, + getListViewMetadataByName: UiApi.getListViewMetadataByName, + getListViewRecordsByName: UiApi.getListViewRecordsByName, + getListViewRecordsById: UiApi.getListViewRecordsById, + listViewResults: UiApi.listViewResults, + getListViewResults: UiApi.getListViewResults, + getObjectListViews: UiApi.getObjectListViews, + getSobjectListViews: UiApi.getSobjectListViews, + getUiApiActionsLookupAccount: UiApi.getUiApiActionsLookupAccount, + getUiapiLookupsOpportunityAccountId: + UiApi.getUiapiLookupsOpportunityAccountId, + getLookupFieldSuggestions: UiApi.getLookupFieldSuggestions, + getLookupSuggestionsOpportunityAccount: + UiApi.getLookupSuggestionsOpportunityAccount, + getLookupSuggestionsCaseContact: UiApi.getLookupSuggestionsCaseContact, + getMruListViewRecords: UiApi.getMruListViewRecords, + getPhotoActions: UiApi.getPhotoActions, + getRecordUiDataAndMetadata: UiApi.getRecordUiDataAndMetadata, + getRecordEditPageActions: UiApi.getRecordEditPageActions, + getUiApiActionsRecordRelatedList: UiApi.getUiApiActionsRecordRelatedList, + getRelatedListActions: UiApi.getRelatedListActions, + getRelatedListRecordsContacts: UiApi.getRelatedListRecordsContacts, + getUiapiRelatedListPreferences: UiApi.getUiapiRelatedListPreferences, + getSobjectListView: UiApi.getSobjectListView, + updateRecord: UiApi.updateRecord, + updateFavorite: UiApi.updateFavorite, + updateRelatedListPreferences: UiApi.updateRelatedListPreferences, + updateListViewPreferences: UiApi.updateListViewPreferences, + }, + files: { + getFileContent: Files.getFileContent, + getFileInformation: Files.getFileInformation, + getFileShares: Files.getFileShares, + deleteFile: Files.deleteFile, + uploadFile: Files.uploadFile, + }, + analyticsReports: { + getDashboard: AnalyticsReports.getDashboard, + listDashboards: AnalyticsReports.listDashboards, + listEmailTemplates: AnalyticsReports.listEmailTemplates, + listReports: AnalyticsReports.listReports, + runReport: AnalyticsReports.runReport, + listAnalyticsTemplates: AnalyticsReports.listAnalyticsTemplates, + getReportInstance: AnalyticsReports.getReportInstance, + getReport: AnalyticsReports.getReport, + queryReport: AnalyticsReports.queryReport, + }, +} as const; + +const salesforceWebhooksNested = { + accountCreatedOrUpdated, + contactUpdated, + newContact, + newLead, + newOrUpdatedOpportunity, + genericSObjectRecordUpdated, + taskCreatedOrCompleted, +} as const; + +const salesforceWebhookSchemas = { + accountCreatedOrUpdated: { + description: 'Account created or updated', + payload: SalesforceWebhookPayloadSchema, + response: z.object({ success: z.boolean() }), + }, + contactUpdated: { + description: 'Contact updated', + payload: SalesforceWebhookPayloadSchema, + response: z.object({ success: z.boolean() }), + }, + newContact: { + description: 'New contact created', + payload: SalesforceWebhookPayloadSchema, + response: z.object({ success: z.boolean() }), + }, + newLead: { + description: 'New lead created', + payload: SalesforceWebhookPayloadSchema, + response: z.object({ success: z.boolean() }), + }, + newOrUpdatedOpportunity: { + description: 'Opportunity created or updated', + payload: SalesforceWebhookPayloadSchema, + response: z.object({ success: z.boolean() }), + }, + genericSObjectRecordUpdated: { + description: 'Generic sObject record updated', + payload: SalesforceWebhookPayloadSchema, + response: z.object({ success: z.boolean() }), + }, + taskCreatedOrCompleted: { + description: 'Task created or completed', + payload: SalesforceWebhookPayloadSchema, + response: z.object({ success: z.boolean() }), + }, +} as const; + +export const salesforceEndpointSchemas = { + 'accounts.createAccount': { + input: SalesforceEndpointInputSchemas.createAccount, + output: SalesforceEndpointOutputSchemas.createAccount, + }, + 'accounts.getAccount': { + input: SalesforceEndpointInputSchemas.getAccount, + output: SalesforceEndpointOutputSchemas.getAccount, + }, + 'accounts.listAccounts': { + input: SalesforceEndpointInputSchemas.listAccounts, + output: SalesforceEndpointOutputSchemas.listAccounts, + }, + 'accounts.searchAccounts': { + input: SalesforceEndpointInputSchemas.searchAccounts, + output: SalesforceEndpointOutputSchemas.searchAccounts, + }, + 'accounts.updateAccount': { + input: SalesforceEndpointInputSchemas.updateAccount, + output: SalesforceEndpointOutputSchemas.updateAccount, + }, + 'accounts.updateAccountObjectById': { + input: SalesforceEndpointInputSchemas.updateAccountObjectById, + output: SalesforceEndpointOutputSchemas.updateAccountObjectById, + }, + 'accounts.deleteAccount': { + input: SalesforceEndpointInputSchemas.deleteAccount, + output: SalesforceEndpointOutputSchemas.deleteAccount, + }, + 'accounts.accountCreationWithContentTypeOption': { + input: SalesforceEndpointInputSchemas.accountCreationWithContentTypeOption, + output: + SalesforceEndpointOutputSchemas.accountCreationWithContentTypeOption, + }, + 'accounts.fetchAccountByIdWithQuery': { + input: SalesforceEndpointInputSchemas.fetchAccountByIdWithQuery, + output: SalesforceEndpointOutputSchemas.fetchAccountByIdWithQuery, + }, + 'accounts.removeAccountByUniqueIdentifier': { + input: SalesforceEndpointInputSchemas.removeAccountByUniqueIdentifier, + output: SalesforceEndpointOutputSchemas.removeAccountByUniqueIdentifier, + }, + 'accounts.retrieveAccountDataAndErrorResponses': { + input: SalesforceEndpointInputSchemas.retrieveAccountDataAndErrorResponses, + output: + SalesforceEndpointOutputSchemas.retrieveAccountDataAndErrorResponses, + }, + + 'contacts.createContact': { + input: SalesforceEndpointInputSchemas.createContact, + output: SalesforceEndpointOutputSchemas.createContact, + }, + 'contacts.getContact': { + input: SalesforceEndpointInputSchemas.getContact, + output: SalesforceEndpointOutputSchemas.getContact, + }, + 'contacts.listContacts': { + input: SalesforceEndpointInputSchemas.listContacts, + output: SalesforceEndpointOutputSchemas.listContacts, + }, + 'contacts.deleteContact': { + input: SalesforceEndpointInputSchemas.deleteContact, + output: SalesforceEndpointOutputSchemas.deleteContact, + }, + 'contacts.associateContactToAccount': { + input: SalesforceEndpointInputSchemas.associateContactToAccount, + output: SalesforceEndpointOutputSchemas.associateContactToAccount, + }, + 'contacts.updateContact': { + input: SalesforceEndpointInputSchemas.updateContact, + output: SalesforceEndpointOutputSchemas.updateContact, + }, + 'contacts.updateContactById': { + input: SalesforceEndpointInputSchemas.updateContactById, + output: SalesforceEndpointOutputSchemas.updateContactById, + }, + 'contacts.searchContacts': { + input: SalesforceEndpointInputSchemas.searchContacts, + output: SalesforceEndpointOutputSchemas.searchContacts, + }, + 'contacts.createNewContactWithJsonHeader': { + input: SalesforceEndpointInputSchemas.createNewContactWithJsonHeader, + output: SalesforceEndpointOutputSchemas.createNewContactWithJsonHeader, + }, + 'contacts.queryContactsByName': { + input: SalesforceEndpointInputSchemas.queryContactsByName, + output: SalesforceEndpointOutputSchemas.queryContactsByName, + }, + 'contacts.removeASpecificContactById': { + input: SalesforceEndpointInputSchemas.removeASpecificContactById, + output: SalesforceEndpointOutputSchemas.removeASpecificContactById, + }, + 'contacts.retrieveContactInfoWithStandardResponses': { + input: + SalesforceEndpointInputSchemas.retrieveContactInfoWithStandardResponses, + output: + SalesforceEndpointOutputSchemas.retrieveContactInfoWithStandardResponses, + }, + 'contacts.getContactById': { + input: SalesforceEndpointInputSchemas.getContactById, + output: SalesforceEndpointOutputSchemas.getContactById, + }, + + 'leads.createLead': { + input: SalesforceEndpointInputSchemas.createLead, + output: SalesforceEndpointOutputSchemas.createLead, + }, + 'leads.getLead': { + input: SalesforceEndpointInputSchemas.getLead, + output: SalesforceEndpointOutputSchemas.getLead, + }, + 'leads.listLeads': { + input: SalesforceEndpointInputSchemas.listLeads, + output: SalesforceEndpointOutputSchemas.listLeads, + }, + 'leads.deleteLead': { + input: SalesforceEndpointInputSchemas.deleteLead, + output: SalesforceEndpointOutputSchemas.deleteLead, + }, + 'leads.applyLeadAssignmentRules': { + input: SalesforceEndpointInputSchemas.applyLeadAssignmentRules, + output: SalesforceEndpointOutputSchemas.applyLeadAssignmentRules, + }, + 'leads.updateLead': { + input: SalesforceEndpointInputSchemas.updateLead, + output: SalesforceEndpointOutputSchemas.updateLead, + }, + 'leads.updateLeadByIdWithJsonPayload': { + input: SalesforceEndpointInputSchemas.updateLeadByIdWithJsonPayload, + output: SalesforceEndpointOutputSchemas.updateLeadByIdWithJsonPayload, + }, + 'leads.searchLeads': { + input: SalesforceEndpointInputSchemas.searchLeads, + output: SalesforceEndpointOutputSchemas.searchLeads, + }, + 'leads.createLeadWithSpecifiedContentType': { + input: SalesforceEndpointInputSchemas.createLeadWithSpecifiedContentType, + output: SalesforceEndpointOutputSchemas.createLeadWithSpecifiedContentType, + }, + 'leads.deleteALeadObjectByItsId': { + input: SalesforceEndpointInputSchemas.deleteALeadObjectByItsId, + output: SalesforceEndpointOutputSchemas.deleteALeadObjectByItsId, + }, + 'leads.retrieveLeadById': { + input: SalesforceEndpointInputSchemas.retrieveLeadById, + output: SalesforceEndpointOutputSchemas.retrieveLeadById, + }, + 'leads.retrieveLeadDataWithVariousResponses': { + input: SalesforceEndpointInputSchemas.retrieveLeadDataWithVariousResponses, + output: + SalesforceEndpointOutputSchemas.retrieveLeadDataWithVariousResponses, + }, + + 'opportunities.createOpportunity': { + input: SalesforceEndpointInputSchemas.createOpportunity, + output: SalesforceEndpointOutputSchemas.createOpportunity, + }, + 'opportunities.getOpportunity': { + input: SalesforceEndpointInputSchemas.getOpportunity, + output: SalesforceEndpointOutputSchemas.getOpportunity, + }, + 'opportunities.listOpportunities': { + input: SalesforceEndpointInputSchemas.listOpportunities, + output: SalesforceEndpointOutputSchemas.listOpportunities, + }, + 'opportunities.deleteOpportunity': { + input: SalesforceEndpointInputSchemas.deleteOpportunity, + output: SalesforceEndpointOutputSchemas.deleteOpportunity, + }, + 'opportunities.addOpportunityLineItem': { + input: SalesforceEndpointInputSchemas.addOpportunityLineItem, + output: SalesforceEndpointOutputSchemas.addOpportunityLineItem, + }, + 'opportunities.updateOpportunity': { + input: SalesforceEndpointInputSchemas.updateOpportunity, + output: SalesforceEndpointOutputSchemas.updateOpportunity, + }, + 'opportunities.updateOpportunityById': { + input: SalesforceEndpointInputSchemas.updateOpportunityById, + output: SalesforceEndpointOutputSchemas.updateOpportunityById, + }, + 'opportunities.searchOpportunities': { + input: SalesforceEndpointInputSchemas.searchOpportunities, + output: SalesforceEndpointOutputSchemas.searchOpportunities, + }, + 'opportunities.cloneOpportunityWithProducts': { + input: SalesforceEndpointInputSchemas.cloneOpportunityWithProducts, + output: SalesforceEndpointOutputSchemas.cloneOpportunityWithProducts, + }, + 'opportunities.listPricebookEntries': { + input: SalesforceEndpointInputSchemas.listPricebookEntries, + output: SalesforceEndpointOutputSchemas.listPricebookEntries, + }, + 'opportunities.listPricebooks': { + input: SalesforceEndpointInputSchemas.listPricebooks, + output: SalesforceEndpointOutputSchemas.listPricebooks, + }, + 'opportunities.createOpportunityRecord': { + input: SalesforceEndpointInputSchemas.createOpportunityRecord, + output: SalesforceEndpointOutputSchemas.createOpportunityRecord, + }, + 'opportunities.removeOpportunityById': { + input: SalesforceEndpointInputSchemas.removeOpportunityById, + output: SalesforceEndpointOutputSchemas.removeOpportunityById, + }, + 'opportunities.retrieveOpportunitiesData': { + input: SalesforceEndpointInputSchemas.retrieveOpportunitiesData, + output: SalesforceEndpointOutputSchemas.retrieveOpportunitiesData, + }, + 'opportunities.retrieveOpportunityByIdWithOptionalFields': { + input: + SalesforceEndpointInputSchemas.retrieveOpportunityByIdWithOptionalFields, + output: + SalesforceEndpointOutputSchemas.retrieveOpportunityByIdWithOptionalFields, + }, + + 'campaigns.createCampaign': { + input: SalesforceEndpointInputSchemas.createCampaign, + output: SalesforceEndpointOutputSchemas.createCampaign, + }, + 'campaigns.getCampaign': { + input: SalesforceEndpointInputSchemas.getCampaign, + output: SalesforceEndpointOutputSchemas.getCampaign, + }, + 'campaigns.listCampaigns': { + input: SalesforceEndpointInputSchemas.listCampaigns, + output: SalesforceEndpointOutputSchemas.listCampaigns, + }, + 'campaigns.deleteCampaign': { + input: SalesforceEndpointInputSchemas.deleteCampaign, + output: SalesforceEndpointOutputSchemas.deleteCampaign, + }, + 'campaigns.addContactToCampaign': { + input: SalesforceEndpointInputSchemas.addContactToCampaign, + output: SalesforceEndpointOutputSchemas.addContactToCampaign, + }, + 'campaigns.updateCampaign': { + input: SalesforceEndpointInputSchemas.updateCampaign, + output: SalesforceEndpointOutputSchemas.updateCampaign, + }, + 'campaigns.updateCampaignByIdWithJson': { + input: SalesforceEndpointInputSchemas.updateCampaignByIdWithJson, + output: SalesforceEndpointOutputSchemas.updateCampaignByIdWithJson, + }, + 'campaigns.addLeadToCampaign': { + input: SalesforceEndpointInputSchemas.addLeadToCampaign, + output: SalesforceEndpointOutputSchemas.addLeadToCampaign, + }, + 'campaigns.removeFromCampaign': { + input: SalesforceEndpointInputSchemas.removeFromCampaign, + output: SalesforceEndpointOutputSchemas.removeFromCampaign, + }, + 'campaigns.searchCampaigns': { + input: SalesforceEndpointInputSchemas.searchCampaigns, + output: SalesforceEndpointOutputSchemas.searchCampaigns, + }, + 'campaigns.createCampaignRecordViaPost': { + input: SalesforceEndpointInputSchemas.createCampaignRecordViaPost, + output: SalesforceEndpointOutputSchemas.createCampaignRecordViaPost, + }, + 'campaigns.removeCampaignObjectById': { + input: SalesforceEndpointInputSchemas.removeCampaignObjectById, + output: SalesforceEndpointOutputSchemas.removeCampaignObjectById, + }, + 'campaigns.retrieveCampaignDataWithErrorHandling': { + input: SalesforceEndpointInputSchemas.retrieveCampaignDataWithErrorHandling, + output: + SalesforceEndpointOutputSchemas.retrieveCampaignDataWithErrorHandling, + }, + 'campaigns.retrieveSpecificCampaignObjectDetails': { + input: SalesforceEndpointInputSchemas.retrieveSpecificCampaignObjectDetails, + output: + SalesforceEndpointOutputSchemas.retrieveSpecificCampaignObjectDetails, + }, + + 'notes.createNote': { + input: SalesforceEndpointInputSchemas.createNote, + output: SalesforceEndpointOutputSchemas.createNote, + }, + 'notes.updateNote': { + input: SalesforceEndpointInputSchemas.updateNote, + output: SalesforceEndpointOutputSchemas.updateNote, + }, + 'notes.updateSpecificNoteById': { + input: SalesforceEndpointInputSchemas.updateSpecificNoteById, + output: SalesforceEndpointOutputSchemas.updateSpecificNoteById, + }, + 'notes.searchNotes': { + input: SalesforceEndpointInputSchemas.searchNotes, + output: SalesforceEndpointOutputSchemas.searchNotes, + }, + 'notes.getNote': { + input: SalesforceEndpointInputSchemas.getNote, + output: SalesforceEndpointOutputSchemas.getNote, + }, + 'notes.listNotes': { + input: SalesforceEndpointInputSchemas.listNotes, + output: SalesforceEndpointOutputSchemas.listNotes, + }, + 'notes.deleteNote': { + input: SalesforceEndpointInputSchemas.deleteNote, + output: SalesforceEndpointOutputSchemas.deleteNote, + }, + 'notes.createNoteRecordWithContentTypeHeader': { + input: SalesforceEndpointInputSchemas.createNoteRecordWithContentTypeHeader, + output: + SalesforceEndpointOutputSchemas.createNoteRecordWithContentTypeHeader, + }, + 'notes.removeNoteObjectById': { + input: SalesforceEndpointInputSchemas.removeNoteObjectById, + output: SalesforceEndpointOutputSchemas.removeNoteObjectById, + }, + 'notes.getNoteByIdWithFields': { + input: SalesforceEndpointInputSchemas.getNoteByIdWithFields, + output: SalesforceEndpointOutputSchemas.getNoteByIdWithFields, + }, + 'notes.retrieveNoteObjectInformation': { + input: SalesforceEndpointInputSchemas.retrieveNoteObjectInformation, + output: SalesforceEndpointOutputSchemas.retrieveNoteObjectInformation, + }, + + 'tasks.createTask': { + input: SalesforceEndpointInputSchemas.createTask, + output: SalesforceEndpointOutputSchemas.createTask, + }, + 'tasks.completeTask': { + input: SalesforceEndpointInputSchemas.completeTask, + output: SalesforceEndpointOutputSchemas.completeTask, + }, + 'tasks.logCall': { + input: SalesforceEndpointInputSchemas.logCall, + output: SalesforceEndpointOutputSchemas.logCall, + }, + 'tasks.logEmailActivity': { + input: SalesforceEndpointInputSchemas.logEmailActivity, + output: SalesforceEndpointOutputSchemas.logEmailActivity, + }, + 'tasks.updateTask': { + input: SalesforceEndpointInputSchemas.updateTask, + output: SalesforceEndpointOutputSchemas.updateTask, + }, + 'tasks.searchTasks': { + input: SalesforceEndpointInputSchemas.searchTasks, + output: SalesforceEndpointOutputSchemas.searchTasks, + }, + 'tasks.sendEmail': { + input: SalesforceEndpointInputSchemas.sendEmail, + output: SalesforceEndpointOutputSchemas.sendEmail, + }, + 'tasks.sendEmailFromTemplate': { + input: SalesforceEndpointInputSchemas.sendEmailFromTemplate, + output: SalesforceEndpointOutputSchemas.sendEmailFromTemplate, + }, + 'tasks.sendMassEmail': { + input: SalesforceEndpointInputSchemas.sendMassEmail, + output: SalesforceEndpointOutputSchemas.sendMassEmail, + }, + + 'jobs.closeOrAbortJob': { + input: SalesforceEndpointInputSchemas.closeOrAbortJob, + output: SalesforceEndpointOutputSchemas.closeOrAbortJob, + }, + 'jobs.deleteJobQuery': { + input: SalesforceEndpointInputSchemas.deleteJobQuery, + output: SalesforceEndpointOutputSchemas.deleteJobQuery, + }, + 'jobs.getJobFailedRecordResults': { + input: SalesforceEndpointInputSchemas.getJobFailedRecordResults, + output: SalesforceEndpointOutputSchemas.getJobFailedRecordResults, + }, + 'jobs.getQueryJobInfo': { + input: SalesforceEndpointInputSchemas.getQueryJobInfo, + output: SalesforceEndpointOutputSchemas.getQueryJobInfo, + }, + 'jobs.getQueryJobResults': { + input: SalesforceEndpointInputSchemas.getQueryJobResults, + output: SalesforceEndpointOutputSchemas.getQueryJobResults, + }, + 'jobs.getJobSuccessfulRecordResults': { + input: SalesforceEndpointInputSchemas.getJobSuccessfulRecordResults, + output: SalesforceEndpointOutputSchemas.getJobSuccessfulRecordResults, + }, + 'jobs.getJobUnprocessedRecordResults': { + input: SalesforceEndpointInputSchemas.getJobUnprocessedRecordResults, + output: SalesforceEndpointOutputSchemas.getJobUnprocessedRecordResults, + }, + 'jobs.uploadJobData': { + input: SalesforceEndpointInputSchemas.uploadJobData, + output: SalesforceEndpointOutputSchemas.uploadJobData, + }, + + 'soqlSosl.runSoqlQuery': { + input: SalesforceEndpointInputSchemas.runSoqlQuery, + output: SalesforceEndpointOutputSchemas.runSoqlQuery, + }, + 'soqlSosl.queryAll': { + input: SalesforceEndpointInputSchemas.queryAll, + output: SalesforceEndpointOutputSchemas.queryAll, + }, + 'soqlSosl.search': { + input: SalesforceEndpointInputSchemas.search, + output: SalesforceEndpointOutputSchemas.search, + }, + 'soqlSosl.executeSoslSearch': { + input: SalesforceEndpointInputSchemas.executeSoslSearch, + output: SalesforceEndpointOutputSchemas.executeSoslSearch, + }, + 'soqlSosl.toolingQuery': { + input: SalesforceEndpointInputSchemas.toolingQuery, + output: SalesforceEndpointOutputSchemas.toolingQuery, + }, + 'soqlSosl.parameterizedSearch': { + input: SalesforceEndpointInputSchemas.parameterizedSearch, + output: SalesforceEndpointOutputSchemas.parameterizedSearch, + }, + 'soqlSosl.postParameterizedSearch': { + input: SalesforceEndpointInputSchemas.postParameterizedSearch, + output: SalesforceEndpointOutputSchemas.postParameterizedSearch, + }, + 'soqlSosl.getSearchLayout': { + input: SalesforceEndpointInputSchemas.getSearchLayout, + output: SalesforceEndpointOutputSchemas.getSearchLayout, + }, + 'soqlSosl.query': { + input: SalesforceEndpointInputSchemas.query, + output: SalesforceEndpointOutputSchemas.query, + }, + 'soqlSosl.executeSoqlQuery': { + input: SalesforceEndpointInputSchemas.executeSoqlQuery, + output: SalesforceEndpointOutputSchemas.executeSoqlQuery, + }, + 'soqlSosl.getSearchSuggestions': { + input: SalesforceEndpointInputSchemas.getSearchSuggestions, + output: SalesforceEndpointOutputSchemas.getSearchSuggestions, + }, + 'soqlSosl.searchKnowledgeArticles': { + input: SalesforceEndpointInputSchemas.searchKnowledgeArticles, + output: SalesforceEndpointOutputSchemas.searchKnowledgeArticles, + }, + 'soqlSosl.getParameterizedSearch': { + input: SalesforceEndpointInputSchemas.getParameterizedSearch, + output: SalesforceEndpointOutputSchemas.getParameterizedSearch, + }, + + 'composite.postCompositeSobjects': { + input: SalesforceEndpointInputSchemas.postCompositeSobjects, + output: SalesforceEndpointOutputSchemas.postCompositeSobjects, + }, + 'composite.createSobjectTree': { + input: SalesforceEndpointInputSchemas.createSobjectTree, + output: SalesforceEndpointOutputSchemas.createSobjectTree, + }, + 'composite.deleteSobjectCollections': { + input: SalesforceEndpointInputSchemas.deleteSobjectCollections, + output: SalesforceEndpointOutputSchemas.deleteSobjectCollections, + }, + 'composite.postCompositeGraph': { + input: SalesforceEndpointInputSchemas.postCompositeGraph, + output: SalesforceEndpointOutputSchemas.postCompositeGraph, + }, + 'composite.compositeGraphAction': { + input: SalesforceEndpointInputSchemas.compositeGraphAction, + output: SalesforceEndpointOutputSchemas.compositeGraphAction, + }, + 'composite.getABatchOfRecords': { + input: SalesforceEndpointInputSchemas.getABatchOfRecords, + output: SalesforceEndpointOutputSchemas.getABatchOfRecords, + }, + 'composite.getCompositeResources': { + input: SalesforceEndpointInputSchemas.getCompositeResources, + output: SalesforceEndpointOutputSchemas.getCompositeResources, + }, + 'composite.getCompositeSobjects': { + input: SalesforceEndpointInputSchemas.getCompositeSobjects, + output: SalesforceEndpointOutputSchemas.getCompositeSobjects, + }, + 'composite.getSobjectCollections': { + input: SalesforceEndpointInputSchemas.getSobjectCollections, + output: SalesforceEndpointOutputSchemas.getSobjectCollections, + }, + 'composite.patchCompositeSobjects': { + input: SalesforceEndpointInputSchemas.patchCompositeSobjects, + output: SalesforceEndpointOutputSchemas.patchCompositeSobjects, + }, + + 'metadata.createSObjectRecord': { + input: SalesforceEndpointInputSchemas.createSObjectRecord, + output: SalesforceEndpointOutputSchemas.createSObjectRecord, + }, + 'metadata.cloneRecord': { + input: SalesforceEndpointInputSchemas.cloneRecord, + output: SalesforceEndpointOutputSchemas.cloneRecord, + }, + 'metadata.createCustomField': { + input: SalesforceEndpointInputSchemas.createCustomField, + output: SalesforceEndpointOutputSchemas.createCustomField, + }, + 'metadata.createCustomObject': { + input: SalesforceEndpointInputSchemas.createCustomObject, + output: SalesforceEndpointOutputSchemas.createCustomObject, + }, + 'metadata.deleteSobject': { + input: SalesforceEndpointInputSchemas.deleteSobject, + output: SalesforceEndpointOutputSchemas.deleteSobject, + }, + 'metadata.deleteSobjectRows': { + input: SalesforceEndpointInputSchemas.deleteSobjectRows, + output: SalesforceEndpointOutputSchemas.deleteSobjectRows, + }, + 'metadata.getSobjects': { + input: SalesforceEndpointInputSchemas.getSobjects, + output: SalesforceEndpointOutputSchemas.getSobjects, + }, + 'metadata.executeSobjectQuickAction': { + input: SalesforceEndpointInputSchemas.executeSobjectQuickAction, + output: SalesforceEndpointOutputSchemas.executeSobjectQuickAction, + }, + 'metadata.getApi': { + input: SalesforceEndpointInputSchemas.getApi, + output: SalesforceEndpointOutputSchemas.getApi, + }, + 'metadata.getChatterResources': { + input: SalesforceEndpointInputSchemas.getChatterResources, + output: SalesforceEndpointOutputSchemas.getChatterResources, + }, + 'metadata.getSobjectPlatformaction': { + input: SalesforceEndpointInputSchemas.getSobjectPlatformaction, + output: SalesforceEndpointOutputSchemas.getSobjectPlatformaction, + }, + 'metadata.headQuickActions': { + input: SalesforceEndpointInputSchemas.headQuickActions, + output: SalesforceEndpointOutputSchemas.headQuickActions, + }, + 'metadata.headSobjectsUserPassword': { + input: SalesforceEndpointInputSchemas.headSobjectsUserPassword, + output: SalesforceEndpointOutputSchemas.headSobjectsUserPassword, + }, + 'metadata.getPicklistValuesByRecordType': { + input: SalesforceEndpointInputSchemas.getPicklistValuesByRecordType, + output: SalesforceEndpointOutputSchemas.getPicklistValuesByRecordType, + }, + 'metadata.getAllFieldsForObject': { + input: SalesforceEndpointInputSchemas.getAllFieldsForObject, + output: SalesforceEndpointOutputSchemas.getAllFieldsForObject, + }, + 'metadata.getAllCustomObjects': { + input: SalesforceEndpointInputSchemas.getAllCustomObjects, + output: SalesforceEndpointOutputSchemas.getAllCustomObjects, + }, + 'metadata.getSobjectsSobjectDescribeApprovallayouts': { + input: + SalesforceEndpointInputSchemas.getSobjectsSobjectDescribeApprovallayouts, + output: + SalesforceEndpointOutputSchemas.getSobjectsSobjectDescribeApprovallayouts, + }, + 'metadata.getSobjectApprovalLayouts': { + input: SalesforceEndpointInputSchemas.getSobjectApprovalLayouts, + output: SalesforceEndpointOutputSchemas.getSobjectApprovalLayouts, + }, + 'metadata.getChildRecords': { + input: SalesforceEndpointInputSchemas.getChildRecords, + output: SalesforceEndpointOutputSchemas.getChildRecords, + }, + 'metadata.getConsentAction': { + input: SalesforceEndpointInputSchemas.getConsentAction, + output: SalesforceEndpointOutputSchemas.getConsentAction, + }, + 'metadata.headActionsCustom': { + input: SalesforceEndpointInputSchemas.headActionsCustom, + output: SalesforceEndpointOutputSchemas.headActionsCustom, + }, + 'metadata.listCustomInvocableActions': { + input: SalesforceEndpointInputSchemas.listCustomInvocableActions, + output: SalesforceEndpointOutputSchemas.listCustomInvocableActions, + }, + 'metadata.getSupportedObjectsDirectory': { + input: SalesforceEndpointInputSchemas.getSupportedObjectsDirectory, + output: SalesforceEndpointOutputSchemas.getSupportedObjectsDirectory, + }, + 'metadata.getGlobalActions': { + input: SalesforceEndpointInputSchemas.getGlobalActions, + output: SalesforceEndpointOutputSchemas.getGlobalActions, + }, + 'metadata.headSobjectsGlobalDescribeLayouts': { + input: SalesforceEndpointInputSchemas.headSobjectsGlobalDescribeLayouts, + output: SalesforceEndpointOutputSchemas.headSobjectsGlobalDescribeLayouts, + }, + 'metadata.getSObjectsDescribeLayoutsRecordTypeId': { + input: + SalesforceEndpointInputSchemas.getSObjectsDescribeLayoutsRecordTypeId, + output: + SalesforceEndpointOutputSchemas.getSObjectsDescribeLayoutsRecordTypeId, + }, + 'metadata.getOrgLimits': { + input: SalesforceEndpointInputSchemas.getOrgLimits, + output: SalesforceEndpointOutputSchemas.getOrgLimits, + }, + 'metadata.headProcessRulesSObject': { + input: SalesforceEndpointInputSchemas.headProcessRulesSObject, + output: SalesforceEndpointOutputSchemas.headProcessRulesSObject, + }, + 'metadata.headSobjectQuickActionDefaultValues': { + input: SalesforceEndpointInputSchemas.headSobjectQuickActionDefaultValues, + output: SalesforceEndpointOutputSchemas.headSobjectQuickActionDefaultValues, + }, + 'metadata.getQuickActions': { + input: SalesforceEndpointInputSchemas.getQuickActions, + output: SalesforceEndpointOutputSchemas.getQuickActions, + }, + 'metadata.getRecordCounts': { + input: SalesforceEndpointInputSchemas.getRecordCounts, + output: SalesforceEndpointOutputSchemas.getRecordCounts, + }, + 'metadata.getSobjectRelationship': { + input: SalesforceEndpointInputSchemas.getSobjectRelationship, + output: SalesforceEndpointOutputSchemas.getSobjectRelationship, + }, + 'metadata.getSobjectQuickActionDefaultValues': { + input: SalesforceEndpointInputSchemas.getSobjectQuickActionDefaultValues, + output: SalesforceEndpointOutputSchemas.getSobjectQuickActionDefaultValues, + }, + 'metadata.getSObjectQuickActionDefaultValues': { + input: SalesforceEndpointInputSchemas.getSObjectQuickActionDefaultValues, + output: SalesforceEndpointOutputSchemas.getSObjectQuickActionDefaultValues, + }, + 'metadata.getSobjectByExternalId': { + input: SalesforceEndpointInputSchemas.getSobjectByExternalId, + output: SalesforceEndpointOutputSchemas.getSobjectByExternalId, + }, + 'metadata.headSobjectsQuickAction': { + input: SalesforceEndpointInputSchemas.headSobjectsQuickAction, + output: SalesforceEndpointOutputSchemas.headSobjectsQuickAction, + }, + 'metadata.getSObjectRecord': { + input: SalesforceEndpointInputSchemas.getSObjectRecord, + output: SalesforceEndpointOutputSchemas.getSObjectRecord, + }, + 'metadata.headActionsStandard': { + input: SalesforceEndpointInputSchemas.headActionsStandard, + output: SalesforceEndpointOutputSchemas.headActionsStandard, + }, + 'metadata.listStandardInvocableActions': { + input: SalesforceEndpointInputSchemas.listStandardInvocableActions, + output: SalesforceEndpointOutputSchemas.listStandardInvocableActions, + }, + 'metadata.getSupport': { + input: SalesforceEndpointInputSchemas.getSupport, + output: SalesforceEndpointOutputSchemas.getSupport, + }, + 'metadata.getSupportKnowledgeArticles': { + input: SalesforceEndpointInputSchemas.getSupportKnowledgeArticles, + output: SalesforceEndpointOutputSchemas.getSupportKnowledgeArticles, + }, + 'metadata.getTheme': { + input: SalesforceEndpointInputSchemas.getTheme, + output: SalesforceEndpointOutputSchemas.getTheme, + }, + 'metadata.getSObjectsUpdated': { + input: SalesforceEndpointInputSchemas.getSObjectsUpdated, + output: SalesforceEndpointOutputSchemas.getSObjectsUpdated, + }, + 'metadata.getUserInfo': { + input: SalesforceEndpointInputSchemas.getUserInfo, + output: SalesforceEndpointOutputSchemas.getUserInfo, + }, + 'metadata.sobjectUserPassword': { + input: SalesforceEndpointInputSchemas.sobjectUserPassword, + output: SalesforceEndpointOutputSchemas.sobjectUserPassword, + }, + 'metadata.massTransferOwnership': { + input: SalesforceEndpointInputSchemas.massTransferOwnership, + output: SalesforceEndpointOutputSchemas.massTransferOwnership, + }, + 'metadata.updateSobject': { + input: SalesforceEndpointInputSchemas.updateSobject, + output: SalesforceEndpointOutputSchemas.updateSobject, + }, + 'metadata.sobjectRowsUpdate': { + input: SalesforceEndpointInputSchemas.sobjectRowsUpdate, + output: SalesforceEndpointOutputSchemas.sobjectRowsUpdate, + }, + 'metadata.upsertSobjectByExternalId': { + input: SalesforceEndpointInputSchemas.upsertSobjectByExternalId, + output: SalesforceEndpointOutputSchemas.upsertSobjectByExternalId, + }, + 'metadata.setUserPassword': { + input: SalesforceEndpointInputSchemas.setUserPassword, + output: SalesforceEndpointOutputSchemas.setUserPassword, + }, + + 'uiApi.createARecord': { + input: SalesforceEndpointInputSchemas.createARecord, + output: SalesforceEndpointOutputSchemas.createARecord, + }, + 'uiApi.createRecordUiApi': { + input: SalesforceEndpointInputSchemas.createRecordUiApi, + output: SalesforceEndpointOutputSchemas.createRecordUiApi, + }, + 'uiApi.getUiapiListInfoAccountAllAccounts': { + input: SalesforceEndpointInputSchemas.getUiapiListInfoAccountAllAccounts, + output: SalesforceEndpointOutputSchemas.getUiapiListInfoAccountAllAccounts, + }, + 'uiApi.getUiapiListInfoAccountSearchResult': { + input: SalesforceEndpointInputSchemas.getUiapiListInfoAccountSearchResult, + output: SalesforceEndpointOutputSchemas.getUiapiListInfoAccountSearchResult, + }, + 'uiApi.headAppmenuSalesforce1': { + input: SalesforceEndpointInputSchemas.headAppmenuSalesforce1, + output: SalesforceEndpointOutputSchemas.headAppmenuSalesforce1, + }, + 'uiApi.getCompactLayouts': { + input: SalesforceEndpointInputSchemas.getCompactLayouts, + output: SalesforceEndpointOutputSchemas.getCompactLayouts, + }, + 'uiApi.getListViewActions': { + input: SalesforceEndpointInputSchemas.getListViewActions, + output: SalesforceEndpointOutputSchemas.getListViewActions, + }, + 'uiApi.getUiapiListInfoAccountRecent': { + input: SalesforceEndpointInputSchemas.getUiapiListInfoAccountRecent, + output: SalesforceEndpointOutputSchemas.getUiapiListInfoAccountRecent, + }, + 'uiApi.getUiApiListInfoRecent': { + input: SalesforceEndpointInputSchemas.getUiApiListInfoRecent, + output: SalesforceEndpointOutputSchemas.getUiApiListInfoRecent, + }, + 'uiApi.getUiapimruListInfoAccount': { + input: SalesforceEndpointInputSchemas.getUiapimruListInfoAccount, + output: SalesforceEndpointOutputSchemas.getUiapimruListInfoAccount, + }, + 'uiApi.getUiApiMruListRecordsAccount': { + input: SalesforceEndpointInputSchemas.getUiApiMruListRecordsAccount, + output: SalesforceEndpointOutputSchemas.getUiApiMruListRecordsAccount, + }, + 'uiApi.getUiapiActionsMruListAccount': { + input: SalesforceEndpointInputSchemas.getUiapiActionsMruListAccount, + output: SalesforceEndpointOutputSchemas.getUiapiActionsMruListAccount, + }, + 'uiApi.getMruListViewMetadata': { + input: SalesforceEndpointInputSchemas.getMruListViewMetadata, + output: SalesforceEndpointOutputSchemas.getMruListViewMetadata, + }, + 'uiApi.getUiApiAppsUserNavItems': { + input: SalesforceEndpointInputSchemas.getUiApiAppsUserNavItems, + output: SalesforceEndpointOutputSchemas.getUiApiAppsUserNavItems, + }, + 'uiApi.getAllNavigationItems': { + input: SalesforceEndpointInputSchemas.getAllNavigationItems, + output: SalesforceEndpointOutputSchemas.getAllNavigationItems, + }, + 'uiApi.getApp': { + input: SalesforceEndpointInputSchemas.getApp, + output: SalesforceEndpointOutputSchemas.getApp, + }, + 'uiApi.getApps': { + input: SalesforceEndpointInputSchemas.getApps, + output: SalesforceEndpointOutputSchemas.getApps, + }, + 'uiApi.getListViewMetadataBatch': { + input: SalesforceEndpointInputSchemas.getListViewMetadataBatch, + output: SalesforceEndpointOutputSchemas.getListViewMetadataBatch, + }, + 'uiApi.getRelatedListPreferencesBatch': { + input: SalesforceEndpointInputSchemas.getRelatedListPreferencesBatch, + output: SalesforceEndpointOutputSchemas.getRelatedListPreferencesBatch, + }, + 'uiApi.getLastSelectedApp': { + input: SalesforceEndpointInputSchemas.getLastSelectedApp, + output: SalesforceEndpointOutputSchemas.getLastSelectedApp, + }, + 'uiApi.getListViewMetadataByName': { + input: SalesforceEndpointInputSchemas.getListViewMetadataByName, + output: SalesforceEndpointOutputSchemas.getListViewMetadataByName, + }, + 'uiApi.getListViewRecordsByName': { + input: SalesforceEndpointInputSchemas.getListViewRecordsByName, + output: SalesforceEndpointOutputSchemas.getListViewRecordsByName, + }, + 'uiApi.getListViewRecordsById': { + input: SalesforceEndpointInputSchemas.getListViewRecordsById, + output: SalesforceEndpointOutputSchemas.getListViewRecordsById, + }, + 'uiApi.listViewResults': { + input: SalesforceEndpointInputSchemas.listViewResults, + output: SalesforceEndpointOutputSchemas.listViewResults, + }, + 'uiApi.getListViewResults': { + input: SalesforceEndpointInputSchemas.getListViewResults, + output: SalesforceEndpointOutputSchemas.getListViewResults, + }, + 'uiApi.getObjectListViews': { + input: SalesforceEndpointInputSchemas.getObjectListViews, + output: SalesforceEndpointOutputSchemas.getObjectListViews, + }, + 'uiApi.getSobjectListViews': { + input: SalesforceEndpointInputSchemas.getSobjectListViews, + output: SalesforceEndpointOutputSchemas.getSobjectListViews, + }, + 'uiApi.getUiApiActionsLookupAccount': { + input: SalesforceEndpointInputSchemas.getUiApiActionsLookupAccount, + output: SalesforceEndpointOutputSchemas.getUiApiActionsLookupAccount, + }, + 'uiApi.getUiapiLookupsOpportunityAccountId': { + input: SalesforceEndpointInputSchemas.getUiapiLookupsOpportunityAccountId, + output: SalesforceEndpointOutputSchemas.getUiapiLookupsOpportunityAccountId, + }, + 'uiApi.getLookupFieldSuggestions': { + input: SalesforceEndpointInputSchemas.getLookupFieldSuggestions, + output: SalesforceEndpointOutputSchemas.getLookupFieldSuggestions, + }, + 'uiApi.getLookupSuggestionsOpportunityAccount': { + input: + SalesforceEndpointInputSchemas.getLookupSuggestionsOpportunityAccount, + output: + SalesforceEndpointOutputSchemas.getLookupSuggestionsOpportunityAccount, + }, + 'uiApi.getLookupSuggestionsCaseContact': { + input: SalesforceEndpointInputSchemas.getLookupSuggestionsCaseContact, + output: SalesforceEndpointOutputSchemas.getLookupSuggestionsCaseContact, + }, + 'uiApi.getMruListViewRecords': { + input: SalesforceEndpointInputSchemas.getMruListViewRecords, + output: SalesforceEndpointOutputSchemas.getMruListViewRecords, + }, + 'uiApi.getPhotoActions': { + input: SalesforceEndpointInputSchemas.getPhotoActions, + output: SalesforceEndpointOutputSchemas.getPhotoActions, + }, + 'uiApi.getRecordUiDataAndMetadata': { + input: SalesforceEndpointInputSchemas.getRecordUiDataAndMetadata, + output: SalesforceEndpointOutputSchemas.getRecordUiDataAndMetadata, + }, + 'uiApi.getRecordEditPageActions': { + input: SalesforceEndpointInputSchemas.getRecordEditPageActions, + output: SalesforceEndpointOutputSchemas.getRecordEditPageActions, + }, + 'uiApi.getUiApiActionsRecordRelatedList': { + input: SalesforceEndpointInputSchemas.getUiApiActionsRecordRelatedList, + output: SalesforceEndpointOutputSchemas.getUiApiActionsRecordRelatedList, + }, + 'uiApi.getRelatedListActions': { + input: SalesforceEndpointInputSchemas.getRelatedListActions, + output: SalesforceEndpointOutputSchemas.getRelatedListActions, + }, + 'uiApi.getRelatedListRecordsContacts': { + input: SalesforceEndpointInputSchemas.getRelatedListRecordsContacts, + output: SalesforceEndpointOutputSchemas.getRelatedListRecordsContacts, + }, + 'uiApi.getUiapiRelatedListPreferences': { + input: SalesforceEndpointInputSchemas.getUiapiRelatedListPreferences, + output: SalesforceEndpointOutputSchemas.getUiapiRelatedListPreferences, + }, + 'uiApi.getSobjectListView': { + input: SalesforceEndpointInputSchemas.getSobjectListView, + output: SalesforceEndpointOutputSchemas.getSobjectListView, + }, + 'uiApi.updateRecord': { + input: SalesforceEndpointInputSchemas.updateRecord, + output: SalesforceEndpointOutputSchemas.updateRecord, + }, + 'uiApi.updateFavorite': { + input: SalesforceEndpointInputSchemas.updateFavorite, + output: SalesforceEndpointOutputSchemas.updateFavorite, + }, + 'uiApi.updateRelatedListPreferences': { + input: SalesforceEndpointInputSchemas.updateRelatedListPreferences, + output: SalesforceEndpointOutputSchemas.updateRelatedListPreferences, + }, + 'uiApi.updateListViewPreferences': { + input: SalesforceEndpointInputSchemas.updateListViewPreferences, + output: SalesforceEndpointOutputSchemas.updateListViewPreferences, + }, + + 'files.getFileContent': { + input: SalesforceEndpointInputSchemas.getFileContent, + output: SalesforceEndpointOutputSchemas.getFileContent, + }, + 'files.getFileInformation': { + input: SalesforceEndpointInputSchemas.getFileInformation, + output: SalesforceEndpointOutputSchemas.getFileInformation, + }, + 'files.getFileShares': { + input: SalesforceEndpointInputSchemas.getFileShares, + output: SalesforceEndpointOutputSchemas.getFileShares, + }, + 'files.deleteFile': { + input: SalesforceEndpointInputSchemas.deleteFile, + output: SalesforceEndpointOutputSchemas.deleteFile, + }, + 'files.uploadFile': { + input: SalesforceEndpointInputSchemas.uploadFile, + output: SalesforceEndpointOutputSchemas.uploadFile, + }, + + 'analyticsReports.getDashboard': { + input: SalesforceEndpointInputSchemas.getDashboard, + output: SalesforceEndpointOutputSchemas.getDashboard, + }, + 'analyticsReports.listDashboards': { + input: SalesforceEndpointInputSchemas.listDashboards, + output: SalesforceEndpointOutputSchemas.listDashboards, + }, + 'analyticsReports.listEmailTemplates': { + input: SalesforceEndpointInputSchemas.listEmailTemplates, + output: SalesforceEndpointOutputSchemas.listEmailTemplates, + }, + 'analyticsReports.listReports': { + input: SalesforceEndpointInputSchemas.listReports, + output: SalesforceEndpointOutputSchemas.listReports, + }, + 'analyticsReports.runReport': { + input: SalesforceEndpointInputSchemas.runReport, + output: SalesforceEndpointOutputSchemas.runReport, + }, + 'analyticsReports.listAnalyticsTemplates': { + input: SalesforceEndpointInputSchemas.listAnalyticsTemplates, + output: SalesforceEndpointOutputSchemas.listAnalyticsTemplates, + }, + 'analyticsReports.getReportInstance': { + input: SalesforceEndpointInputSchemas.getReportInstance, + output: SalesforceEndpointOutputSchemas.getReportInstance, + }, + 'analyticsReports.getReport': { + input: SalesforceEndpointInputSchemas.getReport, + output: SalesforceEndpointOutputSchemas.getReport, + }, + 'analyticsReports.queryReport': { + input: SalesforceEndpointInputSchemas.queryReport, + output: SalesforceEndpointOutputSchemas.queryReport, + }, +} as const satisfies RequiredPluginEndpointSchemas< + typeof salesforceEndpointsNested +>; + +const defaultAuthType: AuthTypes = 'oauth_2' as const; + +const salesforceEndpointMeta = { + 'accounts.createAccount': { + riskLevel: 'write', + description: 'Create account in Salesforce', + }, + 'accounts.getAccount': { + riskLevel: 'read', + description: 'Get account by ID', + }, + 'accounts.listAccounts': { + riskLevel: 'read', + description: 'List accounts', + }, + 'accounts.searchAccounts': { + riskLevel: 'read', + description: 'Search accounts', + }, + 'accounts.updateAccount': { + riskLevel: 'write', + description: 'Update account', + }, + 'accounts.updateAccountObjectById': { + riskLevel: 'write', + description: 'Update account by id (deprecated)', + }, + 'accounts.deleteAccount': { + riskLevel: 'destructive', + irreversible: true, + description: 'Delete account', + }, + 'accounts.accountCreationWithContentTypeOption': { + riskLevel: 'write', + description: 'Create account (deprecated)', + }, + 'accounts.fetchAccountByIdWithQuery': { + riskLevel: 'read', + description: 'Fetch account by ID with query (deprecated)', + }, + 'accounts.removeAccountByUniqueIdentifier': { + riskLevel: 'destructive', + irreversible: true, + description: 'Remove account by unique identifier (deprecated)', + }, + 'accounts.retrieveAccountDataAndErrorResponses': { + riskLevel: 'read', + description: 'Retrieve account data and error responses (deprecated)', + }, + + 'contacts.createContact': { + riskLevel: 'write', + description: 'Create contact', + }, + 'contacts.getContact': { + riskLevel: 'read', + description: 'Get contact by ID', + }, + 'contacts.listContacts': { + riskLevel: 'read', + description: 'List contacts', + }, + 'contacts.deleteContact': { + riskLevel: 'destructive', + irreversible: true, + description: 'Delete contact', + }, + 'contacts.associateContactToAccount': { + riskLevel: 'write', + description: 'Associate contact to account', + }, + 'contacts.updateContact': { + riskLevel: 'write', + description: 'Update contact', + }, + 'contacts.updateContactById': { + riskLevel: 'write', + description: 'Update contact by id (deprecated)', + }, + 'contacts.searchContacts': { + riskLevel: 'read', + description: 'Search contacts', + }, + 'contacts.createNewContactWithJsonHeader': { + riskLevel: 'write', + description: 'Create new contact with JSON header (deprecated)', + }, + 'contacts.queryContactsByName': { + riskLevel: 'read', + description: 'Query contacts by name (deprecated)', + }, + 'contacts.removeASpecificContactById': { + riskLevel: 'destructive', + irreversible: true, + description: 'Remove contact by ID (deprecated)', + }, + 'contacts.retrieveContactInfoWithStandardResponses': { + riskLevel: 'read', + description: 'Retrieve contact info (deprecated)', + }, + 'contacts.getContactById': { + riskLevel: 'read', + description: 'Get contact by ID', + }, + + 'leads.createLead': { + riskLevel: 'write', + description: 'Create lead', + }, + 'leads.getLead': { + riskLevel: 'read', + description: 'Get lead by ID', + }, + 'leads.listLeads': { + riskLevel: 'read', + description: 'List leads', + }, + 'leads.deleteLead': { + riskLevel: 'destructive', + irreversible: true, + description: 'Delete lead', + }, + 'leads.applyLeadAssignmentRules': { + riskLevel: 'write', + description: 'Apply lead assignment rules', + }, + 'leads.updateLead': { + riskLevel: 'write', + description: 'Update lead', + }, + 'leads.updateLeadByIdWithJsonPayload': { + riskLevel: 'write', + description: 'Update lead by id (deprecated)', + }, + 'leads.searchLeads': { + riskLevel: 'read', + description: 'Search leads', + }, + 'leads.createLeadWithSpecifiedContentType': { + riskLevel: 'write', + description: 'Create lead with content type (deprecated)', + }, + 'leads.deleteALeadObjectByItsId': { + riskLevel: 'destructive', + irreversible: true, + description: 'Delete lead object by ID (deprecated)', + }, + 'leads.retrieveLeadById': { + riskLevel: 'read', + description: 'Retrieve lead by ID', + }, + 'leads.retrieveLeadDataWithVariousResponses': { + riskLevel: 'read', + description: 'Retrieve lead data (deprecated)', + }, + + 'opportunities.createOpportunity': { + riskLevel: 'write', + description: 'Create opportunity', + }, + 'opportunities.getOpportunity': { + riskLevel: 'read', + description: 'Get opportunity by ID', + }, + 'opportunities.listOpportunities': { + riskLevel: 'read', + description: 'List opportunities', + }, + 'opportunities.deleteOpportunity': { + riskLevel: 'destructive', + irreversible: true, + description: 'Delete opportunity', + }, + 'opportunities.addOpportunityLineItem': { + riskLevel: 'write', + description: 'Add line item to opportunity', + }, + 'opportunities.updateOpportunity': { + riskLevel: 'write', + description: 'Update opportunity', + }, + 'opportunities.updateOpportunityById': { + riskLevel: 'write', + description: 'Update opportunity by id (deprecated)', + }, + 'opportunities.searchOpportunities': { + riskLevel: 'read', + description: 'Search opportunities', + }, + 'opportunities.cloneOpportunityWithProducts': { + riskLevel: 'write', + description: 'Clone opportunity with products', + }, + 'opportunities.listPricebookEntries': { + riskLevel: 'read', + description: 'List pricebook entries', + }, + 'opportunities.listPricebooks': { + riskLevel: 'read', + description: 'List pricebooks', + }, + 'opportunities.createOpportunityRecord': { + riskLevel: 'write', + description: 'Create opportunity record (deprecated)', + }, + 'opportunities.removeOpportunityById': { + riskLevel: 'destructive', + irreversible: true, + description: 'Remove opportunity by ID (deprecated)', + }, + 'opportunities.retrieveOpportunitiesData': { + riskLevel: 'read', + description: 'Retrieve opportunities data', + }, + 'opportunities.retrieveOpportunityByIdWithOptionalFields': { + riskLevel: 'read', + description: 'Retrieve opportunity by ID with fields (deprecated)', + }, + + 'campaigns.createCampaign': { + riskLevel: 'write', + description: 'Create campaign', + }, + 'campaigns.getCampaign': { + riskLevel: 'read', + description: 'Get campaign by ID', + }, + 'campaigns.listCampaigns': { + riskLevel: 'read', + description: 'List campaigns', + }, + 'campaigns.deleteCampaign': { + riskLevel: 'destructive', + irreversible: true, + description: 'Delete campaign', + }, + 'campaigns.addContactToCampaign': { + riskLevel: 'write', + description: 'Add contact to campaign', + }, + 'campaigns.updateCampaign': { + riskLevel: 'write', + description: 'Update campaign', + }, + 'campaigns.updateCampaignByIdWithJson': { + riskLevel: 'write', + description: 'Update campaign by id (deprecated)', + }, + 'campaigns.addLeadToCampaign': { + riskLevel: 'write', + description: 'Add lead to campaign', + }, + 'campaigns.removeFromCampaign': { + riskLevel: 'destructive', + irreversible: true, + description: 'Remove member from campaign', + }, + 'campaigns.searchCampaigns': { + riskLevel: 'read', + description: 'Search campaigns', + }, + 'campaigns.createCampaignRecordViaPost': { + riskLevel: 'write', + description: 'Create campaign record via POST (deprecated)', + }, + 'campaigns.removeCampaignObjectById': { + riskLevel: 'destructive', + irreversible: true, + description: 'Remove campaign object by ID (deprecated)', + }, + 'campaigns.retrieveCampaignDataWithErrorHandling': { + riskLevel: 'read', + description: 'Retrieve campaign data (deprecated)', + }, + 'campaigns.retrieveSpecificCampaignObjectDetails': { + riskLevel: 'read', + description: 'Retrieve specific campaign details (deprecated)', + }, + + 'notes.createNote': { + riskLevel: 'write', + description: 'Create note', + }, + 'notes.updateNote': { + riskLevel: 'write', + description: 'Update note', + }, + 'notes.updateSpecificNoteById': { + riskLevel: 'write', + description: 'Update note by id (deprecated)', + }, + 'notes.searchNotes': { + riskLevel: 'read', + description: 'Search notes', + }, + 'notes.getNote': { + riskLevel: 'read', + description: 'Get note by ID', + }, + 'notes.listNotes': { + riskLevel: 'read', + description: 'List notes', + }, + 'notes.deleteNote': { + riskLevel: 'destructive', + irreversible: true, + description: 'Delete note', + }, + 'notes.createNoteRecordWithContentTypeHeader': { + riskLevel: 'write', + description: 'Create note record (deprecated)', + }, + 'notes.removeNoteObjectById': { + riskLevel: 'destructive', + irreversible: true, + description: 'Remove note object by ID (deprecated)', + }, + 'notes.getNoteByIdWithFields': { + riskLevel: 'read', + description: 'Get note by ID with fields (deprecated)', + }, + 'notes.retrieveNoteObjectInformation': { + riskLevel: 'read', + description: 'Retrieve note object info (deprecated)', + }, + + 'tasks.createTask': { + riskLevel: 'write', + description: 'Create task', + }, + 'tasks.completeTask': { + riskLevel: 'write', + description: 'Complete task', + }, + 'tasks.logCall': { + riskLevel: 'write', + description: 'Log phone call activity', + }, + 'tasks.logEmailActivity': { + riskLevel: 'write', + description: 'Log email activity', + }, + 'tasks.updateTask': { + riskLevel: 'write', + description: 'Update task', + }, + 'tasks.searchTasks': { + riskLevel: 'read', + description: 'Search tasks', + }, + 'tasks.sendEmail': { + riskLevel: 'write', + description: 'Send email', + }, + 'tasks.sendEmailFromTemplate': { + riskLevel: 'write', + description: 'Send email from template', + }, + 'tasks.sendMassEmail': { + riskLevel: 'write', + description: 'Send mass email', + }, + + 'jobs.closeOrAbortJob': { + riskLevel: 'write', + description: 'Close or abort bulk job', + }, + 'jobs.deleteJobQuery': { + riskLevel: 'destructive', + irreversible: true, + description: 'Delete query job', + }, + 'jobs.getJobFailedRecordResults': { + riskLevel: 'read', + description: 'Get job failed record results', + }, + 'jobs.getQueryJobInfo': { + riskLevel: 'read', + description: 'Get query job info', + }, + 'jobs.getQueryJobResults': { + riskLevel: 'read', + description: 'Get query job results', + }, + 'jobs.getJobSuccessfulRecordResults': { + riskLevel: 'read', + description: 'Get job successful record results', + }, + 'jobs.getJobUnprocessedRecordResults': { + riskLevel: 'read', + description: 'Get job unprocessed record results', + }, + 'jobs.uploadJobData': { + riskLevel: 'write', + description: 'Upload CSV data to a bulk ingest job', + }, + + 'soqlSosl.runSoqlQuery': { + riskLevel: 'read', + description: 'Run SOQL query', + }, + 'soqlSosl.queryAll': { + riskLevel: 'read', + description: 'Run queryAll including deleted records', + }, + 'soqlSosl.search': { + riskLevel: 'read', + description: 'Run SOSL search', + }, + 'soqlSosl.executeSoslSearch': { + riskLevel: 'read', + description: 'Execute SOSL search', + }, + 'soqlSosl.toolingQuery': { + riskLevel: 'read', + description: 'Run Tooling API SOQL query', + }, + 'soqlSosl.parameterizedSearch': { + riskLevel: 'read', + description: 'Run parameterized search', + }, + 'soqlSosl.postParameterizedSearch': { + riskLevel: 'read', + description: 'Post parameterized search', + }, + 'soqlSosl.getSearchLayout': { + riskLevel: 'read', + description: 'Get search layout', + }, + 'soqlSosl.query': { + riskLevel: 'read', + description: 'Execute SOQL query (deprecated)', + }, + 'soqlSosl.executeSoqlQuery': { + riskLevel: 'read', + description: 'Execute SOQL query (deprecated)', + }, + 'soqlSosl.getSearchSuggestions': { + riskLevel: 'read', + description: 'Get search suggestions', + }, + 'soqlSosl.searchKnowledgeArticles': { + riskLevel: 'read', + description: 'Search knowledge articles', + }, + 'soqlSosl.getParameterizedSearch': { + riskLevel: 'read', + description: 'Parameterized search via GET', + }, + + 'composite.postCompositeSobjects': { + riskLevel: 'write', + description: 'Create records using sObject Collections', + }, + 'composite.createSobjectTree': { + riskLevel: 'write', + description: 'Create sObject tree', + }, + 'composite.deleteSobjectCollections': { + riskLevel: 'destructive', + irreversible: true, + description: 'Delete sObject collections', + }, + 'composite.postCompositeGraph': { + riskLevel: 'write', + description: 'Execute composite graph', + }, + 'composite.compositeGraphAction': { + riskLevel: 'write', + description: 'Execute composite graph (deprecated)', + }, + 'composite.getABatchOfRecords': { + riskLevel: 'read', + description: 'Get batch of UI API records', + }, + 'composite.getCompositeResources': { + riskLevel: 'read', + description: 'Get composite resources', + }, + 'composite.getCompositeSobjects': { + riskLevel: 'read', + description: 'Get composite sObjects', + }, + 'composite.getSobjectCollections': { + riskLevel: 'read', + description: 'Get sObject collections', + }, + 'composite.patchCompositeSobjects': { + riskLevel: 'write', + description: 'Upsert records using external ID', + }, + + 'metadata.createSObjectRecord': { + riskLevel: 'write', + description: 'Create sObject record', + }, + 'metadata.cloneRecord': { + riskLevel: 'write', + description: 'Clone record', + }, + 'metadata.createCustomField': { + riskLevel: 'write', + description: 'Create custom field via Tooling API', + }, + 'metadata.createCustomObject': { + riskLevel: 'write', + description: 'Create custom object via Metadata API', + }, + 'metadata.deleteSobject': { + riskLevel: 'destructive', + irreversible: true, + description: 'Delete sObject record', + }, + 'metadata.deleteSobjectRows': { + riskLevel: 'destructive', + irreversible: true, + description: 'Delete sObject rows', + }, + 'metadata.getSobjects': { + riskLevel: 'read', + description: 'Describe global sObjects', + }, + 'metadata.executeSobjectQuickAction': { + riskLevel: 'write', + description: 'Execute sObject quick action', + }, + 'metadata.getApi': { + riskLevel: 'read', + description: 'Get API resources by version', + }, + 'metadata.getChatterResources': { + riskLevel: 'read', + description: 'Get Chatter resources', + }, + 'metadata.getSobjectPlatformaction': { + riskLevel: 'read', + description: 'Get PlatformAction metadata', + }, + 'metadata.headQuickActions': { + riskLevel: 'read', + description: 'Head Quick Actions', + }, + 'metadata.headSobjectsUserPassword': { + riskLevel: 'read', + description: 'Head user password status', + }, + 'metadata.getPicklistValuesByRecordType': { + riskLevel: 'read', + description: 'Get picklist values by record type', + }, + 'metadata.getAllFieldsForObject': { + riskLevel: 'read', + description: 'Get all fields for object', + }, + 'metadata.getAllCustomObjects': { + riskLevel: 'read', + description: 'Get all custom objects', + }, + 'metadata.getSobjectsSobjectDescribeApprovallayouts': { + riskLevel: 'read', + description: 'Get approval layouts for object', + }, + 'metadata.getSobjectApprovalLayouts': { + riskLevel: 'read', + description: 'Get approval layouts for sObject', + }, + 'metadata.getChildRecords': { + riskLevel: 'read', + description: 'Get child records', + }, + 'metadata.getConsentAction': { + riskLevel: 'read', + description: 'Get consent action preferences', + }, + 'metadata.headActionsCustom': { + riskLevel: 'read', + description: 'Head custom actions', + }, + 'metadata.listCustomInvocableActions': { + riskLevel: 'read', + description: 'List custom invocable actions', + }, + 'metadata.getSupportedObjectsDirectory': { + riskLevel: 'read', + description: 'Get supported objects directory', + }, + 'metadata.getGlobalActions': { + riskLevel: 'read', + description: 'Get global actions', + }, + 'metadata.headSobjectsGlobalDescribeLayouts': { + riskLevel: 'read', + description: 'Head global describe layouts', + }, + 'metadata.getSObjectsDescribeLayoutsRecordTypeId': { + riskLevel: 'read', + description: 'Get layouts for object with record type', + }, + 'metadata.getOrgLimits': { + riskLevel: 'read', + description: 'Get org limits', + }, + 'metadata.headProcessRulesSObject': { + riskLevel: 'read', + description: 'Head process rules for sObject', + }, + 'metadata.headSobjectQuickActionDefaultValues': { + riskLevel: 'read', + description: 'Head quick action default values', + }, + 'metadata.getQuickActions': { + riskLevel: 'read', + description: 'Get quick actions', + }, + 'metadata.getRecordCounts': { + riskLevel: 'read', + description: 'Get record counts', + }, + 'metadata.getSobjectRelationship': { + riskLevel: 'read', + description: 'Get sObject relationship', + }, + 'metadata.getSobjectQuickActionDefaultValues': { + riskLevel: 'read', + description: 'Get quick action default values', + }, + 'metadata.getSObjectQuickActionDefaultValues': { + riskLevel: 'read', + description: 'Get quick action default values by ID', + }, + 'metadata.getSobjectByExternalId': { + riskLevel: 'read', + description: 'Get sObject by external ID', + }, + 'metadata.headSobjectsQuickAction': { + riskLevel: 'read', + description: 'Head sObject quick action', + }, + 'metadata.getSObjectRecord': { + riskLevel: 'read', + description: 'Get sObject record by ID', + }, + 'metadata.headActionsStandard': { + riskLevel: 'read', + description: 'Head standard actions', + }, + 'metadata.listStandardInvocableActions': { + riskLevel: 'read', + description: 'List standard invocable actions', + }, + 'metadata.getSupport': { + riskLevel: 'read', + description: 'Get support knowledge root', + }, + 'metadata.getSupportKnowledgeArticles': { + riskLevel: 'read', + description: 'Get support knowledge articles', + }, + 'metadata.getTheme': { + riskLevel: 'read', + description: 'Get theme metadata', + }, + 'metadata.getSObjectsUpdated': { + riskLevel: 'read', + description: 'Get updated sObject records', + }, + 'metadata.getUserInfo': { + riskLevel: 'read', + description: 'Get user info', + }, + 'metadata.sobjectUserPassword': { + riskLevel: 'read', + description: 'Check user password expiration status', + }, + 'metadata.massTransferOwnership': { + riskLevel: 'write', + description: 'Mass transfer record ownership', + }, + 'metadata.updateSobject': { + riskLevel: 'write', + description: 'Update sObject fields', + }, + 'metadata.sobjectRowsUpdate': { + riskLevel: 'write', + description: 'Update sObject rows', + }, + 'metadata.upsertSobjectByExternalId': { + riskLevel: 'write', + description: 'Upsert sObject by external ID', + }, + 'metadata.setUserPassword': { + riskLevel: 'write', + description: 'Set user password', + }, + + 'uiApi.createARecord': { + riskLevel: 'write', + description: 'Create record via UI API', + }, + 'uiApi.createRecordUiApi': { + riskLevel: 'write', + description: 'Create record using UI API', + }, + 'uiApi.getUiapiListInfoAccountAllAccounts': { + riskLevel: 'read', + description: 'Get Account AllAccounts list view metadata', + }, + 'uiApi.getUiapiListInfoAccountSearchResult': { + riskLevel: 'read', + description: 'Get Account SearchResult list view metadata', + }, + 'uiApi.headAppmenuSalesforce1': { + riskLevel: 'read', + description: 'Head AppMenu Salesforce1', + }, + 'uiApi.getCompactLayouts': { + riskLevel: 'read', + description: 'Get compact layouts', + }, + 'uiApi.getListViewActions': { + riskLevel: 'read', + description: 'Get list view actions', + }, + 'uiApi.getUiapiListInfoAccountRecent': { + riskLevel: 'read', + description: 'Get Account Recent list view metadata', + }, + 'uiApi.getUiApiListInfoRecent': { + riskLevel: 'read', + description: 'Get Recent list view metadata for object', + }, + 'uiApi.getUiapimruListInfoAccount': { + riskLevel: 'read', + description: 'Get MRU list info for Account (deprecated)', + }, + 'uiApi.getUiApiMruListRecordsAccount': { + riskLevel: 'read', + description: 'Get MRU list records for Account (deprecated)', + }, + 'uiApi.getUiapiActionsMruListAccount': { + riskLevel: 'read', + description: 'Get MRU list view actions', + }, + 'uiApi.getMruListViewMetadata': { + riskLevel: 'read', + description: 'Get MRU list view metadata', + }, + 'uiApi.getUiApiAppsUserNavItems': { + riskLevel: 'read', + description: 'Get user navigation items', + }, + 'uiApi.getAllNavigationItems': { + riskLevel: 'read', + description: 'Get all navigation items', + }, + 'uiApi.getApp': { + riskLevel: 'read', + description: 'Get app metadata', + }, + 'uiApi.getApps': { + riskLevel: 'read', + description: 'Get apps metadata', + }, + 'uiApi.getListViewMetadataBatch': { + riskLevel: 'read', + description: 'Get batch list view metadata', + }, + 'uiApi.getRelatedListPreferencesBatch': { + riskLevel: 'read', + description: 'Get batch related list user preferences', + }, + 'uiApi.getLastSelectedApp': { + riskLevel: 'read', + description: 'Get last selected app', + }, + 'uiApi.getListViewMetadataByName': { + riskLevel: 'read', + description: 'Get list view metadata by API name', + }, + 'uiApi.getListViewRecordsByName': { + riskLevel: 'read', + description: 'Get list view records by API name', + }, + 'uiApi.getListViewRecordsById': { + riskLevel: 'read', + description: 'Get list view records by ID', + }, + 'uiApi.listViewResults': { + riskLevel: 'read', + description: 'Get list view results', + }, + 'uiApi.getListViewResults': { + riskLevel: 'read', + description: 'Get list view results by sObject', + }, + 'uiApi.getObjectListViews': { + riskLevel: 'read', + description: 'Get list views for an object', + }, + 'uiApi.getSobjectListViews': { + riskLevel: 'read', + description: 'Get list views for sObject', + }, + 'uiApi.getUiApiActionsLookupAccount': { + riskLevel: 'read', + description: 'Get lookup field actions for Account', + }, + 'uiApi.getUiapiLookupsOpportunityAccountId': { + riskLevel: 'read', + description: 'Get lookup field suggestions for Opportunity AccountId', + }, + 'uiApi.getLookupFieldSuggestions': { + riskLevel: 'read', + description: 'Get lookup field suggestions', + }, + 'uiApi.getLookupSuggestionsOpportunityAccount': { + riskLevel: 'read', + description: + 'Get lookup field suggestions for Opportunity AccountId with POST', + }, + 'uiApi.getLookupSuggestionsCaseContact': { + riskLevel: 'read', + description: 'Get lookup field suggestions for Case ContactId with POST', + }, + 'uiApi.getMruListViewRecords': { + riskLevel: 'read', + description: 'Get MRU list view records', + }, + 'uiApi.getPhotoActions': { + riskLevel: 'read', + description: 'Get photo actions', + }, + 'uiApi.getRecordUiDataAndMetadata': { + riskLevel: 'read', + description: 'Get record UI data and metadata', + }, + 'uiApi.getRecordEditPageActions': { + riskLevel: 'read', + description: 'Get record edit page actions', + }, + 'uiApi.getUiApiActionsRecordRelatedList': { + riskLevel: 'read', + description: 'Get record related list actions', + }, + 'uiApi.getRelatedListActions': { + riskLevel: 'read', + description: 'Get related list actions', + }, + 'uiApi.getRelatedListRecordsContacts': { + riskLevel: 'read', + description: 'Get related list records for Contacts', + }, + 'uiApi.getUiapiRelatedListPreferences': { + riskLevel: 'read', + description: 'Get related list user preferences', + }, + 'uiApi.getSobjectListView': { + riskLevel: 'read', + description: 'Get sObject list view information', + }, + 'uiApi.updateRecord': { + riskLevel: 'write', + description: 'Update a record via UI API', + }, + 'uiApi.updateFavorite': { + riskLevel: 'write', + description: 'Update a favorite', + }, + 'uiApi.updateRelatedListPreferences': { + riskLevel: 'write', + description: 'Update related list preferences', + }, + 'uiApi.updateListViewPreferences': { + riskLevel: 'write', + description: 'Update list view preferences', + }, + + 'files.getFileContent': { + riskLevel: 'read', + description: 'Get binary file content', + }, + 'files.getFileInformation': { + riskLevel: 'read', + description: 'Get file metadata information', + }, + 'files.getFileShares': { + riskLevel: 'read', + description: 'Get file shares information', + }, + 'files.deleteFile': { + riskLevel: 'destructive', + irreversible: true, + description: 'Delete file permanently', + }, + 'files.uploadFile': { + riskLevel: 'write', + description: 'Upload a file to Salesforce Files', + }, + + 'analyticsReports.getDashboard': { + riskLevel: 'read', + description: 'Get dashboard metadata', + }, + 'analyticsReports.listDashboards': { + riskLevel: 'read', + description: 'List all dashboards', + }, + 'analyticsReports.listEmailTemplates': { + riskLevel: 'read', + description: 'List email templates', + }, + 'analyticsReports.listReports': { + riskLevel: 'read', + description: 'List all reports', + }, + 'analyticsReports.runReport': { + riskLevel: 'read', + description: 'Run report and return results', + }, + 'analyticsReports.listAnalyticsTemplates': { + riskLevel: 'read', + description: 'List CRM Analytics templates', + }, + 'analyticsReports.getReportInstance': { + riskLevel: 'read', + description: 'Get report instance results (deprecated)', + }, + 'analyticsReports.getReport': { + riskLevel: 'read', + description: 'Get report metadata (deprecated)', + }, + 'analyticsReports.queryReport': { + riskLevel: 'read', + description: 'Query report (deprecated)', + }, +} as const satisfies RequiredPluginEndpointMeta< + typeof salesforceEndpointsNested +>; + +export const salesforceAuthConfig = { + api_key: { + account: ['tenant_external_id'] as const, + }, + oauth_2: { + account: ['tenant_external_id', 'instance_url'] as const, + }, +} as const satisfies PluginAuthConfig; + +export type BaseSalesforcePlugin = + CorsairPlugin< + 'salesforce', + typeof SalesforceSchema, + typeof salesforceEndpointsNested, + typeof salesforceWebhooksNested, + T, + typeof defaultAuthType + >; + +export type InternalSalesforcePlugin = + BaseSalesforcePlugin; +export type ExternalSalesforcePlugin = + BaseSalesforcePlugin; + +export function salesforce( + incomingOptions: SalesforcePluginOptions & T = {} as SalesforcePluginOptions & + T, +): ExternalSalesforcePlugin { + const options = { + ...incomingOptions, + authType: incomingOptions.authType ?? defaultAuthType, + }; + let loginHost = options.loginUrl ?? SALESFORCE_LOGIN_HOST; + while (loginHost.endsWith('/')) { + loginHost = loginHost.slice(0, -1); + } + return { + id: 'salesforce', + authConfig: salesforceAuthConfig, + schema: SalesforceSchema, + options: options, + hooks: options.hooks, + webhookHooks: options.webhookHooks, + endpoints: salesforceEndpointsNested, + webhooks: salesforceWebhooksNested, + endpointMeta: salesforceEndpointMeta, + endpointSchemas: salesforceEndpointSchemas, + oauthConfig: { + providerName: 'Salesforce', + authUrl: `${loginHost}/services/oauth2/authorize`, + tokenUrl: `${loginHost}/services/oauth2/token`, + scopes: ['api', 'refresh_token', 'id'], + }, + webhookSchemas: salesforceWebhookSchemas, + pluginWebhookMatcher: (request) => { + const headers = request.headers; + const hasSig = + 'x-salesforce-signature' in headers || 'x-sfdc-signature' in headers; + const body = request.body as Record | undefined; + const header = body?.ChangeEventHeader; + const hasCdc = + !!body && + ((header !== null && + typeof header === 'object' && + !Array.isArray(header)) || + typeof body.sobject === 'string'); + return hasSig || hasCdc; + }, + pluginTenantWebhookMatcher: matchSalesforceTenantWebhook, + oauthWebhookTenantLinkResolver: resolveSalesforceOAuthWebhookTenantLink, + errorHandlers: { + ...errorHandlers, + ...options.errorHandlers, + }, + keyBuilder: async (ctx: SalesforceKeyBuilderContext, source) => { + if (source === 'webhook' && options.webhookSecret) { + return options.webhookSecret; + } + + if (source === 'webhook') { + const res = await ctx.keys.get_webhook_signature(); + return res ?? ''; + } + + if (source === 'endpoint' && options.key) { + return options.key; + } + + if (source === 'endpoint' && ctx.authType === 'api_key') { + const res = await ctx.keys.get_api_key(); + if (!res) throw new AuthMissingError('salesforce', 'api_key'); + return res; + } + + if (source === 'endpoint' && ctx.authType === 'oauth_2') { + const res = await ctx.keys.get_access_token(); + if (!res) throw new AuthMissingError('salesforce', 'oauth_2'); + return res; + } + + throw new AuthMissingError('salesforce', ctx.authType ?? 'oauth_2'); + }, + } satisfies InternalSalesforcePlugin; +} + +export type { + SalesforceEndpointInputs, + SalesforceEndpointOutputs, +} from './endpoints/types'; diff --git a/packages/salesforce/integration.test.ts b/packages/salesforce/integration.test.ts new file mode 100644 index 000000000..495bbdd5c --- /dev/null +++ b/packages/salesforce/integration.test.ts @@ -0,0 +1,95 @@ +/** + * Live checks against a Salesforce org. + * + * Skipped unless `SALESFORCE_ACCESS_TOKEN` and `SALESFORCE_INSTANCE_URL` are + * set. Writes create a throwaway Account named with a corsair-live prefix and + * delete it afterwards. + */ +import { Accounts, Contacts, SoqlSosl } from './endpoints'; +import { SalesforceEndpointOutputSchemas as Outputs } from './endpoints/types'; +import { SalesforceAccountEntity } from './schema/database'; + +const accessToken = process.env.SALESFORCE_ACCESS_TOKEN; +const instanceUrl = process.env.SALESFORCE_INSTANCE_URL; + +const describeLive = accessToken && instanceUrl ? describe : describe.skip; + +type Ctx = Parameters[0]; + +const upserts: { id: string; data: unknown }[] = []; + +function makeStore() { + return { + upsertByEntityId: async (id: string, data: unknown) => { + upserts.push({ id, data }); + }, + deleteByEntityId: async (_id: string) => true, + }; +} + +function makeCtx(): Ctx { + return { + key: accessToken ?? '', + options: { instanceUrl }, + db: { + account: makeStore(), + contact: makeStore(), + lead: makeStore(), + opportunity: makeStore(), + }, + $getAccountId: async () => 'integration-test', + } as unknown as Ctx; +} + +describeLive('Salesforce live API', () => { + beforeEach(() => { + upserts.length = 0; + }); + + it('runs SOQL against Account and matches the query envelope', async () => { + const result = await SoqlSosl.runSoqlQuery(makeCtx(), { + q: 'SELECT Id, Name FROM Account LIMIT 5', + }); + expect(() => Outputs.runSoqlQuery.parse(result)).not.toThrow(); + expect(typeof result.done).toBe('boolean'); + expect(Array.isArray(result.records)).toBe(true); + }); + + it('lists accounts and caches official-shaped rows', async () => { + const result = await Accounts.listAccounts(makeCtx(), { limit: 5 }); + expect(() => Outputs.listAccounts.parse(result)).not.toThrow(); + if (result.records[0]) { + expect(() => + SalesforceAccountEntity.parse(result.records[0]), + ).not.toThrow(); + expect(upserts[0]?.id).toBe((result.records[0] as { Id: string }).Id); + } + }); + + it('creates, reads, updates, and deletes a throwaway account', async () => { + const stamp = `corsair-live-${Date.now()}`; + const created = await Accounts.createAccount(makeCtx(), { Name: stamp }); + expect(created.id).toMatch(/^[a-zA-Z0-9]{15,18}$/); + + try { + const got = await Accounts.getAccount(makeCtx(), { id: created.id }); + expect(got.Name === stamp || got.Id === created.id).toBe(true); + + const updated = await Accounts.updateAccount(makeCtx(), { + id: created.id, + Phone: '555-0100', + }); + expect(updated.success).toBe(true); + } finally { + const deleted = await Accounts.deleteAccount(makeCtx(), { + id: created.id, + }); + expect(deleted.success).toBe(true); + } + }); + + it('lists contacts without throwing', async () => { + const result = await Contacts.listContacts(makeCtx(), { limit: 5 }); + expect(() => Outputs.listContacts.parse(result)).not.toThrow(); + }); +}); diff --git a/packages/salesforce/jest.config.cjs b/packages/salesforce/jest.config.cjs new file mode 100644 index 000000000..8c6218f64 --- /dev/null +++ b/packages/salesforce/jest.config.cjs @@ -0,0 +1,55 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: [''], + testMatch: [ + '**/*.test.ts', + '**/tests/**/*.test.ts', + '**/plugins/**/*.test.ts', + '**/setup/**/*.test.ts', + ], + collectCoverageFrom: [ + '**/*.ts', + '!**/*.d.ts', + '!**/node_modules/**', + '!**/dist/**', + '!jest.config.ts', + '!tests/**', + ], + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], + transform: { + '^.+\\.yaml$': '/../corsair/jest-yaml-transform.cjs', + '^.+\\.ts$': [ + 'ts-jest', + { + useESM: true, + tsconfig: { + esModuleInterop: true, + allowSyntheticDefaultImports: true, + verbatimModuleSyntax: false, + module: 'ESNext', + moduleResolution: 'Bundler', + }, + }, + ], + '.*\\.js$': [ + 'ts-jest', + { + useESM: true, + tsconfig: { + esModuleInterop: true, + allowSyntheticDefaultImports: true, + }, + }, + ], + }, + moduleNameMapper: { + '^corsair/core$': '/../corsair/core.ts', + '^corsair/http$': '/../corsair/http.ts', + '^(\\.\\.?/.*)\\.js$': '$1', + }, + transformIgnorePatterns: ['node_modules/(?!.*uuid.*)'], + extensionsToTreatAsEsm: ['.ts'], + testTimeout: 30000, + verbose: true, +}; diff --git a/packages/salesforce/package.json b/packages/salesforce/package.json new file mode 100644 index 000000000..f2dda6fa4 --- /dev/null +++ b/packages/salesforce/package.json @@ -0,0 +1,44 @@ +{ + "name": "@corsair-dev/salesforce", + "version": "0.1.0", + "description": "Salesforce plugin for Corsair", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "dev-source": "./index.ts", + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "scripts": { + "build": "rm -rf dist && tsc --build --force && tsup", + "typecheck": "tsc --noEmit", + "test": "jest" + }, + "peerDependencies": { + "corsair": ">=0.1.0", + "zod": "^4.1.13" + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "corsair": "workspace:*", + "jest": "^29.7.0", + "ts-jest": "^29.4.9", + "tsup": "^8.0.1", + "typescript": "catalog:", + "zod": "^4.1.13" + }, + "keywords": [ + "corsair", + "salesforce", + "plugin" + ], + "author": "", + "license": "Apache-2.0", + "files": [ + "dist" + ] +} diff --git a/packages/salesforce/schema.test.ts b/packages/salesforce/schema.test.ts new file mode 100644 index 000000000..480c2e68c --- /dev/null +++ b/packages/salesforce/schema.test.ts @@ -0,0 +1,187 @@ +/** + * Guards persisted entity schemas against dropping an official field or + * requiring a field Salesforce omits. + * + * Key lists are the standard fields from the official object reference + * (Summer '26 / API 67.0), excluding license-gated and person-account-only + * fields that a typical org never returns. + */ + +import { SalesforceSchema } from './schema'; +import { + SalesforceAccountEntity, + SalesforceCampaignEntity, + SalesforceContactEntity, + SalesforceLeadEntity, + SalesforceOpportunityEntity, + SalesforceTaskEntity, +} from './schema/database'; + +const OFFICIAL_ACCOUNT_KEYS = [ + 'Id', + 'AccountNumber', + 'AccountSource', + 'AnnualRevenue', + 'BillingCity', + 'BillingCountry', + 'BillingPostalCode', + 'BillingState', + 'BillingStreet', + 'CreatedById', + 'CreatedDate', + 'Description', + 'Fax', + 'Industry', + 'IsDeleted', + 'LastModifiedById', + 'LastModifiedDate', + 'Name', + 'NumberOfEmployees', + 'OwnerId', + 'ParentId', + 'Phone', + 'Rating', + 'ShippingCity', + 'ShippingCountry', + 'ShippingPostalCode', + 'ShippingState', + 'ShippingStreet', + 'Sic', + 'Site', + 'SystemModstamp', + 'Type', + 'Website', +] as const; + +const OFFICIAL_CONTACT_KEYS = [ + 'Id', + 'AccountId', + 'Email', + 'FirstName', + 'LastName', + 'Phone', + 'Title', + 'MailingCity', + 'MailingCountry', + 'MailingStreet', + 'OwnerId', + 'CreatedDate', + 'LastModifiedDate', +] as const; + +const OFFICIAL_LEAD_KEYS = [ + 'Id', + 'Company', + 'Email', + 'FirstName', + 'LastName', + 'Status', + 'Phone', + 'IsConverted', + 'OwnerId', + 'CreatedDate', +] as const; + +const OFFICIAL_OPPORTUNITY_KEYS = [ + 'Id', + 'AccountId', + 'Amount', + 'CloseDate', + 'Name', + 'StageName', + 'IsClosed', + 'IsWon', + 'Probability', + 'OwnerId', +] as const; + +function declaredKeys(schema: { shape: Record }): string[] { + return Object.keys(schema.shape); +} + +describe('Salesforce schema', () => { + it('declares a semver version', () => { + expect(SalesforceSchema.version).toMatch(/^\d+\.\d+\.\d+$/); + }); + + it('declares CRM entities from the official object reference', () => { + expect(Object.keys(SalesforceSchema.entities).sort()).toEqual( + [ + 'account', + 'campaign', + 'campaignMember', + 'contact', + 'contentDocument', + 'emailMessage', + 'lead', + 'note', + 'opportunity', + 'opportunityLineItem', + 'pricebook', + 'pricebookEntry', + 'task', + 'user', + ].sort(), + ); + }); +}); + +describe('entity schemas declare official API names', () => { + it('account uses PascalCase Id/Name, not snake_case', () => { + const keys = declaredKeys(SalesforceAccountEntity); + expect(keys).toContain('Id'); + expect(keys).toContain('Name'); + expect(keys).toContain('CreatedDate'); + expect(keys).not.toContain('id'); + expect(keys).not.toContain('created_at'); + }); + + it.each([ + ['account', SalesforceAccountEntity, OFFICIAL_ACCOUNT_KEYS], + ['contact', SalesforceContactEntity, OFFICIAL_CONTACT_KEYS], + ['lead', SalesforceLeadEntity, OFFICIAL_LEAD_KEYS], + ['opportunity', SalesforceOpportunityEntity, OFFICIAL_OPPORTUNITY_KEYS], + ] as const)('%s declares official fields', (_label, schema, official) => { + const keys = new Set(declaredKeys(schema)); + for (const field of official) { + expect(keys.has(field)).toBe(true); + } + }); + + it('parses a REST retrieve Account with attributes envelope', () => { + const parsed = SalesforceAccountEntity.parse({ + attributes: { + type: 'Account', + url: '/services/data/v60.0/sobjects/Account/001xx000003DGb2AAG', + }, + Id: '001xx000003DGb2AAG', + Name: 'Acme', + Type: 'Customer', + Industry: 'Technology', + CreatedDate: '2026-08-13T00:00:00.000+0000', + }); + expect(parsed.Id).toBe('001xx000003DGb2AAG'); + expect(parsed.Name).toBe('Acme'); + }); + + it('parses a SOQL Contact row that omits most fields', () => { + const parsed = SalesforceContactEntity.parse({ + Id: '003xx000004TmiqAAC', + LastName: 'Doe', + }); + expect(parsed.Id).toBe('003xx000004TmiqAAC'); + }); + + it('parses Campaign and Task rows', () => { + expect( + SalesforceCampaignEntity.parse({ Id: '701xx0000000001AAA', Name: 'Q1' }) + .Name, + ).toBe('Q1'); + expect( + SalesforceTaskEntity.parse({ + Id: '00Txx0000000001EAA', + Status: 'Completed', + }).Status, + ).toBe('Completed'); + }); +}); diff --git a/packages/salesforce/schema/database.ts b/packages/salesforce/schema/database.ts new file mode 100644 index 000000000..71b10592d --- /dev/null +++ b/packages/salesforce/schema/database.ts @@ -0,0 +1,616 @@ +import { z } from 'zod'; + +/** + * Locally persisted Salesforce entities. + * + * CRM records the plugin creates, reads, updates and deletes are mirrored: + * Account, Contact, Lead, Opportunity, Campaign, CampaignMember, Note, Task, + * OpportunityLineItem, Pricebook2, PricebookEntry, User, EmailMessage, and + * ContentDocument. Bulk jobs, UI-API layout metadata, and Tooling/Metadata + * describe payloads are not — they are transport, not records. + * + * Field names match official REST/SOAP API names (PascalCase). + * Object reference: https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_list.htm + * REST retrieve: https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_sobject_retrieve.htm + * + * Each field is labeled from the official object-reference table. Only `Id` is + * required: Salesforce omits or nulls most fields depending on FLS, record + * type, and which columns a SOQL SELECT asked for. `.loose()` keeps custom + * fields (`__c`) and the REST `attributes` envelope. + */ + +const S = z.string().nullable().optional(); +const N = z.number().nullable().optional(); +const B = z.boolean().nullable().optional(); + +/** + * Compound address. Official: + * https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/compound_fields_address.htm + */ +export const SalesforceAddress = z + .object({ + city: S, + country: S, + countryCode: S, + geocodeAccuracy: S, + latitude: N, + longitude: N, + postalCode: S, + state: S, + stateCode: S, + street: S, + }) + .loose(); +export type SalesforceAddress = z.infer; + +const Address = SalesforceAddress.nullable().optional(); + +/** + * Account. Official: + * https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_account.htm + */ +export const SalesforceAccountEntity = z + .object({ + /** Unique 15/18-character Salesforce identifier. */ + Id: z.string(), + /** Account number assigned to this account (not the system Id). */ + AccountNumber: S, + /** Source of the account record (Advertisement, Trade Show, …). */ + AccountSource: S, + /** Estimated annual revenue of the account. */ + AnnualRevenue: N, + /** Compound billing address. Read-only. */ + BillingAddress: Address, + BillingCity: S, + BillingCountry: S, + BillingGeocodeAccuracy: S, + BillingLatitude: N, + BillingLongitude: N, + BillingPostalCode: S, + BillingState: S, + BillingStreet: S, + CreatedById: S, + CreatedDate: S, + /** Text description of the account. */ + Description: S, + Fax: S, + /** Industry associated with this account. */ + Industry: S, + IsDeleted: B, + IsPersonAccount: B, + LastActivityDate: S, + LastModifiedById: S, + LastModifiedDate: S, + LastReferencedDate: S, + LastViewedDate: S, + MasterRecordId: S, + /** Required. Account Name. Max 255 characters. */ + Name: S, + /** Label: Employees. */ + NumberOfEmployees: N, + OwnerId: S, + /** Ownership type: Private, Public, Subsidiary. */ + Ownership: S, + ParentId: S, + Phone: S, + PhotoUrl: S, + /** Prospect rating: Hot, Warm, Cold. */ + Rating: S, + RecordTypeId: S, + ShippingAddress: Address, + ShippingCity: S, + ShippingCountry: S, + ShippingGeocodeAccuracy: S, + ShippingLatitude: N, + ShippingLongitude: N, + ShippingPostalCode: S, + ShippingState: S, + ShippingStreet: S, + Sic: S, + SicDesc: S, + /** Label: Account Site. */ + Site: S, + SystemModstamp: S, + TickerSymbol: S, + /** Type of account: Customer, Competitor, Partner. */ + Type: S, + Website: S, + }) + .loose(); +export type SalesforceAccountEntity = z.infer; + +/** + * Contact. Official: + * https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_contact.htm + */ +export const SalesforceContactEntity = z + .object({ + Id: z.string(), + AccountId: S, + AssistantName: S, + AssistantPhone: S, + Birthdate: S, + CreatedById: S, + CreatedDate: S, + Department: S, + Description: S, + DoNotCall: B, + Email: S, + EmailBouncedDate: S, + EmailBouncedReason: S, + Fax: S, + FirstName: S, + HasOptedOutOfEmail: B, + HasOptedOutOfFax: B, + HomePhone: S, + IndividualId: S, + IsDeleted: B, + IsEmailBounced: B, + LastActivityDate: S, + LastModifiedById: S, + LastModifiedDate: S, + LastReferencedDate: S, + LastViewedDate: S, + /** Required on create. Max 80 characters. */ + LastName: S, + LeadSource: S, + MailingAddress: Address, + MailingCity: S, + MailingCountry: S, + MailingGeocodeAccuracy: S, + MailingLatitude: N, + MailingLongitude: N, + MailingPostalCode: S, + MailingState: S, + MailingStreet: S, + MasterRecordId: S, + MobilePhone: S, + Name: S, + OtherAddress: Address, + OtherCity: S, + OtherCountry: S, + OtherPhone: S, + OtherPostalCode: S, + OtherState: S, + OtherStreet: S, + OwnerId: S, + Phone: S, + PhotoUrl: S, + RecordTypeId: S, + ReportsToId: S, + Salutation: S, + SystemModstamp: S, + Title: S, + }) + .loose(); +export type SalesforceContactEntity = z.infer; + +/** + * Lead. Official: + * https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_lead.htm + */ +export const SalesforceLeadEntity = z + .object({ + Id: z.string(), + Address: Address, + AnnualRevenue: N, + City: S, + Company: S, + ConvertedAccountId: S, + ConvertedContactId: S, + ConvertedDate: S, + ConvertedOpportunityId: S, + Country: S, + CreatedById: S, + CreatedDate: S, + Description: S, + Email: S, + EmailBouncedDate: S, + EmailBouncedReason: S, + Fax: S, + FirstName: S, + HasOptedOutOfEmail: B, + IndividualId: S, + Industry: S, + IsConverted: B, + IsDeleted: B, + IsUnreadByOwner: B, + LastActivityDate: S, + LastModifiedById: S, + LastModifiedDate: S, + LastReferencedDate: S, + LastViewedDate: S, + /** Required on create unless person accounts are enabled. */ + LastName: S, + LeadSource: S, + MasterRecordId: S, + MobilePhone: S, + Name: S, + NumberOfEmployees: N, + OwnerId: S, + Phone: S, + PostalCode: S, + Rating: S, + RecordTypeId: S, + Salutation: S, + State: S, + /** Lead status picklist. */ + Status: S, + Street: S, + SystemModstamp: S, + Title: S, + Website: S, + }) + .loose(); +export type SalesforceLeadEntity = z.infer; + +/** + * Opportunity. Official: + * https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_opportunity.htm + */ +export const SalesforceOpportunityEntity = z + .object({ + Id: z.string(), + AccountId: S, + Amount: N, + CampaignId: S, + /** Required on create. Date the opportunity closes. */ + CloseDate: S, + ContactId: S, + CreatedById: S, + CreatedDate: S, + Description: S, + ExpectedRevenue: N, + ForecastCategory: S, + ForecastCategoryName: S, + HasOpportunityLineItem: B, + IsClosed: B, + IsDeleted: B, + IsPrivate: B, + IsWon: B, + LastActivityDate: S, + LastModifiedById: S, + LastModifiedDate: S, + LastReferencedDate: S, + LastViewedDate: S, + LeadSource: S, + /** Required on create. */ + Name: S, + NextStep: S, + OwnerId: S, + Pricebook2Id: S, + Probability: N, + RecordTypeId: S, + /** Required on create. Sales stage picklist. */ + StageName: S, + SystemModstamp: S, + TotalOpportunityQuantity: N, + Type: S, + }) + .loose(); +export type SalesforceOpportunityEntity = z.infer< + typeof SalesforceOpportunityEntity +>; + +/** + * Campaign. Official: + * https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_campaign.htm + */ +export const SalesforceCampaignEntity = z + .object({ + Id: z.string(), + ActualCost: N, + AmountAllOpportunities: N, + AmountWonOpportunities: N, + BudgetedCost: N, + CampaignMemberRecordTypeId: S, + CreatedById: S, + CreatedDate: S, + Description: S, + EndDate: S, + ExpectedResponse: N, + ExpectedRevenue: N, + IsActive: B, + IsDeleted: B, + LastActivityDate: S, + LastModifiedById: S, + LastModifiedDate: S, + LastReferencedDate: S, + LastViewedDate: S, + /** Required on create. */ + Name: S, + NumberOfContacts: N, + NumberOfConvertedLeads: N, + NumberOfLeads: N, + NumberOfOpportunities: N, + NumberOfResponses: N, + NumberOfWonOpportunities: N, + NumberSent: N, + OwnerId: S, + ParentId: S, + RecordTypeId: S, + StartDate: S, + Status: S, + SystemModstamp: S, + Type: S, + }) + .loose(); +export type SalesforceCampaignEntity = z.infer; + +/** + * CampaignMember. Official: + * https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_campaignmember.htm + */ +export const SalesforceCampaignMemberEntity = z + .object({ + Id: z.string(), + CampaignId: S, + ContactId: S, + CreatedById: S, + CreatedDate: S, + FirstRespondedDate: S, + HasResponded: B, + IsDeleted: B, + LastModifiedById: S, + LastModifiedDate: S, + LeadId: S, + Status: S, + SystemModstamp: S, + }) + .loose(); +export type SalesforceCampaignMemberEntity = z.infer< + typeof SalesforceCampaignMemberEntity +>; + +/** + * Note. Official: + * https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_note.htm + */ +export const SalesforceNoteEntity = z + .object({ + Id: z.string(), + Body: S, + CreatedById: S, + CreatedDate: S, + IsDeleted: B, + IsPrivate: B, + LastModifiedById: S, + LastModifiedDate: S, + OwnerId: S, + ParentId: S, + SystemModstamp: S, + Title: S, + }) + .loose(); +export type SalesforceNoteEntity = z.infer; + +/** + * Task. Official: + * https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_task.htm + */ +export const SalesforceTaskEntity = z + .object({ + Id: z.string(), + AccountId: S, + ActivityDate: S, + CallDisposition: S, + CallDurationInSeconds: N, + CallObject: S, + CallType: S, + CompletedDateTime: S, + CreatedById: S, + CreatedDate: S, + Description: S, + IsArchived: B, + IsClosed: B, + IsDeleted: B, + IsHighPriority: B, + IsRecurrence: B, + IsReminderSet: B, + LastModifiedById: S, + LastModifiedDate: S, + OwnerId: S, + Priority: S, + RecurrenceRegeneratedType: S, + ReminderDateTime: S, + Status: S, + Subject: S, + SystemModstamp: S, + TaskSubtype: S, + WhatId: S, + WhoId: S, + }) + .loose(); +export type SalesforceTaskEntity = z.infer; + +/** + * OpportunityLineItem. Official: + * https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_opportunitylineitem.htm + */ +export const SalesforceOpportunityLineItemEntity = z + .object({ + Id: z.string(), + CreatedById: S, + CreatedDate: S, + Description: S, + IsDeleted: B, + LastModifiedById: S, + LastModifiedDate: S, + ListPrice: N, + Name: S, + OpportunityId: S, + PricebookEntryId: S, + Product2Id: S, + ProductCode: S, + Quantity: N, + ServiceDate: S, + SortOrder: N, + SystemModstamp: S, + TotalPrice: N, + UnitPrice: N, + }) + .loose(); +export type SalesforceOpportunityLineItemEntity = z.infer< + typeof SalesforceOpportunityLineItemEntity +>; + +/** + * Pricebook2. Official: + * https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_pricebook2.htm + */ +export const SalesforcePricebookEntity = z + .object({ + Id: z.string(), + CreatedById: S, + CreatedDate: S, + Description: S, + IsActive: B, + IsArchived: B, + IsDeleted: B, + IsStandard: B, + LastModifiedById: S, + LastModifiedDate: S, + LastReferencedDate: S, + LastViewedDate: S, + Name: S, + SystemModstamp: S, + }) + .loose(); +export type SalesforcePricebookEntity = z.infer< + typeof SalesforcePricebookEntity +>; + +/** + * PricebookEntry. Official: + * https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_pricebookentry.htm + */ +export const SalesforcePricebookEntryEntity = z + .object({ + Id: z.string(), + CreatedById: S, + CreatedDate: S, + IsActive: B, + IsArchived: B, + IsDeleted: B, + LastModifiedById: S, + LastModifiedDate: S, + Name: S, + Pricebook2Id: S, + Product2Id: S, + ProductCode: S, + SystemModstamp: S, + UnitPrice: N, + UseStandardPrice: B, + }) + .loose(); +export type SalesforcePricebookEntryEntity = z.infer< + typeof SalesforcePricebookEntryEntity +>; + +/** + * User. Official: + * https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_user.htm + */ +export const SalesforceUserEntity = z + .object({ + Id: z.string(), + AboutMe: S, + AccountId: S, + Alias: S, + City: S, + CompanyName: S, + Country: S, + CreatedById: S, + CreatedDate: S, + Department: S, + Email: S, + EmailEncodingKey: S, + FirstName: S, + IsActive: B, + LanguageLocaleKey: S, + LastLoginDate: S, + LastModifiedById: S, + LastModifiedDate: S, + LastName: S, + LocaleSidKey: S, + MobilePhone: S, + Name: S, + Phone: S, + PostalCode: S, + ProfileId: S, + State: S, + Street: S, + SystemModstamp: S, + TimeZoneSidKey: S, + Title: S, + Username: S, + UserRoleId: S, + UserType: S, + }) + .loose(); +export type SalesforceUserEntity = z.infer; + +/** + * EmailMessage. Official: + * https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_emailmessage.htm + */ +export const SalesforceEmailMessageEntity = z + .object({ + Id: z.string(), + BccAddress: S, + CcAddress: S, + CreatedById: S, + CreatedDate: S, + FromAddress: S, + FromName: S, + HasAttachment: B, + Headers: S, + HtmlBody: S, + Incoming: B, + IsDeleted: B, + LastModifiedById: S, + LastModifiedDate: S, + MessageDate: S, + ParentId: S, + RelatedToId: S, + Status: S, + Subject: S, + SystemModstamp: S, + TextBody: S, + ToAddress: S, + }) + .loose(); +export type SalesforceEmailMessageEntity = z.infer< + typeof SalesforceEmailMessageEntity +>; + +/** + * ContentDocument. Official: + * https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_contentdocument.htm + */ +export const SalesforceContentDocumentEntity = z + .object({ + Id: z.string(), + ContentModifiedDate: S, + ContentSize: N, + CreatedById: S, + CreatedDate: S, + Description: S, + FileExtension: S, + FileType: S, + IsDeleted: B, + LastModifiedById: S, + LastModifiedDate: S, + LatestPublishedVersionId: S, + OwnerId: S, + ParentId: S, + PublishStatus: S, + SharingOption: S, + SharingPrivacy: S, + SystemModstamp: S, + Title: S, + }) + .loose(); +export type SalesforceContentDocumentEntity = z.infer< + typeof SalesforceContentDocumentEntity +>; diff --git a/packages/salesforce/schema/index.ts b/packages/salesforce/schema/index.ts new file mode 100644 index 000000000..91470dd0e --- /dev/null +++ b/packages/salesforce/schema/index.ts @@ -0,0 +1,38 @@ +import { + SalesforceAccountEntity, + SalesforceCampaignEntity, + SalesforceCampaignMemberEntity, + SalesforceContactEntity, + SalesforceContentDocumentEntity, + SalesforceEmailMessageEntity, + SalesforceLeadEntity, + SalesforceNoteEntity, + SalesforceOpportunityEntity, + SalesforceOpportunityLineItemEntity, + SalesforcePricebookEntity, + SalesforcePricebookEntryEntity, + SalesforceTaskEntity, + SalesforceUserEntity, +} from './database'; + +export const SalesforceSchema = { + version: '1.0.0', + entities: { + account: SalesforceAccountEntity, + contact: SalesforceContactEntity, + lead: SalesforceLeadEntity, + opportunity: SalesforceOpportunityEntity, + campaign: SalesforceCampaignEntity, + campaignMember: SalesforceCampaignMemberEntity, + note: SalesforceNoteEntity, + task: SalesforceTaskEntity, + opportunityLineItem: SalesforceOpportunityLineItemEntity, + pricebook: SalesforcePricebookEntity, + pricebookEntry: SalesforcePricebookEntryEntity, + user: SalesforceUserEntity, + emailMessage: SalesforceEmailMessageEntity, + contentDocument: SalesforceContentDocumentEntity, + }, +} as const; + +export * from './database'; diff --git a/packages/salesforce/tsconfig.json b/packages/salesforce/tsconfig.json new file mode 100644 index 000000000..15e507a13 --- /dev/null +++ b/packages/salesforce/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["esnext"], + "types": ["node", "jest"], + "module": "ESNext", + "moduleResolution": "Bundler", + "outDir": "./dist", + "rootDir": "./", + "composite": true, + "incremental": true, + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true, + "skipLibCheck": true + }, + "include": ["./**/*"], + "exclude": ["dist", "node_modules"], + "references": [] +} diff --git a/packages/salesforce/tsup.config.ts b/packages/salesforce/tsup.config.ts new file mode 100644 index 000000000..3ec221e23 --- /dev/null +++ b/packages/salesforce/tsup.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + clean: false, + dts: false, + format: ['esm'], + target: 'esnext', + platform: 'node', + bundle: true, + splitting: true, + minify: true, + outDir: 'dist', + external: ['corsair', 'zod'], + entry: ['index.ts'], +}); diff --git a/packages/salesforce/utils.ts b/packages/salesforce/utils.ts new file mode 100644 index 000000000..5472958ec --- /dev/null +++ b/packages/salesforce/utils.ts @@ -0,0 +1,251 @@ +/** + * Escapes special characters in string values to safely construct SOQL queries, + * including LIKE wildcards (`%`, `_`). + */ +export function escapeSoql(value: string): string { + return value + .replace(/\\/g, '\\\\') + .replace(/'/g, "\\'") + .replace(/%/g, '\\%') + .replace(/_/g, '\\_'); +} + +/** + * Parses CSV text responses (e.g. from Salesforce Bulk API v2 result endpoints) + * into an array of key-value records. If response is already an array, returns it. + */ +export function parseCsvRecords( + response: unknown, +): Array> { + if (Array.isArray(response)) { + return response as Array>; + } + + if (typeof response === 'string' && response.trim().length > 0) { + const rows = parseCsvRows(response); + const headers = rows[0]; + if (!headers || rows.length <= 1) return []; + + const records: Array> = []; + for (let i = 1; i < rows.length; i++) { + const values = rows[i]; + if (!values || values.every((value) => value === '')) continue; + const record: Record = {}; + for (let j = 0; j < headers.length; j++) { + const header = headers[j]; + if (header) { + record[header] = values[j] ?? ''; + } + } + records.push(record); + } + return records; + } + + return []; +} + +function parseCsvRows(text: string): string[][] { + const rows: string[][] = []; + let row: string[] = []; + let current = ''; + let inQuotes = false; + let quoted = false; + + const pushField = () => { + row.push(quoted ? current : current.trim()); + current = ''; + quoted = false; + }; + + for (let i = 0; i < text.length; i++) { + const char = text[i]; + if (inQuotes) { + if (char === '"') { + if (text[i + 1] === '"') { + current += '"'; + i++; + } else { + inQuotes = false; + } + } else { + current += char; + } + continue; + } + if (char === '"') { + inQuotes = true; + quoted = true; + continue; + } + if (char === ',') { + pushField(); + continue; + } + if (char === '\n' || char === '\r') { + if (char === '\r' && text[i + 1] === '\n') i++; + pushField(); + if (row.some((value) => value !== '')) rows.push(row); + row = []; + continue; + } + current += char; + } + + pushField(); + if (row.some((value) => value !== '')) rows.push(row); + return rows; +} + +/** Keeps only fields Salesforce reports as createable on the sObject. */ +export function createableNames(describe: { + fields?: Array<{ name?: string; createable?: boolean }>; +}): Set { + const names = new Set(); + for (const field of describe.fields ?? []) { + if (field.createable && typeof field.name === 'string') { + names.add(field.name); + } + } + return names; +} + +export function cloneableFields( + record: Record, + createable: Set, +): Record { + const out: Record = {}; + for (const [key, value] of Object.entries(record)) { + if (createable.has(key)) out[key] = value; + } + return out; +} + +export function assertSobjectName(name: string): string { + if (!/^[A-Za-z][A-Za-z0-9_]{0,39}$/.test(name)) { + throw new Error('Invalid Salesforce sObject name'); + } + return name; +} + +const SOQL_FIELD = /^[A-Za-z][A-Za-z0-9_.]*$/; +const SOQL_OP = /^(=|!=|<>|LIKE|>|<|>=|<=|IN)$/i; + +function isSoqlLiteral(value: string): boolean { + const v = value.trim(); + if (/^(true|false|null)$/i.test(v)) return true; + if (/^-?\d+(\.\d+)?$/.test(v)) return true; + return /^'(?:[^'\\]|\\.)*'$/.test(v); +} + +function splitSoqlList(inner: string): string[] { + const parts: string[] = []; + let current = ''; + let inQuote = false; + for (let i = 0; i < inner.length; i++) { + const ch = inner[i]; + if (inQuote) { + current += ch; + if (ch === '\\' && i + 1 < inner.length) { + current += inner[++i]; + } else if (ch === "'") { + inQuote = false; + } + continue; + } + if (ch === "'") { + inQuote = true; + current += ch; + continue; + } + if (ch === ',') { + parts.push(current); + current = ''; + continue; + } + current += ch; + } + parts.push(current); + return parts; +} + +function splitSoqlLogic(sql: string): string[] { + const clauses: string[] = []; + let current = ''; + let inQuote = false; + let i = 0; + while (i < sql.length) { + const ch = sql[i]; + if (inQuote) { + current += ch; + if (ch === '\\' && i + 1 < sql.length) { + current += sql[++i]; + } else if (ch === "'") { + inQuote = false; + } + i++; + continue; + } + if (ch === "'") { + inQuote = true; + current += ch; + i++; + continue; + } + const rest = sql.slice(i); + const m = rest.match(/^\s+(AND|OR)\s+/i); + if (m) { + clauses.push(current.trim()); + current = ''; + i += m[0].length; + continue; + } + current += ch; + i++; + } + clauses.push(current.trim()); + return clauses; +} + +function assertSoqlClause(clause: string): void { + const m = clause.match( + /^([A-Za-z][A-Za-z0-9_.]*)\s*(=|!=|<>|LIKE|>=|<=|>|<|IN)\s*(.+)$/i, + ); + if (!m || !m[1] || !m[2] || !m[3]) { + throw new Error('Invalid SOQL WHERE clause'); + } + if (!SOQL_FIELD.test(m[1]) || !SOQL_OP.test(m[2])) { + throw new Error('Invalid SOQL WHERE clause'); + } + const rawValue = m[3].trim(); + if (m[2].toUpperCase() === 'IN') { + const list = rawValue.match(/^\((.*)\)$/); + if (!list) throw new Error('Invalid SOQL WHERE clause'); + const items = splitSoqlList(list[1] ?? ''); + if (items.length === 0 || items.some((item) => !isSoqlLiteral(item))) { + throw new Error('Invalid SOQL WHERE clause'); + } + return; + } + if (!isSoqlLiteral(rawValue)) { + throw new Error('Invalid SOQL WHERE clause'); + } +} + +/** + * Validates a caller-supplied SOQL WHERE fragment. Only allowlisted + * field/operator/literal clauses joined by AND/OR are accepted. + */ +export function soqlWhere(fragment: string | undefined): string | undefined { + if (!fragment) return undefined; + const trimmed = fragment.trim(); + if (!trimmed) return undefined; + const clauses = splitSoqlLogic(trimmed); + if (clauses.some((clause) => !clause)) { + throw new Error('Invalid SOQL WHERE clause'); + } + for (const clause of clauses) { + assertSoqlClause(clause); + } + return trimmed; +} diff --git a/packages/salesforce/webhooks.test.ts b/packages/salesforce/webhooks.test.ts new file mode 100644 index 000000000..8e77f8cd3 --- /dev/null +++ b/packages/salesforce/webhooks.test.ts @@ -0,0 +1,170 @@ +import { flattenFields } from './endpoints/shared'; +import { + cloneableFields, + escapeSoql, + parseCsvRecords, + soqlWhere, +} from './utils'; +import { resolveSalesforceOAuthWebhookTenantLink } from './webhooks/oauth-tenant-link'; +import { + createSalesforceChangeMatch, + recordIdFromPayload, +} from './webhooks/types'; + +describe('flattenFields', () => { + it('spreads CustomFields onto the Salesforce body', () => { + expect( + flattenFields({ + Name: 'Acme', + CustomFields: { Region__c: 'West' }, + }), + ).toEqual({ Name: 'Acme', Region__c: 'West' }); + }); +}); + +describe('escapeSoql', () => { + it('escapes quotes, backslashes, and LIKE wildcards', () => { + expect(escapeSoql("O'Brien%_")).toBe("O\\'Brien\\%\\_"); + }); +}); + +describe('parseCsvRecords', () => { + it('keeps quoted newlines inside a single record', () => { + expect(parseCsvRecords('Name,Notes\n"Acme","line1\nline2"\n')).toEqual([ + { Name: 'Acme', Notes: 'line1\nline2' }, + ]); + }); + + it('preserves whitespace inside quoted fields', () => { + expect(parseCsvRecords('Name,Notes\nAcme," padded "\n')).toEqual([ + { Name: 'Acme', Notes: ' padded ' }, + ]); + }); +}); + +describe('cloneableFields', () => { + it('keeps only createable fields from the record and overrides', () => { + const allowed = new Set(['Name', 'Phone']); + expect( + cloneableFields( + { Id: '001xx', Name: 'Acme', LastModifiedDate: '2026-01-01' }, + allowed, + ), + ).toEqual({ Name: 'Acme' }); + expect( + cloneableFields({ Name: 'Beta', OwnerId: '005xx' }, allowed), + ).toEqual({ Name: 'Beta' }); + }); +}); + +describe('Salesforce webhook matchers', () => { + it('matches Account CREATE CDC payloads', () => { + const match = createSalesforceChangeMatch({ + entityName: 'Account', + changeTypes: ['CREATE', 'CREATED'], + }); + expect( + match({ + headers: {}, + body: { + ChangeEventHeader: { + entityName: 'Account', + changeType: 'CREATE', + recordIds: ['001xx000003DGb2AAG'], + }, + }, + }), + ).toBe(true); + expect( + match({ + headers: {}, + body: { + ChangeEventHeader: { + entityName: 'Contact', + changeType: 'CREATE', + }, + }, + }), + ).toBe(false); + }); + + it('does not treat GAP_CREATE as CREATE', () => { + const match = createSalesforceChangeMatch({ + entityName: 'Account', + changeTypes: ['CREATE', 'CREATED'], + }); + expect( + match({ + headers: {}, + body: { + ChangeEventHeader: { + entityName: 'Account', + changeType: 'GAP_CREATE', + }, + }, + }), + ).toBe(false); + }); + + it('reads the record id from ChangeEventHeader', () => { + expect( + recordIdFromPayload({ + ChangeEventHeader: { recordIds: ['001xx'] }, + }), + ).toBe('001xx'); + }); +}); + +describe('soqlWhere', () => { + it('accepts allowlisted field/operator clauses', () => { + expect(soqlWhere("Name = 'Acme' AND Status IN ('Open','Closed')")).toBe( + "Name = 'Acme' AND Status IN ('Open','Closed')", + ); + }); + + it('accepts inclusive comparison operators', () => { + expect(soqlWhere('Amount >= 10')).toBe('Amount >= 10'); + expect(soqlWhere('Amount <= 25')).toBe('Amount <= 25'); + }); + + it('rejects concatenated SOQL fragments', () => { + expect(() => soqlWhere("Name = 'x' OR Id != '' LIMIT 1")).toThrow( + 'Invalid SOQL WHERE clause', + ); + }); +}); + +describe('resolveSalesforceOAuthWebhookTenantLink', () => { + const originalFetch = global.fetch; + + afterEach(() => { + global.fetch = originalFetch; + }); + + it('extracts a 15-character org id from a Salesforce identity URL', async () => { + await expect( + resolveSalesforceOAuthWebhookTenantLink({ + access_token: 'token', + id: 'https://login.salesforce.com/id/00D000000000123/005xx', + } as never), + ).resolves.toEqual({ + linkType: 'tenant_external_id', + externalId: '00D000000000123', + }); + }); + + it('does not send the bearer token to a non-Salesforce host', async () => { + let fetched = false; + global.fetch = (async () => { + fetched = true; + return { ok: true, json: async () => ({}) } as Response; + }) as typeof fetch; + await expect( + resolveSalesforceOAuthWebhookTenantLink({ + access_token: 'token', + id: 'https://evil.example/id/00D000000000123/005xx', + } as never), + ).resolves.toBeNull(); + expect(fetched).toBe(false); + }); +}); diff --git a/packages/salesforce/webhooks/index.ts b/packages/salesforce/webhooks/index.ts new file mode 100644 index 000000000..d86d2e7d5 --- /dev/null +++ b/packages/salesforce/webhooks/index.ts @@ -0,0 +1,4 @@ +export * from './oauth-tenant-link'; +export * from './tenant-matcher'; +export * from './triggers'; +export * from './types'; diff --git a/packages/salesforce/webhooks/oauth-tenant-link.ts b/packages/salesforce/webhooks/oauth-tenant-link.ts new file mode 100644 index 000000000..0690d4980 --- /dev/null +++ b/packages/salesforce/webhooks/oauth-tenant-link.ts @@ -0,0 +1,84 @@ +import type { TokenResponse, WebhookTenantMatch } from 'corsair/core'; +import { toExternalId } from 'corsair/core'; + +const IDENTITY_FETCH_TIMEOUT_MS = 5_000; + +function salesforceIdentityUrl(value: string): URL | null { + try { + const url = new URL(value); + if (url.protocol !== 'https:') return null; + const host = url.hostname.toLowerCase(); + const allowed = + host === 'login.salesforce.com' || + host === 'test.salesforce.com' || + host.endsWith('.my.salesforce.com') || + /^[a-z0-9-]+\.salesforce\.com$/.test(host); + if (!allowed || !url.pathname.startsWith('/id/')) return null; + return url; + } catch { + return null; + } +} + +function orgIdFromIdentityPath(pathname: string): string | null { + const parts = pathname.split('/id/'); + const orgId = parts[1]?.split('/')[0]; + if (orgId && /^00D[a-zA-Z0-9]{12}$|^00D[a-zA-Z0-9]{15}$/.test(orgId)) { + return orgId; + } + return null; +} + +export async function resolveSalesforceOAuthWebhookTenantLink( + tokens: TokenResponse, +): Promise { + // 1. Direct external ID or organization_id field in token response + const directId = toExternalId( + tokens.organization_id || + tokens.tenant_external_id || + (tokens.custom_attributes as Record | undefined) + ?.organization_id, + ); + if (directId) { + return { linkType: 'tenant_external_id', externalId: directId }; + } + + // 2. Extract org ID from Salesforce identity URL (https://login.salesforce.com/id/{orgId}/{userId}) + if (typeof tokens.id === 'string') { + const identityUrl = salesforceIdentityUrl(tokens.id); + if (identityUrl) { + const orgId = orgIdFromIdentityPath(identityUrl.pathname); + if (orgId) { + return { linkType: 'tenant_external_id', externalId: orgId }; + } + } + } + + // 3. User Identity Endpoint fetch fallback if tokens.id is a URL + if (typeof tokens.id === 'string' && tokens.access_token) { + const identityUrl = salesforceIdentityUrl(tokens.id); + if (!identityUrl) return null; + try { + const res = await fetch(identityUrl, { + headers: { Authorization: `Bearer ${tokens.access_token}` }, + signal: AbortSignal.timeout(IDENTITY_FETCH_TIMEOUT_MS), + }); + if (res.ok) { + const payload = (await res.json()) as { + organization_id?: string; + org_id?: string; + }; + const fetchedId = toExternalId( + payload.organization_id || payload.org_id, + ); + if (fetchedId) { + return { linkType: 'tenant_external_id', externalId: fetchedId }; + } + } + } catch { + // ignore network error + } + } + + return null; +} diff --git a/packages/salesforce/webhooks/tenant-matcher.ts b/packages/salesforce/webhooks/tenant-matcher.ts new file mode 100644 index 000000000..f0c295b21 --- /dev/null +++ b/packages/salesforce/webhooks/tenant-matcher.ts @@ -0,0 +1,20 @@ +import type { RawWebhookRequest, WebhookTenantMatch } from 'corsair/core'; +import { asRecord, firstString, readBodyRecord } from 'corsair/core'; + +export function matchSalesforceTenantWebhook( + request: RawWebhookRequest, +): WebhookTenantMatch | null { + const body = readBodyRecord(request); + if (!body) return null; + + const externalId = firstString([ + body.organization_id, + body.tenant_external_id, + asRecord(body.data)?.organization_id, + asRecord(body.data)?.tenant_external_id, + ]); + + if (!externalId) return null; + + return { linkType: 'tenant_external_id', externalId }; +} diff --git a/packages/salesforce/webhooks/triggers.ts b/packages/salesforce/webhooks/triggers.ts new file mode 100644 index 000000000..756fdba01 --- /dev/null +++ b/packages/salesforce/webhooks/triggers.ts @@ -0,0 +1,214 @@ +import { cacheEntity } from '../endpoints/persist'; +import type { SalesforceWebhooks } from '../index'; +import { + SalesforceAccountEntity, + SalesforceContactEntity, + SalesforceLeadEntity, + SalesforceOpportunityEntity, + SalesforceTaskEntity, +} from '../schema/database'; +import { + createSalesforceChangeMatch, + recordIdFromPayload, + verifySalesforceWebhookSignature, +} from './types'; + +function verified( + ctx: { key: string }, + request: Parameters< + SalesforceWebhooks['accountCreatedOrUpdated']['handler'] + >[1], +) { + return verifySalesforceWebhookSignature(request, ctx.key); +} + +export const accountCreatedOrUpdated: SalesforceWebhooks['accountCreatedOrUpdated'] = + { + match: createSalesforceChangeMatch({ + entityName: 'Account', + changeTypes: ['CREATE', 'UPDATE', 'CREATED', 'UPDATED'], + }), + handler: async (ctx, request) => { + const verification = verified(ctx, request); + if (!verification.valid) { + return { + success: false, + statusCode: 401, + error: verification.error || 'Signature verification failed', + }; + } + const id = recordIdFromPayload(request.payload); + if (id) { + await cacheEntity( + ctx.db?.account, + SalesforceAccountEntity, + { Id: id, ...request.payload }, + { label: 'account' }, + ); + } + return { success: true, data: { success: true } }; + }, + }; + +export const contactUpdated: SalesforceWebhooks['contactUpdated'] = { + match: createSalesforceChangeMatch({ + entityName: 'Contact', + changeTypes: ['UPDATE', 'UPDATED'], + }), + handler: async (ctx, request) => { + const verification = verified(ctx, request); + if (!verification.valid) { + return { + success: false, + statusCode: 401, + error: verification.error || 'Signature verification failed', + }; + } + const id = recordIdFromPayload(request.payload); + if (id) { + await cacheEntity( + ctx.db?.contact, + SalesforceContactEntity, + { Id: id, ...request.payload }, + { label: 'contact' }, + ); + } + return { success: true, data: { success: true } }; + }, +}; + +export const newContact: SalesforceWebhooks['newContact'] = { + match: createSalesforceChangeMatch({ + entityName: 'Contact', + changeTypes: ['CREATE', 'CREATED'], + }), + handler: async (ctx, request) => { + const verification = verified(ctx, request); + if (!verification.valid) { + return { + success: false, + statusCode: 401, + error: verification.error || 'Signature verification failed', + }; + } + const id = recordIdFromPayload(request.payload); + if (id) { + await cacheEntity( + ctx.db?.contact, + SalesforceContactEntity, + { Id: id, ...request.payload }, + { label: 'contact' }, + ); + } + return { success: true, data: { success: true } }; + }, +}; + +export const newLead: SalesforceWebhooks['newLead'] = { + match: createSalesforceChangeMatch({ + entityName: 'Lead', + changeTypes: ['CREATE', 'CREATED'], + }), + handler: async (ctx, request) => { + const verification = verified(ctx, request); + if (!verification.valid) { + return { + success: false, + statusCode: 401, + error: verification.error || 'Signature verification failed', + }; + } + const id = recordIdFromPayload(request.payload); + if (id) { + await cacheEntity( + ctx.db?.lead, + SalesforceLeadEntity, + { Id: id, ...request.payload }, + { label: 'lead' }, + ); + } + return { success: true, data: { success: true } }; + }, +}; + +export const newOrUpdatedOpportunity: SalesforceWebhooks['newOrUpdatedOpportunity'] = + { + match: createSalesforceChangeMatch({ + entityName: 'Opportunity', + changeTypes: ['CREATE', 'UPDATE', 'CREATED', 'UPDATED'], + }), + handler: async (ctx, request) => { + const verification = verified(ctx, request); + if (!verification.valid) { + return { + success: false, + statusCode: 401, + error: verification.error || 'Signature verification failed', + }; + } + const id = recordIdFromPayload(request.payload); + if (id) { + await cacheEntity( + ctx.db?.opportunity, + SalesforceOpportunityEntity, + { Id: id, ...request.payload }, + { label: 'opportunity' }, + ); + } + return { success: true, data: { success: true } }; + }, + }; + +export const genericSObjectRecordUpdated: SalesforceWebhooks['genericSObjectRecordUpdated'] = + { + match: createSalesforceChangeMatch({ + changeTypes: ['UPDATE', 'UPDATED'], + }), + handler: async (ctx, request) => { + const verification = verified(ctx, request); + if (!verification.valid) { + return { + success: false, + statusCode: 401, + error: verification.error || 'Signature verification failed', + }; + } + return { success: true, data: { success: true } }; + }, + }; + +export const taskCreatedOrCompleted: SalesforceWebhooks['taskCreatedOrCompleted'] = + { + match: (request) => { + const created = createSalesforceChangeMatch({ + entityName: 'Task', + changeTypes: ['CREATE', 'CREATED'], + })(request); + const completed = createSalesforceChangeMatch({ + entityName: 'Task', + changeTypes: ['UPDATE', 'UPDATED'], + status: 'Completed', + })(request); + return created || completed; + }, + handler: async (ctx, request) => { + const verification = verified(ctx, request); + if (!verification.valid) { + return { + success: false, + statusCode: 401, + error: verification.error || 'Signature verification failed', + }; + } + const id = recordIdFromPayload(request.payload); + if (id) { + await cacheEntity( + ctx.db?.task, + SalesforceTaskEntity, + { Id: id, ...request.payload }, + { label: 'task' }, + ); + } + return { success: true, data: { success: true } }; + }, + }; diff --git a/packages/salesforce/webhooks/types.ts b/packages/salesforce/webhooks/types.ts new file mode 100644 index 000000000..4ddac2e5a --- /dev/null +++ b/packages/salesforce/webhooks/types.ts @@ -0,0 +1,145 @@ +import type { + CorsairWebhookMatcher, + RawWebhookRequest, + WebhookRequest, +} from 'corsair/core'; +import { verifyHmacSignature } from 'corsair/http'; +import { z } from 'zod'; + +/** + * Salesforce Change Data Capture / Flow HTTP payload. + * Official CDC: https://developer.salesforce.com/docs/atlas.en-us.change_data_capture.meta/change_data_capture/cdc_message_structure.htm + */ +export const SalesforceChangeEventHeaderSchema = z + .object({ + entityName: z.string().optional(), + changeType: z.string().optional(), + recordIds: z.array(z.string()).optional(), + commitTimestamp: z.number().optional(), + commitUser: z.string().optional(), + }) + .loose(); + +export const SalesforceWebhookPayloadSchema = z + .object({ + ChangeEventHeader: SalesforceChangeEventHeaderSchema.optional(), + Id: z.string().optional(), + id: z.string().optional(), + sobject: z.string().optional(), + type: z.string().optional(), + Status: z.string().optional(), + LastModifiedDate: z.string().optional(), + SystemModstamp: z.string().optional(), + organization_id: z.string().optional(), + }) + .loose(); + +export type SalesforceWebhookPayload = z.infer< + typeof SalesforceWebhookPayloadSchema +>; + +export type SalesforceWebhookOutputs = { + accountCreatedOrUpdated: { success: boolean }; + contactUpdated: { success: boolean }; + newContact: { success: boolean }; + newLead: { success: boolean }; + newOrUpdatedOpportunity: { success: boolean }; + genericSObjectRecordUpdated: { success: boolean }; + taskCreatedOrCompleted: { success: boolean }; +}; + +function parseBody(body: unknown): Record | null { + if (typeof body === 'string') { + try { + const parsed = JSON.parse(body); + return parsed !== null && + typeof parsed === 'object' && + !Array.isArray(parsed) + ? (parsed as Record) + : null; + } catch { + return null; + } + } + return body !== null && typeof body === 'object' && !Array.isArray(body) + ? (body as Record) + : null; +} + +function headerOf(body: Record) { + const header = body.ChangeEventHeader; + if (header && typeof header === 'object' && !Array.isArray(header)) { + return header as Record; + } + return undefined; +} + +function entityNameOf(body: Record): string | undefined { + const header = headerOf(body); + if (typeof header?.entityName === 'string') return header.entityName; + if (typeof body.sobject === 'string') return body.sobject; + return undefined; +} + +function changeTypeOf(body: Record): string | undefined { + const header = headerOf(body); + if (typeof header?.changeType === 'string') return header.changeType; + if (typeof body.type === 'string') return body.type; + return undefined; +} + +export function createSalesforceChangeMatch(options: { + entityName?: string; + changeTypes: string[]; + status?: string; +}): CorsairWebhookMatcher { + return (request: RawWebhookRequest) => { + const body = parseBody(request.body); + if (!body) return false; + const entity = entityNameOf(body); + const change = (changeTypeOf(body) ?? '').toUpperCase(); + if (options.entityName && entity !== options.entityName) return false; + if (!options.changeTypes.some((t) => change === t.toUpperCase())) { + return false; + } + if (options.status && body.Status !== options.status) return false; + return true; + }; +} + +export function verifySalesforceWebhookSignature( + request: WebhookRequest, + secret: string, +): { valid: boolean; error?: string } { + if (!secret) { + return { valid: false, error: 'No webhook secret configured' }; + } + + const rawHeader = + request.headers['x-salesforce-signature'] || + request.headers['x-sfdc-signature']; + const signature = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader; + + if (!signature) { + return { valid: false, error: 'Missing webhook signature header' }; + } + + const rawBody = + typeof request.rawBody === 'string' + ? request.rawBody + : JSON.stringify(request.payload ?? {}); + + const isValid = verifyHmacSignature(rawBody, secret, signature); + if (!isValid) { + return { valid: false, error: 'Invalid webhook signature' }; + } + + return { valid: true }; +} + +export function recordIdFromPayload( + payload: SalesforceWebhookPayload, +): string | undefined { + const fromHeader = payload.ChangeEventHeader?.recordIds?.[0]; + return fromHeader || payload.Id || payload.id; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e2e91d2ba..c67d3e83c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2813,6 +2813,30 @@ importers: specifier: 4.4.3 version: 4.4.3 + packages/salesforce: + devDependencies: + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 + corsair: + specifier: workspace:* + version: link:../corsair + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + ts-jest: + specifier: ^29.4.9 + version: 29.4.9(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@30.4.1)(babel-jest@29.7.0(@babel/core@7.29.7))(esbuild@0.27.0)(jest-util@30.4.1)(jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(typescript@5.9.3) + tsup: + specifier: ^8.0.1 + version: 8.5.1(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0) + typescript: + specifier: 'catalog:' + version: 5.9.3 + zod: + specifier: 4.4.3 + version: 4.4.3 + packages/sentry: devDependencies: '@types/jest':