Add upsert operation to various resources (Salesforce) (#1743)

*  Add upsert operation to various resources (Salesforce)

The operation was added to the resources: Contact, Account, Lead, Opportunity and Custom Object.

*  Rename operation create/update to Create or Update

*  Improvements

Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
This commit is contained in:
Ricardo Espinoza 2021-06-09 23:25:20 -04:00 committed by GitHub
parent e298d2a1a8
commit 6ade0a00f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 349 additions and 15 deletions

View file

@ -25,6 +25,11 @@ export const accountOperations = [
value: 'create', value: 'create',
description: 'Create an account', description: 'Create an account',
}, },
{
name: 'Create or Update',
value: 'upsert',
description: 'Create a new account, or update the current one if it already exists (upsert)',
},
{ {
name: 'Get', name: 'Get',
value: 'get', value: 'get',
@ -61,6 +66,48 @@ export const accountFields = [
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* account:create */ /* account:create */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
{
displayName: 'Match Against',
name: 'externalId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getExternalIdFields',
loadOptionsDependsOn: [
'resource',
],
},
required: true,
default: '',
displayOptions: {
show: {
resource: [
'account',
],
operation: [
'upsert',
],
},
},
description: `The field to check to see if the account already exists`,
},
{
displayName: 'Value to Match',
name: 'externalIdValue',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: [
'account',
],
operation: [
'upsert',
],
},
},
description: `If this value exists in the 'match against' field, update the account. Otherwise create a new one`,
},
{ {
displayName: 'Name', displayName: 'Name',
name: 'name', name: 'name',
@ -74,6 +121,7 @@ export const accountFields = [
], ],
operation: [ operation: [
'create', 'create',
'upsert',
], ],
}, },
}, },
@ -92,6 +140,7 @@ export const accountFields = [
], ],
operation: [ operation: [
'create', 'create',
'upsert',
], ],
}, },
}, },

View file

@ -1,4 +1,6 @@
export interface IAccount { export interface IAccount {
// tslint:disable-next-line: no-any
[key: string]: any;
Name?: string; Name?: string;
Fax?: string; Fax?: string;
Type?: string; Type?: string;

View file

@ -1,4 +1,6 @@
import { INodeProperties } from 'n8n-workflow'; import {
INodeProperties,
} from 'n8n-workflow';
export const attachmentOperations = [ export const attachmentOperations = [
{ {

View file

@ -30,6 +30,11 @@ export const contactOperations = [
value: 'create', value: 'create',
description: 'Create a contact', description: 'Create a contact',
}, },
{
name: 'Create or Update',
value: 'upsert',
description: 'Create a new contact, or update the current one if it already exists (upsert)',
},
{ {
name: 'Delete', name: 'Delete',
value: 'delete', value: 'delete',
@ -66,6 +71,48 @@ export const contactFields = [
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* contact:create */ /* contact:create */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
{
displayName: 'Match Against',
name: 'externalId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getExternalIdFields',
loadOptionsDependsOn: [
'resource',
],
},
required: true,
default: '',
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'upsert',
],
},
},
description: `The field to check to see if the contact already exists`,
},
{
displayName: 'Value to Match',
name: 'externalIdValue',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'upsert',
],
},
},
description: `If this value exists in the 'match against' field, update the contact. Otherwise create a new one`,
},
{ {
displayName: 'Last Name', displayName: 'Last Name',
name: 'lastname', name: 'lastname',
@ -79,6 +126,7 @@ export const contactFields = [
], ],
operation: [ operation: [
'create', 'create',
'upsert',
], ],
}, },
}, },
@ -97,6 +145,7 @@ export const contactFields = [
], ],
operation: [ operation: [
'create', 'create',
'upsert',
], ],
}, },
}, },
@ -642,7 +691,7 @@ export const contactFields = [
}, },
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* contact:get */ /* contact:get */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
{ {
displayName: 'Contact ID', displayName: 'Contact ID',

View file

@ -1,4 +1,6 @@
export interface IContact { export interface IContact {
// tslint:disable-next-line: no-any
[key: string]: any;
LastName?: string; LastName?: string;
Fax?: string; Fax?: string;
Email?: string; Email?: string;

View file

@ -20,6 +20,11 @@ export const customObjectOperations = [
value: 'create', value: 'create',
description: 'Create a custom object record', description: 'Create a custom object record',
}, },
{
name: 'Create or Update',
value: 'upsert',
description: 'Create a new record, or update the current one if it already exists (upsert)',
},
{ {
name: 'Get', name: 'Get',
value: 'get', value: 'get',
@ -67,11 +72,54 @@ export const customObjectFields = [
], ],
operation: [ operation: [
'create', 'create',
'upsert',
], ],
}, },
}, },
description: 'Name of the custom object.', description: 'Name of the custom object.',
}, },
{
displayName: 'Match Against',
name: 'externalId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getExternalIdFields',
loadOptionsDependsOn: [
'customObject',
],
},
required: true,
default: '',
displayOptions: {
show: {
resource: [
'customObject',
],
operation: [
'upsert',
],
},
},
description: `The field to check to see if the object already exists`,
},
{
displayName: 'Value to Match',
name: 'externalIdValue',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: [
'customObject',
],
operation: [
'upsert',
],
},
},
description: `If this value exists in the 'match against' field, update the object. Otherwise create a new one`,
},
{ {
displayName: 'Fields', displayName: 'Fields',
name: 'customFieldsUi', name: 'customFieldsUi',
@ -87,6 +135,7 @@ export const customObjectFields = [
], ],
operation: [ operation: [
'create', 'create',
'upsert',
], ],
}, },
}, },

View file

@ -30,6 +30,11 @@ export const leadOperations = [
value: 'create', value: 'create',
description: 'Create a lead', description: 'Create a lead',
}, },
{
name: 'Create or Update',
value: 'upsert',
description: 'Create a new lead, or update the current one if it already exists (upsert)',
},
{ {
name: 'Delete', name: 'Delete',
value: 'delete', value: 'delete',
@ -66,6 +71,48 @@ export const leadFields = [
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* lead:create */ /* lead:create */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
{
displayName: 'Match Against',
name: 'externalId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getExternalIdFields',
loadOptionsDependsOn: [
'resource',
],
},
required: true,
default: '',
displayOptions: {
show: {
resource: [
'lead',
],
operation: [
'upsert',
],
},
},
description: `The field to check to see if the lead already exists`,
},
{
displayName: 'Value to Match',
name: 'externalIdValue',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: [
'lead',
],
operation: [
'upsert',
],
},
},
description: `If this value exists in the 'match against' field, update the lead. Otherwise create a new one`,
},
{ {
displayName: 'Company', displayName: 'Company',
name: 'company', name: 'company',
@ -79,6 +126,7 @@ export const leadFields = [
], ],
operation: [ operation: [
'create', 'create',
'upsert',
], ],
}, },
}, },
@ -97,6 +145,7 @@ export const leadFields = [
], ],
operation: [ operation: [
'create', 'create',
'upsert',
], ],
}, },
}, },
@ -115,6 +164,7 @@ export const leadFields = [
], ],
operation: [ operation: [
'create', 'create',
'upsert',
], ],
}, },
}, },

View file

@ -1,4 +1,6 @@
export interface ILead { export interface ILead {
// tslint:disable-next-line: no-any
[key: string]: any;
Company?: string; Company?: string;
LastName?: string; LastName?: string;
Email?: string; Email?: string;

View file

@ -25,6 +25,11 @@ export const opportunityOperations = [
value: 'create', value: 'create',
description: 'Create an opportunity', description: 'Create an opportunity',
}, },
{
name: 'Create or Update',
value: 'upsert',
description: 'Create a new opportunity, or update the current one if it already exists (upsert)',
},
{ {
name: 'Delete', name: 'Delete',
value: 'delete', value: 'delete',
@ -61,6 +66,48 @@ export const opportunityFields = [
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* opportunity:create */ /* opportunity:create */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
{
displayName: 'Match Against',
name: 'externalId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getExternalIdFields',
loadOptionsDependsOn: [
'resource',
],
},
required: true,
default: '',
displayOptions: {
show: {
resource: [
'opportunity',
],
operation: [
'upsert',
],
},
},
description: `The field to check to see if the opportunity already exists`,
},
{
displayName: 'Value to Match',
name: 'externalIdValue',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: [
'opportunity',
],
operation: [
'upsert',
],
},
},
description: `If this value exists in the 'match against' field, update the opportunity. Otherwise create a new one`,
},
{ {
displayName: 'Name', displayName: 'Name',
name: 'name', name: 'name',
@ -74,6 +121,7 @@ export const opportunityFields = [
], ],
operation: [ operation: [
'create', 'create',
'upsert',
], ],
}, },
}, },
@ -92,6 +140,7 @@ export const opportunityFields = [
], ],
operation: [ operation: [
'create', 'create',
'upsert',
], ],
}, },
}, },
@ -113,6 +162,7 @@ export const opportunityFields = [
], ],
operation: [ operation: [
'create', 'create',
'upsert',
], ],
}, },
}, },
@ -131,6 +181,7 @@ export const opportunityFields = [
], ],
operation: [ operation: [
'create', 'create',
'upsert',
], ],
}, },
}, },

View file

@ -1,4 +1,6 @@
export interface IOpportunity { export interface IOpportunity {
// tslint:disable-next-line: no-any
[key: string]: any;
Name?: string; Name?: string;
StageName?: string; StageName?: string;
CloseDate?: string; CloseDate?: string;

View file

@ -390,7 +390,7 @@ export class Salesforce implements INodeType {
}, },
// Get all the lead custom fields to display them to user so that he can // Get all the lead custom fields to display them to user so that he can
// select them easily // select them easily
async getCustomFields(this: ILoadOptionsFunctions): Promise < INodePropertyOptions[] > { async getCustomFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = []; const returnData: INodePropertyOptions[] = [];
const resource = this.getNodeParameter('resource', 0) as string; const resource = this.getNodeParameter('resource', 0) as string;
// TODO: find a way to filter this object to get just the lead sources instead of the whole object // TODO: find a way to filter this object to get just the lead sources instead of the whole object
@ -409,6 +409,26 @@ export class Salesforce implements INodeType {
sortOptions(returnData); sortOptions(returnData);
return returnData; return returnData;
}, },
// Get all the external id fields to display them to user so that he can
// select them easily
async getExternalIdFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
let resource = this.getCurrentNodeParameter('resource') as string;
resource = (resource === 'customObject') ? this.getCurrentNodeParameter('customObject') as string : resource;
const { fields } = await salesforceApiRequest.call(this, 'GET', `/sobjects/${resource}/describe`);
for (const field of fields) {
if (field.externalId === true || field.idLookup === true) {
const fieldName = field.label;
const fieldId = field.name;
returnData.push({
name: fieldName,
value: fieldId,
});
}
}
sortOptions(returnData);
return returnData;
},
// Get all the accounts to display them to user so that he can // Get all the accounts to display them to user so that he can
// select them easily // select them easily
async getAccounts(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> { async getAccounts(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
@ -932,7 +952,7 @@ export class Salesforce implements INodeType {
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
if (resource === 'lead') { if (resource === 'lead') {
//https://developer.salesforce.com/docs/api-explorer/sobject/Lead/post-lead //https://developer.salesforce.com/docs/api-explorer/sobject/Lead/post-lead
if (operation === 'create') { if (operation === 'create' || operation === 'upsert') {
const company = this.getNodeParameter('company', i) as string; const company = this.getNodeParameter('company', i) as string;
const lastname = this.getNodeParameter('lastname', i) as string; const lastname = this.getNodeParameter('lastname', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
@ -1015,7 +1035,18 @@ export class Salesforce implements INodeType {
} }
} }
} }
responseData = await salesforceApiRequest.call(this, 'POST', '/sobjects/lead', body); let endpoint = '/sobjects/lead';
let method = 'POST';
if (operation === 'upsert') {
method = 'PATCH';
const externalId = this.getNodeParameter('externalId', 0) as string;
const externalIdValue = this.getNodeParameter('externalIdValue', i) as string;
endpoint = `/sobjects/lead/${externalId}/${externalIdValue}`;
if (body[externalId] !== undefined) {
delete body[externalId];
}
}
responseData = await salesforceApiRequest.call(this, method, endpoint, body);
} }
//https://developer.salesforce.com/docs/api-explorer/sobject/Lead/patch-lead-id //https://developer.salesforce.com/docs/api-explorer/sobject/Lead/patch-lead-id
if (operation === 'update') { if (operation === 'update') {
@ -1180,9 +1211,9 @@ export class Salesforce implements INodeType {
} }
if (resource === 'contact') { if (resource === 'contact') {
//https://developer.salesforce.com/docs/api-explorer/sobject/Contact/post-contact //https://developer.salesforce.com/docs/api-explorer/sobject/Contact/post-contact
if (operation === 'create') { if (operation === 'create' || operation === 'upsert') {
const lastname = this.getNodeParameter('lastname', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const lastname = this.getNodeParameter('lastname', i) as string;
const body: IContact = { const body: IContact = {
LastName: lastname, LastName: lastname,
}; };
@ -1285,7 +1316,18 @@ export class Salesforce implements INodeType {
} }
} }
} }
responseData = await salesforceApiRequest.call(this, 'POST', '/sobjects/contact', body); let endpoint = '/sobjects/contact';
let method = 'POST';
if (operation === 'upsert') {
method = 'PATCH';
const externalId = this.getNodeParameter('externalId', 0) as string;
const externalIdValue = this.getNodeParameter('externalIdValue', i) as string;
endpoint = `/sobjects/contact/${externalId}/${externalIdValue}`;
if (body[externalId] !== undefined) {
delete body[externalId];
}
}
responseData = await salesforceApiRequest.call(this, method, endpoint, body);
} }
//https://developer.salesforce.com/docs/api-explorer/sobject/Contact/patch-contact-id //https://developer.salesforce.com/docs/api-explorer/sobject/Contact/patch-contact-id
if (operation === 'update') { if (operation === 'update') {
@ -1463,11 +1505,12 @@ export class Salesforce implements INodeType {
if (options.isPrivate !== undefined) { if (options.isPrivate !== undefined) {
body.IsPrivate = options.isPrivate as boolean; body.IsPrivate = options.isPrivate as boolean;
} }
responseData = await salesforceApiRequest.call(this, 'POST', '/sobjects/note', body); responseData = await salesforceApiRequest.call(this, 'POST', '/sobjects/note', body);
} }
} }
if (resource === 'customObject') { if (resource === 'customObject') {
if (operation === 'create') { if (operation === 'create' || operation === 'upsert') {
const customObject = this.getNodeParameter('customObject', i) as string; const customObject = this.getNodeParameter('customObject', i) as string;
const customFieldsUi = this.getNodeParameter('customFieldsUi', i) as IDataObject; const customFieldsUi = this.getNodeParameter('customFieldsUi', i) as IDataObject;
const body: IDataObject = {}; const body: IDataObject = {};
@ -1480,7 +1523,18 @@ export class Salesforce implements INodeType {
} }
} }
} }
responseData = await salesforceApiRequest.call(this, 'POST', `/sobjects/${customObject}`, body); let endpoint = `/sobjects/${customObject}`;
let method = 'POST';
if (operation === 'upsert') {
method = 'PATCH';
const externalId = this.getNodeParameter('externalId', 0) as string;
const externalIdValue = this.getNodeParameter('externalIdValue', i) as string;
endpoint = `/sobjects/${customObject}/${externalId}/${externalIdValue}`;
if (body[externalId] !== undefined) {
delete body[externalId];
}
}
responseData = await salesforceApiRequest.call(this, method, endpoint, body);
} }
if (operation === 'update') { if (operation === 'update') {
const recordId = this.getNodeParameter('recordId', i) as string; const recordId = this.getNodeParameter('recordId', i) as string;
@ -1532,7 +1586,7 @@ export class Salesforce implements INodeType {
} }
if (resource === 'opportunity') { if (resource === 'opportunity') {
//https://developer.salesforce.com/docs/api-explorer/sobject/Opportunity/post-opportunity //https://developer.salesforce.com/docs/api-explorer/sobject/Opportunity/post-opportunity
if (operation === 'create') { if (operation === 'create' || operation === 'upsert') {
const name = this.getNodeParameter('name', i) as string; const name = this.getNodeParameter('name', i) as string;
const closeDate = this.getNodeParameter('closeDate', i) as string; const closeDate = this.getNodeParameter('closeDate', i) as string;
const stageName = this.getNodeParameter('stageName', i) as string; const stageName = this.getNodeParameter('stageName', i) as string;
@ -1584,7 +1638,18 @@ export class Salesforce implements INodeType {
} }
} }
} }
responseData = await salesforceApiRequest.call(this, 'POST', '/sobjects/opportunity', body); let endpoint = '/sobjects/opportunity';
let method = 'POST';
if (operation === 'upsert') {
method = 'PATCH';
const externalId = this.getNodeParameter('externalId', 0) as string;
const externalIdValue = this.getNodeParameter('externalIdValue', i) as string;
endpoint = `/sobjects/opportunity/${externalId}/${externalIdValue}`;
if (body[externalId] !== undefined) {
delete body[externalId];
}
}
responseData = await salesforceApiRequest.call(this, method, endpoint, body);
} }
//https://developer.salesforce.com/docs/api-explorer/sobject/Opportunity/post-opportunity //https://developer.salesforce.com/docs/api-explorer/sobject/Opportunity/post-opportunity
if (operation === 'update') { if (operation === 'update') {
@ -1702,9 +1767,9 @@ export class Salesforce implements INodeType {
} }
if (resource === 'account') { if (resource === 'account') {
//https://developer.salesforce.com/docs/api-explorer/sobject/Account/post-account //https://developer.salesforce.com/docs/api-explorer/sobject/Account/post-account
if (operation === 'create') { if (operation === 'create' || operation === 'upsert') {
const name = this.getNodeParameter('name', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const name = this.getNodeParameter('name', i) as string;
const body: IAccount = { const body: IAccount = {
Name: name, Name: name,
}; };
@ -1789,7 +1854,18 @@ export class Salesforce implements INodeType {
} }
} }
} }
responseData = await salesforceApiRequest.call(this, 'POST', '/sobjects/account', body); let endpoint = '/sobjects/account';
let method = 'POST';
if (operation === 'upsert') {
method = 'PATCH';
const externalId = this.getNodeParameter('externalId', 0) as string;
const externalIdValue = this.getNodeParameter('externalIdValue', i) as string;
endpoint = `/sobjects/account/${externalId}/${externalIdValue}`;
if (body[externalId] !== undefined) {
delete body[externalId];
}
}
responseData = await salesforceApiRequest.call(this, method, endpoint, body);
} }
//https://developer.salesforce.com/docs/api-explorer/sobject/Account/patch-account-id //https://developer.salesforce.com/docs/api-explorer/sobject/Account/patch-account-id
if (operation === 'update') { if (operation === 'update') {