diff --git a/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts b/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts index 49292d869c..78454e3519 100644 --- a/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts @@ -6,6 +6,7 @@ import type { JsonObject, IHttpRequestMethods, IRequestOptions, + IPollFunctions, } from 'n8n-workflow'; import { NodeApiError } from 'n8n-workflow'; @@ -14,7 +15,7 @@ import moment from 'moment-timezone'; import jwt from 'jsonwebtoken'; function getOptions( - this: IExecuteFunctions | ILoadOptionsFunctions, + this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions, method: IHttpRequestMethods, endpoint: string, @@ -41,7 +42,7 @@ function getOptions( } async function getAccessToken( - this: IExecuteFunctions | ILoadOptionsFunctions, + this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions, credentials: IDataObject, ): Promise { const now = moment().unix(); @@ -83,7 +84,7 @@ async function getAccessToken( } export async function salesforceApiRequest( - this: IExecuteFunctions | ILoadOptionsFunctions, + this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions, method: IHttpRequestMethods, endpoint: string, @@ -142,7 +143,7 @@ export async function salesforceApiRequest( } export async function salesforceApiRequestAllItems( - this: IExecuteFunctions | ILoadOptionsFunctions, + this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions, propertyName: string, method: IHttpRequestMethods, endpoint: string, diff --git a/packages/nodes-base/nodes/Salesforce/SalesforceTrigger.node.ts b/packages/nodes-base/nodes/Salesforce/SalesforceTrigger.node.ts new file mode 100644 index 0000000000..613ff5daca --- /dev/null +++ b/packages/nodes-base/nodes/Salesforce/SalesforceTrigger.node.ts @@ -0,0 +1,290 @@ +import { NodeApiError } from 'n8n-workflow'; + +import type { + IDataObject, + IPollFunctions, + INodeExecutionData, + INodeType, + INodeTypeDescription, + ILoadOptionsFunctions, + INodePropertyOptions, + JsonObject, +} from 'n8n-workflow'; + +import { DateTime } from 'luxon'; +import { + getQuery, + salesforceApiRequest, + salesforceApiRequestAllItems, + sortOptions, +} from './GenericFunctions'; + +export class SalesforceTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Salesforce Trigger', + name: 'salesforceTrigger', + icon: 'file:salesforce.svg', + group: ['trigger'], + version: 1, + description: + 'Fetches data from Salesforce and starts the workflow on specified polling intervals.', + subtitle: '={{($parameter["triggerOn"])}}', + defaults: { + name: 'Salesforce Trigger', + }, + credentials: [ + { + name: 'salesforceOAuth2Api', + required: true, + }, + ], + polling: true, + inputs: [], + outputs: ['main'], + properties: [ + { + displayName: 'Trigger On', + name: 'triggerOn', + description: 'Which Salesforce event should trigger the node', + type: 'options', + default: '', + options: [ + { + name: 'Account Created', + value: 'accountCreated', + description: 'When a new account is created', + }, + { + name: 'Account Updated', + value: 'accountUpdated', + description: 'When an existing account is modified', + }, + { + name: 'Attachment Created', + value: 'attachmentCreated', + description: 'When a file is uploaded and attached to an object', + }, + { + name: 'Attachment Updated', + value: 'attachmentUpdated', + description: 'When an existing file is modified', + }, + { + name: 'Case Created', + value: 'caseCreated', + description: 'When a new case is created', + }, + { + name: 'Case Updated', + value: 'caseUpdated', + description: 'When an existing case is modified', + }, + { + name: 'Contact Created', + value: 'contactCreated', + description: 'When a new contact is created', + }, + { + name: 'Contact Updated', + value: 'contactUpdated', + description: 'When an existing contact is modified', + }, + { + name: 'Custom Object Created', + value: 'customObjectCreated', + description: 'When a new object of a given type is created', + }, + { + name: 'Custom Object Updated', + value: 'customObjectUpdated', + description: 'When an object of a given type is modified', + }, + { + name: 'Lead Created', + value: 'leadCreated', + description: 'When a new lead is created', + }, + { + name: 'Lead Updated', + value: 'leadUpdated', + description: 'When an existing lead is modified', + }, + { + name: 'Opportunity Created', + value: 'opportunityCreated', + description: 'When a new opportunity is created', + }, + { + name: 'Opportunity Updated', + value: 'opportunityUpdated', + description: 'When an existing opportunity is created', + }, + { + name: 'Task Created', + value: 'taskCreated', + description: 'When a new task is created', + }, + { + name: 'Task Updated', + value: 'taskUpdated', + description: 'When an existing task is modified', + }, + { + name: 'User Created', + value: 'userCreated', + description: 'When a new user is created', + }, + { + name: 'User Updated', + value: 'userUpdated', + description: 'When an existing user is modified', + }, + ], + }, + { + displayName: 'Custom Object Name or ID', + name: 'customObject', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCustomObjects', + }, + required: true, + default: '', + displayOptions: { + show: { + triggerOn: ['customObjectUpdated', 'customObjectCreated'], + }, + }, + description: + 'Name of the custom object. Choose from the list, or specify an ID using an expression.', + }, + ], + }; + + methods = { + loadOptions: { + // Get all the custom objects recurrence instances to display them to user so that they can + // select them easily + async getCustomObjects(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + // TODO: find a way to filter this object to get just the lead sources instead of the whole object + const { sobjects: objects } = await salesforceApiRequest.call(this, 'GET', '/sobjects'); + for (const object of objects) { + if (object.custom === true) { + const objectName = object.label; + const objectId = object.name; + returnData.push({ + name: objectName, + value: objectId, + }); + } + } + sortOptions(returnData); + return returnData; + }, + }, + }; + + async poll(this: IPollFunctions): Promise { + const workflowData = this.getWorkflowStaticData('node'); + let responseData; + const qs: IDataObject = {}; + const triggerOn = this.getNodeParameter('triggerOn') as string; + let triggerResource = triggerOn.slice(0, 1).toUpperCase() + triggerOn.slice(1, -7); + const changeType = triggerOn.slice(-7); + + if (triggerResource === 'CustomObject') { + triggerResource = this.getNodeParameter('customObject') as string; + } + + const now = DateTime.now().toISO(); + const startDate = (workflowData.lastTimeChecked as string) || now; + const endDate = now; + try { + const pollStartDate = startDate; + const pollEndDate = endDate; + + const options = { + conditionsUi: { + conditionValues: [] as IDataObject[], + }, + }; + if (this.getMode() !== 'manual') { + if (changeType === 'Created') { + options.conditionsUi.conditionValues.push({ + field: 'CreatedDate', + operation: '>=', + value: pollStartDate, + }); + options.conditionsUi.conditionValues.push({ + field: 'CreatedDate', + operation: '<', + value: pollEndDate, + }); + } else { + options.conditionsUi.conditionValues.push({ + field: 'LastModifiedDate', + operation: '>=', + value: pollStartDate, + }); + options.conditionsUi.conditionValues.push({ + field: 'LastModifiedDate', + operation: '<', + value: pollEndDate, + }); + // make sure the resource wasn't just created. + options.conditionsUi.conditionValues.push({ + field: 'CreatedDate', + operation: '<', + value: pollStartDate, + }); + } + } + + try { + if (this.getMode() === 'manual') { + qs.q = getQuery(options, triggerResource, false, 1); + } else { + qs.q = getQuery(options, triggerResource, true); + } + responseData = await salesforceApiRequestAllItems.call( + this, + 'records', + 'GET', + '/query', + {}, + qs, + ); + } catch (error) { + throw new NodeApiError(this.getNode(), error as JsonObject); + } + + if (!responseData?.length) { + workflowData.lastTimeChecked = endDate; + return null; + } + } catch (error) { + if (this.getMode() === 'manual' || !workflowData.lastTimeChecked) { + throw error; + } + const workflow = this.getWorkflow(); + const node = this.getNode(); + this.logger.error( + `There was a problem in '${node.name}' node in workflow '${workflow.id}': '${error.description}'`, + { + node: node.name, + workflowId: workflow.id, + error, + }, + ); + throw error; + } + workflowData.lastTimeChecked = endDate; + + if (Array.isArray(responseData) && responseData.length) { + return [this.helpers.returnJsonArray(responseData as IDataObject[])]; + } + + return null; + } +} diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index bd38382123..4c0dac11ce 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -700,6 +700,7 @@ "dist/nodes/Rundeck/Rundeck.node.js", "dist/nodes/S3/S3.node.js", "dist/nodes/Salesforce/Salesforce.node.js", + "dist/nodes/Salesforce/SalesforceTrigger.node.js", "dist/nodes/Salesmate/Salesmate.node.js", "dist/nodes/Schedule/ScheduleTrigger.node.js", "dist/nodes/SeaTable/SeaTable.node.js",