Skip to content

Commit

Permalink
feat: Add Salesforce Trigger Node (#8920)
Browse files Browse the repository at this point in the history
Co-authored-by: Michael Kret <michael.k@radency.com>
  • Loading branch information
bramkn and michael-radency committed Apr 3, 2024
1 parent 3fd97e4 commit 571b613
Show file tree
Hide file tree
Showing 3 changed files with 296 additions and 4 deletions.
9 changes: 5 additions & 4 deletions packages/nodes-base/nodes/Salesforce/GenericFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
JsonObject,
IHttpRequestMethods,
IRequestOptions,
IPollFunctions,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';

Expand All @@ -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,

Expand All @@ -41,7 +42,7 @@ function getOptions(
}

async function getAccessToken(
this: IExecuteFunctions | ILoadOptionsFunctions,
this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
credentials: IDataObject,
): Promise<IDataObject> {
const now = moment().unix();
Expand Down Expand Up @@ -83,7 +84,7 @@ async function getAccessToken(
}

export async function salesforceApiRequest(
this: IExecuteFunctions | ILoadOptionsFunctions,
this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
method: IHttpRequestMethods,
endpoint: string,

Expand Down Expand Up @@ -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,
Expand Down
290 changes: 290 additions & 0 deletions packages/nodes-base/nodes/Salesforce/SalesforceTrigger.node.ts
Original file line number Diff line number Diff line change
@@ -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 <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
},
],
};

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<INodePropertyOptions[]> {
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<INodeExecutionData[][] | null> {
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;
}
}
1 change: 1 addition & 0 deletions packages/nodes-base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 571b613

Please sign in to comment.