Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Salesforce Trigger Node #8920

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Comment on lines +193 to +194
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick:
Why not to use something like 'Account_Created' and const [resource, change] = triggerOn.split('_')
that way we would not have magical values like -7 and avoid some unnecessary calculation to capitalize word


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
Loading