diff --git a/packages/pieces/community/salesforce/package.json b/packages/pieces/community/salesforce/package.json index bbe46c49d5e..e58af62f3c1 100644 --- a/packages/pieces/community/salesforce/package.json +++ b/packages/pieces/community/salesforce/package.json @@ -1,4 +1,4 @@ { "name": "@activepieces/piece-salesforce", - "version": "0.4.0" + "version": "0.5.0" } diff --git a/packages/pieces/community/salesforce/src/lib/action/run-report.ts b/packages/pieces/community/salesforce/src/lib/action/run-report.ts index 78295bf83d9..393a56d35f7 100644 --- a/packages/pieces/community/salesforce/src/lib/action/run-report.ts +++ b/packages/pieces/community/salesforce/src/lib/action/run-report.ts @@ -4,214 +4,205 @@ import { salesforceAuth } from '../..'; import { callSalesforceApi, salesforcesCommon } from '../common'; export const runReport = createAction({ - auth: salesforceAuth, - name: 'run_report', - displayName: 'Run Report', - description: - 'Execute a Salesforce analytics report and return the results as easy-to-use rows.', - props: { - report_id: salesforcesCommon.report, - filters: Property.Json({ - displayName: 'Filters', - description: - "Apply dynamic filters to the report run. Leave empty to use the report's saved filters.", - required: false, - defaultValue: [], - }), - }, - async run(context) { - const { report_id, filters } = context.propsValue; - - let body = undefined; - if (filters && Array.isArray(filters) && filters.length > 0) { - body = { - reportMetadata: { - reportFilters: filters, - }, - }; - } - - const queryParam = '?includeDetails=true'; - - const response = await callSalesforceApi( - HttpMethod.POST, - context.auth, - `/services/data/v56.0/analytics/reports/${report_id}${queryParam}`, - body - ); - - const reportData = response.body; - - return transformReportToRows(reportData); - }, + auth: salesforceAuth, + name: 'run_report', + displayName: 'Run Report', + description: + 'Execute a Salesforce analytics report and return the results as easy-to-use rows.', + props: { + report_id: salesforcesCommon.report, + filters: Property.Json({ + displayName: 'Filters', + description: + "Apply dynamic filters to the report run. Leave empty to use the report's saved filters.", + required: false, + defaultValue: [ + { + column: 'ACCOUNT.NAME', + operator: 'equals', + value: 'Acme', + }, + ], + }), + }, + async run(context) { + const { report_id, filters } = context.propsValue; + + let body = undefined; + if (filters && Array.isArray(filters) && filters.length > 0) { + body = { + reportMetadata: { + reportFilters: filters, + }, + }; + } + + const queryParam = '?includeDetails=true'; + + const response = await callSalesforceApi( + HttpMethod.POST, + context.auth, + `/services/data/v56.0/analytics/reports/${report_id}${queryParam}`, + body + ); + + const reportData = response.body; + + return transformReportToRows(reportData); + }, }); interface SalesforceReportResponse { - attributes: { - reportId: string; - reportName: string; - }; - reportMetadata: { - detailColumns: string[]; - name: string; - reportFormat: string; - aggregates: string[]; - groupingsDown: { - name: string; - sortOrder: string; - dateGranularity: string; - column: string; - }[]; - }; - reportExtendedMetadata: { - detailColumnInfo: Record; - groupingColumnInfo: Record; - aggregateColumnInfo: Record; - }; - factMap: Record< - string, - { - rows: { dataCells: { label: string; value: unknown }[] }[]; - aggregates: { label: string; value: unknown }[]; - } - >; - groupingsDown: { - groupings: { key: string; label: string; value: unknown }[]; - }; + attributes: { + reportId: string; + reportName: string; + }; + reportMetadata: { + detailColumns: string[]; + name: string; + reportFormat: string; + aggregates: string[]; + groupingsDown: { + name: string; + sortOrder: string; + dateGranularity: string; + column: string; + }[]; + }; + reportExtendedMetadata: { + detailColumnInfo: Record; + groupingColumnInfo: Record; + aggregateColumnInfo: Record; + }; + factMap: Record< + string, + { + rows: { dataCells: { label: string; value: unknown }[] }[]; + aggregates: { label: string; value: unknown }[]; + } + >; + groupingsDown: { + groupings: { key: string; label: string; value: unknown }[]; + }; } function transformReportToRows(report: SalesforceReportResponse): { - reportName: string; - reportId: string; - totalRows: number; - columns: string[]; - rows: Record[]; + reportName: string; + reportId: string; + totalRows: number; + columns: string[]; + rows: Record[]; } { - const detailColumns = report.reportMetadata?.detailColumns ?? []; - const detailColumnInfo = - report.reportExtendedMetadata?.detailColumnInfo ?? {}; - const groupingsDown = report.reportMetadata?.groupingsDown ?? []; - const groupingColumnInfo = - report.reportExtendedMetadata?.groupingColumnInfo ?? {}; - const factMap = report.factMap ?? {}; - - // Build ordered list of column labels for detail columns - const columnLabels = detailColumns.map( - (col) => detailColumnInfo[col]?.label ?? col - ); - - // Build grouping column labels - const groupingLabels = groupingsDown.map( - (g) => groupingColumnInfo[g.name]?.label ?? g.name - ); - - const allRows: Record[] = []; - - // Collect grouping labels from groupingsDown for grouped/summary reports - const groupingValues = extractGroupingValues( - report.groupingsDown?.groupings ?? [] - ); - - // Iterate over all factMap entries to collect rows - // Keys: "T!T" (tabular/grand total), "0!T", "1!T" (summary groups), "0!0", "1!0" (matrix), etc. - for (const [factMapKey, factMapEntry] of Object.entries(factMap)) { - if (!factMapEntry?.rows) continue; - - // Determine grouping context from the factMap key - const groupContext = resolveGroupingContext( - factMapKey, - groupingValues, - groupingLabels - ); - - for (const row of factMapEntry.rows) { - const rowObj: Record = {}; - - // Add grouping columns if present - for (const [key, value] of Object.entries(groupContext)) { - rowObj[key] = value; - } - - // Map each data cell to its column label - if (row.dataCells) { - for (let i = 0; i < row.dataCells.length; i++) { - const label = columnLabels[i] ?? `Column_${i}`; - const cell = row.dataCells[i]; - let value = cell.label ?? cell.value; - if (value === '-' || value === '--') { - value = ''; - } - rowObj[label] = value; - } - } - - allRows.push(rowObj); - } - } - - return { - reportName: - report.attributes?.reportName ?? - report.reportMetadata?.name ?? - 'Unknown Report', - reportId: report.attributes.reportId ?? 'Unknown Report', - totalRows: allRows.length, - columns: [...groupingLabels, ...columnLabels], - rows: allRows, - }; + const detailColumns = report.reportMetadata?.detailColumns ?? []; + const detailColumnInfo = report.reportExtendedMetadata?.detailColumnInfo ?? {}; + const groupingsDown = report.reportMetadata?.groupingsDown ?? []; + const groupingColumnInfo = report.reportExtendedMetadata?.groupingColumnInfo ?? {}; + const factMap = report.factMap ?? {}; + + // Build ordered list of column labels for detail columns + const columnLabels = detailColumns.map((col) => detailColumnInfo[col]?.label ?? col); + + // Build grouping column labels + const groupingLabels = groupingsDown.map((g) => groupingColumnInfo[g.name]?.label ?? g.name); + + const allRows: Record[] = []; + + // Collect grouping labels from groupingsDown for grouped/summary reports + const groupingValues = extractGroupingValues(report.groupingsDown?.groupings ?? []); + + // Iterate over all factMap entries to collect rows + // Keys: "T!T" (tabular/grand total), "0!T", "1!T" (summary groups), "0!0", "1!0" (matrix), etc. + for (const [factMapKey, factMapEntry] of Object.entries(factMap)) { + if (!factMapEntry?.rows) continue; + + // Determine grouping context from the factMap key + const groupContext = resolveGroupingContext(factMapKey, groupingValues, groupingLabels); + + for (const row of factMapEntry.rows) { + const rowObj: Record = {}; + + // Add grouping columns if present + for (const [key, value] of Object.entries(groupContext)) { + rowObj[key] = value; + } + + // Map each data cell to its column label + if (row.dataCells) { + for (let i = 0; i < row.dataCells.length; i++) { + const label = columnLabels[i] ?? `Column_${i}`; + const cell = row.dataCells[i]; + let value = cell.label ?? cell.value; + if (value === '-' || value === '--') { + value = ''; + } + rowObj[label] = value; + } + } + + allRows.push(rowObj); + } + } + + return { + reportName: + report.attributes?.reportName ?? report.reportMetadata?.name ?? 'Unknown Report', + reportId: report.attributes.reportId ?? 'Unknown Report', + totalRows: allRows.length, + columns: [...groupingLabels, ...columnLabels], + rows: allRows, + }; } function extractGroupingValues( - groupings: { - key: string; - label: string; - value: unknown; - groupings?: { key: string; label: string; value: unknown }[]; - }[], - depth = 0, - result: Record = {} + groupings: { + key: string; + label: string; + value: unknown; + groupings?: { key: string; label: string; value: unknown }[]; + }[], + depth = 0, + result: Record = {} ): Record { - for (const grouping of groupings) { - result[grouping.key] = { label: grouping.label, depth }; - if (grouping.groupings && grouping.groupings.length > 0) { - extractGroupingValues(grouping.groupings, depth + 1, result); - } - } - return result; + for (const grouping of groupings) { + result[grouping.key] = { label: grouping.label, depth }; + if (grouping.groupings && grouping.groupings.length > 0) { + extractGroupingValues(grouping.groupings, depth + 1, result); + } + } + return result; } function resolveGroupingContext( - factMapKey: string, - groupingValues: Record, - groupingLabels: string[] + factMapKey: string, + groupingValues: Record, + groupingLabels: string[] ): Record { - const context: Record = {}; + const context: Record = {}; - // factMap keys are like "0!T", "0_1!T", "T!T", etc. - // The part before "!" represents row groupings, after "!" represents column groupings - const [rowPart] = factMapKey.split('!'); + // factMap keys are like "0!T", "0_1!T", "T!T", etc. + // The part before "!" represents row groupings, after "!" represents column groupings + const [rowPart] = factMapKey.split('!'); - if (rowPart === 'T' || !rowPart) { - return context; // Grand total or no grouping - } + if (rowPart === 'T' || !rowPart) { + return context; // Grand total or no grouping + } - // Row grouping keys can be like "0", "0_1" (nested groupings) - const rowKeys = rowPart.split('_'); + // Row grouping keys can be like "0", "0_1" (nested groupings) + const rowKeys = rowPart.split('_'); - let currentKey = ''; + let currentKey = ''; - for (let depth = 0; depth < rowKeys.length; depth++) { - currentKey = - depth === 0 ? rowKeys[depth] : `${currentKey}_${rowKeys[depth]}`; + for (let depth = 0; depth < rowKeys.length; depth++) { + currentKey = depth === 0 ? rowKeys[depth] : `${currentKey}_${rowKeys[depth]}`; - const groupInfo = groupingValues[currentKey]; + const groupInfo = groupingValues[currentKey]; - if (!groupInfo) continue; + if (!groupInfo) continue; - const columnLabel = groupingLabels[depth] ?? `Group_${depth}`; + const columnLabel = groupingLabels[depth] ?? `Group_${depth}`; - context[columnLabel] = groupInfo.label; - } + context[columnLabel] = groupInfo.label; + } - return context; + return context; } diff --git a/packages/pieces/community/salesforce/src/lib/common/index.ts b/packages/pieces/community/salesforce/src/lib/common/index.ts index 5dc6bbd5a11..8c67096edea 100644 --- a/packages/pieces/community/salesforce/src/lib/common/index.ts +++ b/packages/pieces/community/salesforce/src/lib/common/index.ts @@ -1,712 +1,814 @@ import { - AuthenticationType, - HttpMethod, - HttpMessageBody, - HttpResponse, - httpClient, + AuthenticationType, + HttpMethod, + HttpMessageBody, + HttpResponse, + httpClient, } from '@activepieces/pieces-common'; import { OAuth2PropertyValue, Property } from '@activepieces/pieces-framework'; import { salesforceAuth } from '../..'; export const salesforcesCommon = { - account: Property.Dropdown({ - auth: salesforceAuth, - displayName: 'Account', - required: false, - refreshers: [], - options: async ({ auth }) => { - if (!auth) { - return { - disabled: true, - placeholder: 'Connect your account first', - options: [], - }; - } - const response = await querySalesforceApi<{ records: { Id: string, Name: string }[] }>( - HttpMethod.GET, - auth as OAuth2PropertyValue, - `SELECT Id, Name FROM Account ORDER BY Name LIMIT 100` - ); - return { - disabled: false, - options: response.body.records.map((record) => ({ - label: record.Name, - value: record.Id, - })), - }; - }, - }), - object: Property.Dropdown({ - auth: salesforceAuth, - displayName: 'Object', - required: true, - description: 'Select the Object', - refreshers: [], - options: async ({ auth }) => { - if (!auth) { - return { - disabled: true, - placeholder: 'connect your account first', - options: [], - }; - } - const options = await getSalesforceObjects(auth as OAuth2PropertyValue); - return { - disabled: false, - options: options.body['sobjects'] - .map((object: any) => { - return { - label: object.label, - value: object.name, - }; - }) - .sort((a: { label: string }, b: { label: string }) => - a.label.localeCompare(b.label) - ) - .filter((object: { label: string }) => !object.label.startsWith('_')), - }; - }, - }), - record: Property.Dropdown({ - auth: salesforceAuth, - displayName: 'Record', - description: 'The record to select. The list shows the 20 most recently created records.', - required: true, - refreshers: ['object'], - options: async ({ auth, object }) => { - if (!auth || !object) { - return { - disabled: true, - placeholder: 'Select an object first', - options: [], - }; - } + account: Property.Dropdown({ + auth: salesforceAuth, + displayName: 'Account', + required: false, + refreshers: [], + options: async ({ auth }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'Connect your account first', + options: [], + }; + } + const response = await querySalesforceApi<{ + records: { Id: string; Name: string }[]; + }>( + HttpMethod.GET, + auth as OAuth2PropertyValue, + `SELECT Id, Name FROM Account ORDER BY Name LIMIT 100` + ); + return { + disabled: false, + options: response.body.records.map((record) => ({ + label: record.Name, + value: record.Id, + })), + }; + }, + }), + object: Property.Dropdown({ + auth: salesforceAuth, + displayName: 'Object', + required: true, + description: 'Select the Object', + refreshers: [], + options: async ({ auth }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'connect your account first', + options: [], + }; + } + const options = await getSalesforceObjects(auth as OAuth2PropertyValue); + return { + disabled: false, + options: options.body['sobjects'] + .map((object: any) => { + return { + label: object.label, + value: object.name, + }; + }) + .sort((a: { label: string }, b: { label: string }) => + a.label.localeCompare(b.label) + ) + .filter((object: { label: string }) => !object.label.startsWith('_')), + }; + }, + }), + record: Property.Dropdown({ + auth: salesforceAuth, + displayName: 'Record', + description: 'The record to select. The list shows the 20 most recently created records.', + required: true, + refreshers: ['object'], + options: async ({ auth, object }) => { + if (!auth || !object) { + return { + disabled: true, + placeholder: 'Select an object first', + options: [], + }; + } - try { - - const describeResponse = await getSalesforceFields(auth as OAuth2PropertyValue, object as string); - const fields = describeResponse.body['fields'].map((f: any) => f.name); + try { + const describeResponse = await getSalesforceFields( + auth as OAuth2PropertyValue, + object as string + ); + const fields = describeResponse.body['fields'].map((f: any) => f.name); - - let displayField = 'Id'; - if (fields.includes('Name')) { - displayField = 'Name'; - } else if (fields.includes('Subject')) { - displayField = 'Subject'; - } else if (fields.includes('Title')) { - displayField = 'Title'; - } + let displayField = 'Id'; + if (fields.includes('Name')) { + displayField = 'Name'; + } else if (fields.includes('Subject')) { + displayField = 'Subject'; + } else if (fields.includes('Title')) { + displayField = 'Title'; + } - const response = await querySalesforceApi<{ records: { Id: string, [key: string]: any }[] }>( - HttpMethod.GET, - auth as OAuth2PropertyValue, - `SELECT Id, ${displayField} FROM ${object} ORDER BY CreatedDate DESC LIMIT 20` - ); - - return { - disabled: false, - options: response.body.records.map((record) => ({ - label: record[displayField] ?? record.Id, - value: record.Id, - })), - }; - } catch (e) { - console.error(e); - const fallbackResponse = await querySalesforceApi<{ records: { Id: string }[] }>( - HttpMethod.GET, - auth as OAuth2PropertyValue, - `SELECT Id FROM ${object} LIMIT 20` - ); - return { - disabled: false, - options: fallbackResponse.body.records.map((record) => ({ - label: record.Id, - value: record.Id, - })), - } - } - }, - }), - recipient: Property.Dropdown({ - auth: salesforceAuth, - displayName: 'Recipient', - required: true, - refreshers: [], - options: async ({ auth }) => { - if (!auth) { - return { - disabled: true, - placeholder: 'Connect your account first', - options: [], - }; - } + const response = await querySalesforceApi<{ + records: { Id: string; [key: string]: any }[]; + }>( + HttpMethod.GET, + auth as OAuth2PropertyValue, + `SELECT Id, ${displayField} FROM ${object} ORDER BY CreatedDate DESC LIMIT 20` + ); - const contactQuery = `SELECT Id, Name FROM Contact ORDER BY Name LIMIT 50`; - const leadQuery = `SELECT Id, Name FROM Lead ORDER BY Name LIMIT 50`; + return { + disabled: false, + options: response.body.records.map((record) => ({ + label: record[displayField] ?? record.Id, + value: record.Id, + })), + }; + } catch (e) { + console.error(e); + const fallbackResponse = await querySalesforceApi<{ + records: { Id: string }[]; + }>( + HttpMethod.GET, + auth as OAuth2PropertyValue, + `SELECT Id FROM ${object} LIMIT 20` + ); + return { + disabled: false, + options: fallbackResponse.body.records.map((record) => ({ + label: record.Id, + value: record.Id, + })), + }; + } + }, + }), + recipient: Property.Dropdown({ + auth: salesforceAuth, + displayName: 'Recipient', + required: true, + refreshers: [], + options: async ({ auth }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'Connect your account first', + options: [], + }; + } - const [contactsResponse, leadsResponse] = await Promise.all([ - querySalesforceApi<{ records: { Id: string, Name: string }[] }>(HttpMethod.GET, auth as OAuth2PropertyValue, contactQuery), - querySalesforceApi<{ records: { Id: string, Name: string }[] }>(HttpMethod.GET, auth as OAuth2PropertyValue, leadQuery) - ]); + const contactQuery = `SELECT Id, Name FROM Contact ORDER BY Name LIMIT 50`; + const leadQuery = `SELECT Id, Name FROM Lead ORDER BY Name LIMIT 50`; - const contactOptions = contactsResponse.body.records.map((record) => ({ - label: `${record.Name} (Contact)`, - value: record.Id, - })); + const [contactsResponse, leadsResponse] = await Promise.all([ + querySalesforceApi<{ records: { Id: string; Name: string }[] }>( + HttpMethod.GET, + auth as OAuth2PropertyValue, + contactQuery + ), + querySalesforceApi<{ records: { Id: string; Name: string }[] }>( + HttpMethod.GET, + auth as OAuth2PropertyValue, + leadQuery + ), + ]); - const leadOptions = leadsResponse.body.records.map((record) => ({ - label: `${record.Name} (Lead)`, - value: record.Id, - })); + const contactOptions = contactsResponse.body.records.map((record) => ({ + label: `${record.Name} (Contact)`, + value: record.Id, + })); - return { - disabled: false, - options: [...contactOptions, ...leadOptions].sort((a, b) => a.label.localeCompare(b.label)), - }; - }, - }), - field: Property.Dropdown({ - auth: salesforceAuth, - displayName: 'Field', - description: 'Select the Field', - required: true, - refreshers: ['object'], - options: async ({ auth, object }) => { - if (auth === undefined || !object) { - return { - disabled: true, - placeholder: 'connect your account first', - options: [], - }; - } - const options = await getSalesforceFields( - auth as OAuth2PropertyValue, - object as string - ); - return { - disabled: false, - options: options.body['fields'].map((field: any) => { - return { - label: field.label, - value: field.name, - }; - }), - }; - }, - }), - campaign: Property.Dropdown({ - auth: salesforceAuth, - displayName: 'Campaign', - required: true, - refreshers: [], - options: async ({ auth }) => { - if (!auth) { - return { - disabled: true, - placeholder: 'Connect your account first', - options: [], - }; - } - const response = await querySalesforceApi<{ records: { Id: string, Name: string }[] }>( - HttpMethod.GET, - auth as OAuth2PropertyValue, - "SELECT Id, Name FROM Campaign ORDER BY Name LIMIT 200" - ); - return { - disabled: false, - options: response.body.records.map((record) => ({ - label: record.Name, - value: record.Id, - })), - }; - }, - }), - contact: Property.Dropdown({ - auth: salesforceAuth, - displayName: 'Contact', - required: true, - refreshers: [], - options: async ({ auth }) => { - if (!auth) { - return { - disabled: true, - placeholder: 'Connect your account first', - options: [], - }; - } - const response = await querySalesforceApi<{ records: { Id: string, Name: string, Email?: string }[] }>( - HttpMethod.GET, - auth as OAuth2PropertyValue, - "SELECT Id, Name, Email FROM Contact ORDER BY Name LIMIT 200" - ); - return { - disabled: false, - options: response.body.records.map((record) => ({ - label: record.Email ? `${record.Name} — ${record.Email}` : record.Name, - value: record.Id, - })), - }; - }, - }), - lead: Property.Dropdown({ - auth: salesforceAuth, - displayName: 'Lead', - required: true, - refreshers: [], - options: async ({ auth }) => { - if (!auth) { - return { - disabled: true, - placeholder: 'Connect your account first', - options: [], - }; - } - const response = await querySalesforceApi<{ records: { Id: string, Name: string }[] }>( - HttpMethod.GET, - auth as OAuth2PropertyValue, - "SELECT Id, Name FROM Lead ORDER BY Name LIMIT 200" - ); - return { - disabled: false, - options: response.body.records.map((record) => ({ - label: record.Name, - value: record.Id, - })), - }; - }, - }), - status: Property.Dropdown({ - auth: salesforceAuth, - displayName: 'Status', - description: "The campaign member status (e.g., 'Sent', 'Responded').", - required: true, - refreshers: ['campaign_id'], - options: async ({ auth, campaign_id }) => { - if (!auth || !campaign_id) { - return { - disabled: true, - placeholder: 'Select a campaign first', - options: [], - }; - } - // Validate campaign_id to prevent SQL injection (Salesforce IDs are 15-18 alphanumeric characters) - const campaignIdStr = String(campaign_id); - if (!/^[a-zA-Z0-9]{15,18}$/.test(campaignIdStr)) { - return { - disabled: true, - placeholder: 'Invalid campaign ID', - options: [], - }; - } - const response = await querySalesforceApi<{ records: { Label: string }[] }>( - HttpMethod.GET, - auth as OAuth2PropertyValue, - `SELECT Label FROM CampaignMemberStatus WHERE CampaignId = '${campaignIdStr}'` - ); - return { - disabled: false, - options: response.body.records.map((record) => ({ - label: record.Label, - value: record.Label, - })), - }; - }, - }), - leadSource: Property.Dropdown({ - auth: salesforceAuth, - displayName: 'Lead Source', - required: false, - refreshers: [], - options: async ({ auth }) => { - if (!auth) { - return { - disabled: true, - placeholder: 'Connect your account first', - options: [], - }; - } - try { - const describeResponse = await getSalesforceFields(auth as OAuth2PropertyValue, 'Lead'); - const leadSourceField = describeResponse.body['fields'].find((field: any) => field.name === 'LeadSource'); - - if (!leadSourceField || !leadSourceField.picklistValues) { - return { disabled: true, placeholder: 'Lead Source field not found or not a picklist', options: [] }; - } + const leadOptions = leadsResponse.body.records.map((record) => ({ + label: `${record.Name} (Lead)`, + value: record.Id, + })); - return { - disabled: false, - options: leadSourceField.picklistValues.map((value: any) => { - return { - label: value.label, - value: value.value, - }; - }), - }; - } catch (e) { - console.error(e); - return { - disabled: true, - placeholder: "Couldn't fetch lead sources", - options: [], - } - } - }, - }), - owner: Property.Dropdown({ - auth: salesforceAuth, - displayName: 'Owner', - description: 'The owner of the task.', - required: true, - refreshers: [], - options: async ({ auth }) => { - if (!auth) { - return { disabled: true, placeholder: 'Connect your account first', options: [] }; - } - const response = await querySalesforceApi<{ records: { Id: string, Name: string }[] }>( - HttpMethod.GET, - auth as OAuth2PropertyValue, - "SELECT Id, Name FROM User WHERE IsActive = true ORDER BY Name" - ); - return { - disabled: false, - options: response.body.records.map((record) => ({ - label: record.Name, - value: record.Id, - })), - }; - }, - }), - opportunity: Property.Dropdown({ - auth: salesforceAuth, - displayName: 'Opportunity', - required: true, - refreshers: [], - options: async ({ auth }) => { - if (!auth) { - return { disabled: true, placeholder: 'Connect your account first', options: [] }; - } - const response = await querySalesforceApi<{ records: { Id: string, Name: string }[] }>( - HttpMethod.GET, - auth as OAuth2PropertyValue, - "SELECT Id, Name FROM Opportunity ORDER BY CreatedDate DESC LIMIT 100" - ); - return { - disabled: false, - options: response.body.records.map((record) => ({ - label: record.Name, - value: record.Id, - })), - }; - }, - }), - report: Property.Dropdown({ - auth: salesforceAuth, - displayName: 'Report', - required: true, - refreshers: [], - options: async ({ auth }) => { - if (!auth) { - return { disabled: true, placeholder: 'Connect your account first', options: [] }; - } - const response = await querySalesforceApi<{ records: { Id: string, Name: string }[] }>( - HttpMethod.GET, - auth as OAuth2PropertyValue, - "SELECT Id, Name FROM Report ORDER BY Name" - ); - return { - disabled: false, - options: response.body.records.map((record) => ({ - label: record.Name, - value: record.Id, - })), - }; - }, - }), - parentRecord: Property.Dropdown({ - auth: salesforceAuth, - displayName: 'Parent Record', - description: 'The parent record to find child records for. The list shows the 20 most recently created records.', - required: true, - refreshers: ['parent_object'], - options: async ({ auth, parent_object }) => { - if (!auth || !parent_object) { - return { disabled: true, placeholder: 'Select a parent object first', options: [] }; - } - const response = await querySalesforceApi<{ records: { Id: string, Name?: string }[] }>( - HttpMethod.GET, - auth as OAuth2PropertyValue, - `SELECT Id, Name FROM ${parent_object} ORDER BY CreatedDate DESC LIMIT 20` - ); - return { - disabled: false, - options: response.body.records.map((record) => ({ - label: record.Name ?? record.Id, - value: record.Id, - })), - }; - }, - }), + return { + disabled: false, + options: [...contactOptions, ...leadOptions].sort((a, b) => + a.label.localeCompare(b.label) + ), + }; + }, + }), + field: Property.Dropdown({ + auth: salesforceAuth, + displayName: 'Field', + description: 'Select the Field', + required: true, + refreshers: ['object'], + options: async ({ auth, object }) => { + if (auth === undefined || !object) { + return { + disabled: true, + placeholder: 'connect your account first', + options: [], + }; + } + const options = await getSalesforceFields( + auth as OAuth2PropertyValue, + object as string + ); + return { + disabled: false, + options: options.body['fields'].map((field: any) => { + return { + label: field.label, + value: field.name, + }; + }), + }; + }, + }), + campaign: Property.Dropdown({ + auth: salesforceAuth, + displayName: 'Campaign', + required: true, + refreshers: [], + options: async ({ auth }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'Connect your account first', + options: [], + }; + } + const response = await querySalesforceApi<{ + records: { Id: string; Name: string }[]; + }>( + HttpMethod.GET, + auth as OAuth2PropertyValue, + 'SELECT Id, Name FROM Campaign ORDER BY Name LIMIT 200' + ); + return { + disabled: false, + options: response.body.records.map((record) => ({ + label: record.Name, + value: record.Id, + })), + }; + }, + }), + contact: Property.Dropdown({ + auth: salesforceAuth, + displayName: 'Contact', + required: true, + refreshers: [], + options: async ({ auth }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'Connect your account first', + options: [], + }; + } + const response = await querySalesforceApi<{ + records: { Id: string; Name: string; Email?: string }[]; + }>( + HttpMethod.GET, + auth as OAuth2PropertyValue, + 'SELECT Id, Name, Email FROM Contact ORDER BY Name LIMIT 200' + ); + return { + disabled: false, + options: response.body.records.map((record) => ({ + label: record.Email ? `${record.Name} — ${record.Email}` : record.Name, + value: record.Id, + })), + }; + }, + }), + lead: Property.Dropdown({ + auth: salesforceAuth, + displayName: 'Lead', + required: true, + refreshers: [], + options: async ({ auth }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'Connect your account first', + options: [], + }; + } + const response = await querySalesforceApi<{ + records: { Id: string; Name: string }[]; + }>( + HttpMethod.GET, + auth as OAuth2PropertyValue, + 'SELECT Id, Name FROM Lead ORDER BY Name LIMIT 200' + ); + return { + disabled: false, + options: response.body.records.map((record) => ({ + label: record.Name, + value: record.Id, + })), + }; + }, + }), + status: Property.Dropdown({ + auth: salesforceAuth, + displayName: 'Status', + description: "The campaign member status (e.g., 'Sent', 'Responded').", + required: true, + refreshers: ['campaign_id'], + options: async ({ auth, campaign_id }) => { + if (!auth || !campaign_id) { + return { + disabled: true, + placeholder: 'Select a campaign first', + options: [], + }; + } + // Validate campaign_id to prevent SQL injection (Salesforce IDs are 15-18 alphanumeric characters) + const campaignIdStr = String(campaign_id); + if (!/^[a-zA-Z0-9]{15,18}$/.test(campaignIdStr)) { + return { + disabled: true, + placeholder: 'Invalid campaign ID', + options: [], + }; + } + const response = await querySalesforceApi<{ + records: { Label: string }[]; + }>( + HttpMethod.GET, + auth as OAuth2PropertyValue, + `SELECT Label FROM CampaignMemberStatus WHERE CampaignId = '${campaignIdStr}'` + ); + return { + disabled: false, + options: response.body.records.map((record) => ({ + label: record.Label, + value: record.Label, + })), + }; + }, + }), + leadSource: Property.Dropdown({ + auth: salesforceAuth, + displayName: 'Lead Source', + required: false, + refreshers: [], + options: async ({ auth }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'Connect your account first', + options: [], + }; + } + try { + const describeResponse = await getSalesforceFields( + auth as OAuth2PropertyValue, + 'Lead' + ); + const leadSourceField = describeResponse.body['fields'].find( + (field: any) => field.name === 'LeadSource' + ); - childRelationship: Property.Dropdown({ - auth: salesforceAuth, - displayName: 'Child Relationship', - description: 'The child relationship to retrieve records from.', - required: true, - refreshers: ['parent_object'], - options: async ({ auth, parent_object }) => { - if (!auth || !parent_object) { - return { disabled: true, placeholder: 'Select a parent object first', options: [] }; - } - try { - const describeResponse = await getSalesforceFields(auth as OAuth2PropertyValue, parent_object as string); - const relationships = describeResponse.body['childRelationships']; - if (!relationships) { - return { disabled: true, placeholder: 'No child relationships found for this object', options: [] }; - } - return { - disabled: false, - options: relationships.map((rel: any) => ({ - label: `${rel.relationshipName} (${rel.childSObject})`, - value: rel.relationshipName, - })), - }; - } catch (e) { - console.error(e); - return { disabled: true, placeholder: "Couldn't fetch child relationships", options: [] }; - } - }, - }), - optionalContact: Property.Dropdown({ - auth: salesforceAuth, - displayName: 'Contact', - required: false, - refreshers: [], - options: async ({ auth }) => { - if (!auth) { - return { disabled: true, placeholder: 'Connect your account first', options: [] }; - } - const response = await querySalesforceApi<{ records: { Id: string, Name: string, Email?: string }[] }>( - HttpMethod.GET, - auth as OAuth2PropertyValue, - "SELECT Id, Name, Email FROM Contact ORDER BY Name LIMIT 200" - ); - return { - disabled: false, - options: response.body.records.map((record) => ({ - label: record.Email ? `${record.Name} — ${record.Email}` : record.Name, - value: record.Id, - })), - }; - }, - }), - taskStatus: createSalesforcePicklistDropdown({ - objectName: 'Task', - fieldName: 'Status', - displayName: 'Status', - required: true - }), + if (!leadSourceField || !leadSourceField.picklistValues) { + return { + disabled: true, + placeholder: 'Lead Source field not found or not a picklist', + options: [], + }; + } - taskPriority: createSalesforcePicklistDropdown({ - objectName: 'Task', - fieldName: 'Priority', - displayName: 'Priority', - required: true - }), + return { + disabled: false, + options: leadSourceField.picklistValues.map((value: any) => { + return { + label: value.label, + value: value.value, + }; + }), + }; + } catch (e) { + console.error(e); + return { + disabled: true, + placeholder: "Couldn't fetch lead sources", + options: [], + }; + } + }, + }), + owner: Property.Dropdown({ + auth: salesforceAuth, + displayName: 'Owner', + description: 'The owner of the task.', + required: true, + refreshers: [], + options: async ({ auth }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'Connect your account first', + options: [], + }; + } + const response = await querySalesforceApi<{ + records: { Id: string; Name: string }[]; + }>( + HttpMethod.GET, + auth as OAuth2PropertyValue, + 'SELECT Id, Name FROM User WHERE IsActive = true ORDER BY Name' + ); + return { + disabled: false, + options: response.body.records.map((record) => ({ + label: record.Name, + value: record.Id, + })), + }; + }, + }), + opportunity: Property.Dropdown({ + auth: salesforceAuth, + displayName: 'Opportunity', + required: true, + refreshers: [], + options: async ({ auth }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'Connect your account first', + options: [], + }; + } + const response = await querySalesforceApi<{ + records: { Id: string; Name: string }[]; + }>( + HttpMethod.GET, + auth as OAuth2PropertyValue, + 'SELECT Id, Name FROM Opportunity ORDER BY CreatedDate DESC LIMIT 100' + ); + return { + disabled: false, + options: response.body.records.map((record) => ({ + label: record.Name, + value: record.Id, + })), + }; + }, + }), + report: Property.Dropdown({ + auth: salesforceAuth, + displayName: 'Report', + required: true, + refreshers: [], + refreshOnSearch: true, + options: async ({ auth }, { searchValue }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'Connect your account first', + options: [], + }; + } - caseStatus: createSalesforcePicklistDropdown({ - objectName: 'Case', - fieldName: 'Status', - displayName: 'Status', - required: false - }), + let query = 'SELECT Id, Name FROM Report'; - casePriority: createSalesforcePicklistDropdown({ - objectName: 'Case', - fieldName: 'Priority', - displayName: 'Priority', - required: false - }), + if (searchValue) { + const sanitizedSearch = searchValue.replace(/'/g, "\\'"); + query += ` WHERE Name LIKE '${sanitizedSearch}%'`; + } - caseOrigin: createSalesforcePicklistDropdown({ - objectName: 'Case', - fieldName: 'Origin', - displayName: 'Origin', - required: false - }), - opportunityStage: createSalesforcePicklistDropdown({ - objectName: 'Opportunity', - fieldName: 'StageName', - displayName: 'Stage', - required: true - }), - + query += ' ORDER BY Name'; + + const response = await querySalesforceApi<{ + records: { Id: string; Name: string }[]; + }>(HttpMethod.GET, auth as OAuth2PropertyValue, query); + return { + disabled: false, + options: response.body.records.map((record) => ({ + label: record.Name, + value: record.Id, + })), + }; + }, + }), + parentRecord: Property.Dropdown({ + auth: salesforceAuth, + displayName: 'Parent Record', + description: + 'The parent record to find child records for. The list shows the 20 most recently created records.', + required: true, + refreshers: ['parent_object'], + options: async ({ auth, parent_object }) => { + if (!auth || !parent_object) { + return { + disabled: true, + placeholder: 'Select a parent object first', + options: [], + }; + } + const response = await querySalesforceApi<{ + records: { Id: string; Name?: string }[]; + }>( + HttpMethod.GET, + auth as OAuth2PropertyValue, + `SELECT Id, Name FROM ${parent_object} ORDER BY CreatedDate DESC LIMIT 20` + ); + return { + disabled: false, + options: response.body.records.map((record) => ({ + label: record.Name ?? record.Id, + value: record.Id, + })), + }; + }, + }), + + childRelationship: Property.Dropdown({ + auth: salesforceAuth, + displayName: 'Child Relationship', + description: 'The child relationship to retrieve records from.', + required: true, + refreshers: ['parent_object'], + options: async ({ auth, parent_object }) => { + if (!auth || !parent_object) { + return { + disabled: true, + placeholder: 'Select a parent object first', + options: [], + }; + } + try { + const describeResponse = await getSalesforceFields( + auth as OAuth2PropertyValue, + parent_object as string + ); + const relationships = describeResponse.body['childRelationships']; + if (!relationships) { + return { + disabled: true, + placeholder: 'No child relationships found for this object', + options: [], + }; + } + return { + disabled: false, + options: relationships.map((rel: any) => ({ + label: `${rel.relationshipName} (${rel.childSObject})`, + value: rel.relationshipName, + })), + }; + } catch (e) { + console.error(e); + return { + disabled: true, + placeholder: "Couldn't fetch child relationships", + options: [], + }; + } + }, + }), + optionalContact: Property.Dropdown({ + auth: salesforceAuth, + displayName: 'Contact', + required: false, + refreshers: [], + options: async ({ auth }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'Connect your account first', + options: [], + }; + } + const response = await querySalesforceApi<{ + records: { Id: string; Name: string; Email?: string }[]; + }>( + HttpMethod.GET, + auth as OAuth2PropertyValue, + 'SELECT Id, Name, Email FROM Contact ORDER BY Name LIMIT 200' + ); + return { + disabled: false, + options: response.body.records.map((record) => ({ + label: record.Email ? `${record.Name} — ${record.Email}` : record.Name, + value: record.Id, + })), + }; + }, + }), + taskStatus: createSalesforcePicklistDropdown({ + objectName: 'Task', + fieldName: 'Status', + displayName: 'Status', + required: true, + }), + + taskPriority: createSalesforcePicklistDropdown({ + objectName: 'Task', + fieldName: 'Priority', + displayName: 'Priority', + required: true, + }), + + caseStatus: createSalesforcePicklistDropdown({ + objectName: 'Case', + fieldName: 'Status', + displayName: 'Status', + required: false, + }), + + casePriority: createSalesforcePicklistDropdown({ + objectName: 'Case', + fieldName: 'Priority', + displayName: 'Priority', + required: false, + }), + + caseOrigin: createSalesforcePicklistDropdown({ + objectName: 'Case', + fieldName: 'Origin', + displayName: 'Origin', + required: false, + }), + opportunityStage: createSalesforcePicklistDropdown({ + objectName: 'Opportunity', + fieldName: 'StageName', + displayName: 'Stage', + required: true, + }), }; function createSalesforcePicklistDropdown(config: { - objectName: string, - fieldName: string, - displayName: string, - required: boolean, - description?: string, + objectName: string; + fieldName: string; + displayName: string; + required: boolean; + description?: string; }) { - return Property.Dropdown({ - auth: salesforceAuth, - displayName: config.displayName, - description: config.description, - required: config.required, - refreshers: [], - options: async ({ auth }) => { - if (!auth) { - return { disabled: true, placeholder: 'Connect your account first', options: [] }; - } - try { - const describeResponse = await getSalesforceFields(auth as OAuth2PropertyValue, config.objectName); - const field = describeResponse.body['fields'].find((field: any) => field.name === config.fieldName); - if (!field || !field.picklistValues) { - return { disabled: true, placeholder: `${config.fieldName} field not found or not a picklist`, options: [] }; - } - return { - disabled: false, - options: field.picklistValues.map((value: any) => ({ - label: value.label, - value: value.value, - })), - }; - } catch (e) { - console.error(e); - return { disabled: true, placeholder: `Couldn't fetch ${config.fieldName} values`, options: [] }; - } - }, - }); - + return Property.Dropdown({ + auth: salesforceAuth, + displayName: config.displayName, + description: config.description, + required: config.required, + refreshers: [], + options: async ({ auth }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'Connect your account first', + options: [], + }; + } + try { + const describeResponse = await getSalesforceFields( + auth as OAuth2PropertyValue, + config.objectName + ); + const field = describeResponse.body['fields'].find( + (field: any) => field.name === config.fieldName + ); + if (!field || !field.picklistValues) { + return { + disabled: true, + placeholder: `${config.fieldName} field not found or not a picklist`, + options: [], + }; + } + return { + disabled: false, + options: field.picklistValues.map((value: any) => ({ + label: value.label, + value: value.value, + })), + }; + } catch (e) { + console.error(e); + return { + disabled: true, + placeholder: `Couldn't fetch ${config.fieldName} values`, + options: [], + }; + } + }, + }); } export async function callSalesforceApi( - method: HttpMethod, - authentication: OAuth2PropertyValue, - url: string, - body: Record | undefined + method: HttpMethod, + authentication: OAuth2PropertyValue, + url: string, + body: Record | undefined ): Promise> { - return await httpClient.sendRequest({ - method: method, - url: `${authentication.data['instance_url']}${url}`, - body, - authentication: { - type: AuthenticationType.BEARER_TOKEN, - token: authentication['access_token'], - }, - }); + return await httpClient.sendRequest({ + method: method, + url: `${authentication.data['instance_url']}${url}`, + body, + authentication: { + type: AuthenticationType.BEARER_TOKEN, + token: authentication['access_token'], + }, + }); } export async function querySalesforceApi( - method: HttpMethod, - authentication: OAuth2PropertyValue, - query: string + method: HttpMethod, + authentication: OAuth2PropertyValue, + query: string ): Promise> { - return await httpClient.sendRequest({ - method: method, - url: `${authentication.data['instance_url']}/services/data/v56.0/query`, - queryParams: { - q: query, - }, - authentication: { - type: AuthenticationType.BEARER_TOKEN, - token: authentication['access_token'], - }, - }); + return await httpClient.sendRequest({ + method: method, + url: `${authentication.data['instance_url']}/services/data/v56.0/query`, + queryParams: { + q: query, + }, + authentication: { + type: AuthenticationType.BEARER_TOKEN, + token: authentication['access_token'], + }, + }); } export async function createBulkJob( - method: HttpMethod, - authentication: OAuth2PropertyValue, - jobDetails: HttpMessageBody + method: HttpMethod, + authentication: OAuth2PropertyValue, + jobDetails: HttpMessageBody ): Promise> { - return await httpClient.sendRequest({ - method: method, - url: `${authentication.data['instance_url']}/services/data/v58.0/jobs/ingest/`, - body: jobDetails, - authentication: { - type: AuthenticationType.BEARER_TOKEN, - token: authentication['access_token'], - }, - }); + return await httpClient.sendRequest({ + method: method, + url: `${authentication.data['instance_url']}/services/data/v58.0/jobs/ingest/`, + body: jobDetails, + authentication: { + type: AuthenticationType.BEARER_TOKEN, + token: authentication['access_token'], + }, + }); } export async function uploadToBulkJob( - method: HttpMethod, - authentication: OAuth2PropertyValue, - jobId: string, - csv: string + method: HttpMethod, + authentication: OAuth2PropertyValue, + jobId: string, + csv: string ): Promise> { - return await httpClient.sendRequest({ - method: method, - url: `${authentication.data['instance_url']}/services/data/v58.0/jobs/ingest/${jobId}/batches`, - headers: { - 'Content-Type': 'text/csv', - }, - body: csv as unknown as HttpMessageBody, - authentication: { - type: AuthenticationType.BEARER_TOKEN, - token: authentication['access_token'], - }, - }); + return await httpClient.sendRequest({ + method: method, + url: `${authentication.data['instance_url']}/services/data/v58.0/jobs/ingest/${jobId}/batches`, + headers: { + 'Content-Type': 'text/csv', + }, + body: csv as unknown as HttpMessageBody, + authentication: { + type: AuthenticationType.BEARER_TOKEN, + token: authentication['access_token'], + }, + }); } export async function notifyBulkJobComplete( - method: HttpMethod, - authentication: OAuth2PropertyValue, - message: HttpMessageBody, - jobId: string + method: HttpMethod, + authentication: OAuth2PropertyValue, + message: HttpMessageBody, + jobId: string ): Promise> { - return await httpClient.sendRequest({ - method: method, - url: `${authentication.data['instance_url']}/services/data/v58.0/jobs/ingest/${jobId}`, - body: message, - authentication: { - type: AuthenticationType.BEARER_TOKEN, - token: authentication['access_token'], - }, - }); + return await httpClient.sendRequest({ + method: method, + url: `${authentication.data['instance_url']}/services/data/v58.0/jobs/ingest/${jobId}`, + body: message, + authentication: { + type: AuthenticationType.BEARER_TOKEN, + token: authentication['access_token'], + }, + }); } export async function getBulkJobInfo( - method: HttpMethod, - authentication: OAuth2PropertyValue, - jobId: string + method: HttpMethod, + authentication: OAuth2PropertyValue, + jobId: string ): Promise> { - return await httpClient.sendRequest({ - method: method, - url: `${authentication.data['instance_url']}/services/data/v58.0/jobs/ingest/${jobId}`, - authentication: { - type: AuthenticationType.BEARER_TOKEN, - token: authentication['access_token'], - }, - }); + return await httpClient.sendRequest({ + method: method, + url: `${authentication.data['instance_url']}/services/data/v58.0/jobs/ingest/${jobId}`, + authentication: { + type: AuthenticationType.BEARER_TOKEN, + token: authentication['access_token'], + }, + }); } async function getSalesforceObjects( - authentication: OAuth2PropertyValue + authentication: OAuth2PropertyValue ): Promise> { - return await httpClient.sendRequest({ - method: HttpMethod.GET, - url: `${authentication.data['instance_url']}/services/data/v56.0/sobjects`, - authentication: { - type: AuthenticationType.BEARER_TOKEN, - token: authentication['access_token'], - }, - }); + return await httpClient.sendRequest({ + method: HttpMethod.GET, + url: `${authentication.data['instance_url']}/services/data/v56.0/sobjects`, + authentication: { + type: AuthenticationType.BEARER_TOKEN, + token: authentication['access_token'], + }, + }); } // Write function to list all fields name inside salesforce object async function getSalesforceFields( - authentication: OAuth2PropertyValue, - object: string + authentication: OAuth2PropertyValue, + object: string ): Promise> { - return await httpClient.sendRequest({ - method: HttpMethod.GET, - url: `${authentication.data['instance_url']}/services/data/v56.0/sobjects/${object}/describe`, - authentication: { - type: AuthenticationType.BEARER_TOKEN, - token: authentication['access_token'], - }, - }); -} \ No newline at end of file + return await httpClient.sendRequest({ + method: HttpMethod.GET, + url: `${authentication.data['instance_url']}/services/data/v56.0/sobjects/${object}/describe`, + authentication: { + type: AuthenticationType.BEARER_TOKEN, + token: authentication['access_token'], + }, + }); +}