diff --git a/packages/nodes-base/nodes/Salesforce/AccountDescription.ts b/packages/nodes-base/nodes/Salesforce/AccountDescription.ts index 8399018e2d..6b03793360 100644 --- a/packages/nodes-base/nodes/Salesforce/AccountDescription.ts +++ b/packages/nodes-base/nodes/Salesforce/AccountDescription.ts @@ -25,6 +25,11 @@ export const accountOperations = [ value: 'create', 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', value: 'get', @@ -61,6 +66,48 @@ export const accountFields = [ /* -------------------------------------------------------------------------- */ /* 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', name: 'name', @@ -74,6 +121,7 @@ export const accountFields = [ ], operation: [ 'create', + 'upsert', ], }, }, @@ -92,6 +140,7 @@ export const accountFields = [ ], operation: [ 'create', + 'upsert', ], }, }, diff --git a/packages/nodes-base/nodes/Salesforce/AccountInterface.ts b/packages/nodes-base/nodes/Salesforce/AccountInterface.ts index af0468d93d..eb5bde59fc 100644 --- a/packages/nodes-base/nodes/Salesforce/AccountInterface.ts +++ b/packages/nodes-base/nodes/Salesforce/AccountInterface.ts @@ -1,4 +1,6 @@ export interface IAccount { + // tslint:disable-next-line: no-any + [key: string]: any; Name?: string; Fax?: string; Type?: string; diff --git a/packages/nodes-base/nodes/Salesforce/AttachmentDescription.ts b/packages/nodes-base/nodes/Salesforce/AttachmentDescription.ts index 9ea5454ef1..fc2ed3509a 100644 --- a/packages/nodes-base/nodes/Salesforce/AttachmentDescription.ts +++ b/packages/nodes-base/nodes/Salesforce/AttachmentDescription.ts @@ -1,4 +1,6 @@ -import { INodeProperties } from 'n8n-workflow'; +import { + INodeProperties, +} from 'n8n-workflow'; export const attachmentOperations = [ { diff --git a/packages/nodes-base/nodes/Salesforce/ContactDescription.ts b/packages/nodes-base/nodes/Salesforce/ContactDescription.ts index 68a9db7462..3ec78b6e11 100644 --- a/packages/nodes-base/nodes/Salesforce/ContactDescription.ts +++ b/packages/nodes-base/nodes/Salesforce/ContactDescription.ts @@ -30,6 +30,11 @@ export const contactOperations = [ value: 'create', 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', value: 'delete', @@ -66,6 +71,48 @@ export const contactFields = [ /* -------------------------------------------------------------------------- */ /* 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', name: 'lastname', @@ -79,6 +126,7 @@ export const contactFields = [ ], operation: [ 'create', + 'upsert', ], }, }, @@ -97,6 +145,7 @@ export const contactFields = [ ], operation: [ 'create', + 'upsert', ], }, }, @@ -642,7 +691,7 @@ export const contactFields = [ }, /* -------------------------------------------------------------------------- */ - /* contact:get */ + /* contact:get */ /* -------------------------------------------------------------------------- */ { displayName: 'Contact ID', diff --git a/packages/nodes-base/nodes/Salesforce/ContactInterface.ts b/packages/nodes-base/nodes/Salesforce/ContactInterface.ts index 2a3a9784bc..bb02d3b9de 100644 --- a/packages/nodes-base/nodes/Salesforce/ContactInterface.ts +++ b/packages/nodes-base/nodes/Salesforce/ContactInterface.ts @@ -1,4 +1,6 @@ export interface IContact { + // tslint:disable-next-line: no-any + [key: string]: any; LastName?: string; Fax?: string; Email?: string; diff --git a/packages/nodes-base/nodes/Salesforce/CustomObjectDescription.ts b/packages/nodes-base/nodes/Salesforce/CustomObjectDescription.ts index 1e55cf54a3..35f6c7f32e 100644 --- a/packages/nodes-base/nodes/Salesforce/CustomObjectDescription.ts +++ b/packages/nodes-base/nodes/Salesforce/CustomObjectDescription.ts @@ -20,6 +20,11 @@ export const customObjectOperations = [ value: 'create', 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', value: 'get', @@ -67,11 +72,54 @@ export const customObjectFields = [ ], operation: [ 'create', + 'upsert', ], }, }, 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', name: 'customFieldsUi', @@ -87,6 +135,7 @@ export const customObjectFields = [ ], operation: [ 'create', + 'upsert', ], }, }, diff --git a/packages/nodes-base/nodes/Salesforce/LeadDescription.ts b/packages/nodes-base/nodes/Salesforce/LeadDescription.ts index 2bb553dfa1..cff54682c1 100644 --- a/packages/nodes-base/nodes/Salesforce/LeadDescription.ts +++ b/packages/nodes-base/nodes/Salesforce/LeadDescription.ts @@ -30,6 +30,11 @@ export const leadOperations = [ value: 'create', 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', value: 'delete', @@ -66,6 +71,48 @@ export const leadFields = [ /* -------------------------------------------------------------------------- */ /* 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', name: 'company', @@ -79,6 +126,7 @@ export const leadFields = [ ], operation: [ 'create', + 'upsert', ], }, }, @@ -97,6 +145,7 @@ export const leadFields = [ ], operation: [ 'create', + 'upsert', ], }, }, @@ -115,6 +164,7 @@ export const leadFields = [ ], operation: [ 'create', + 'upsert', ], }, }, diff --git a/packages/nodes-base/nodes/Salesforce/LeadInterface.ts b/packages/nodes-base/nodes/Salesforce/LeadInterface.ts index 02196961ed..2c84fc5852 100644 --- a/packages/nodes-base/nodes/Salesforce/LeadInterface.ts +++ b/packages/nodes-base/nodes/Salesforce/LeadInterface.ts @@ -1,4 +1,6 @@ export interface ILead { + // tslint:disable-next-line: no-any + [key: string]: any; Company?: string; LastName?: string; Email?: string; diff --git a/packages/nodes-base/nodes/Salesforce/OpportunityDescription.ts b/packages/nodes-base/nodes/Salesforce/OpportunityDescription.ts index 4a693954f6..95373a84b2 100644 --- a/packages/nodes-base/nodes/Salesforce/OpportunityDescription.ts +++ b/packages/nodes-base/nodes/Salesforce/OpportunityDescription.ts @@ -25,6 +25,11 @@ export const opportunityOperations = [ value: 'create', 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', value: 'delete', @@ -61,6 +66,48 @@ export const opportunityFields = [ /* -------------------------------------------------------------------------- */ /* 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', name: 'name', @@ -74,6 +121,7 @@ export const opportunityFields = [ ], operation: [ 'create', + 'upsert', ], }, }, @@ -92,6 +140,7 @@ export const opportunityFields = [ ], operation: [ 'create', + 'upsert', ], }, }, @@ -113,6 +162,7 @@ export const opportunityFields = [ ], operation: [ 'create', + 'upsert', ], }, }, @@ -131,6 +181,7 @@ export const opportunityFields = [ ], operation: [ 'create', + 'upsert', ], }, }, diff --git a/packages/nodes-base/nodes/Salesforce/OpportunityInterface.ts b/packages/nodes-base/nodes/Salesforce/OpportunityInterface.ts index 4155587820..011068fa62 100644 --- a/packages/nodes-base/nodes/Salesforce/OpportunityInterface.ts +++ b/packages/nodes-base/nodes/Salesforce/OpportunityInterface.ts @@ -1,4 +1,6 @@ export interface IOpportunity { + // tslint:disable-next-line: no-any + [key: string]: any; Name?: string; StageName?: string; CloseDate?: string; diff --git a/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts b/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts index dee9fffe5b..9afccd687f 100644 --- a/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts +++ b/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts @@ -390,7 +390,7 @@ export class Salesforce implements INodeType { }, // Get all the lead custom fields to display them to user so that he can // select them easily - async getCustomFields(this: ILoadOptionsFunctions): Promise < INodePropertyOptions[] > { + async getCustomFields(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; 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 @@ -409,6 +409,26 @@ export class Salesforce implements INodeType { sortOptions(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 { + 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 // select them easily async getAccounts(this: ILoadOptionsFunctions): Promise { @@ -932,7 +952,7 @@ export class Salesforce implements INodeType { for (let i = 0; i < items.length; i++) { if (resource === '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 lastname = this.getNodeParameter('lastname', i) as string; 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 if (operation === 'update') { @@ -1180,9 +1211,9 @@ export class Salesforce implements INodeType { } if (resource === 'contact') { //https://developer.salesforce.com/docs/api-explorer/sobject/Contact/post-contact - if (operation === 'create') { - const lastname = this.getNodeParameter('lastname', i) as string; + if (operation === 'create' || operation === 'upsert') { const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const lastname = this.getNodeParameter('lastname', i) as string; const body: IContact = { 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 if (operation === 'update') { @@ -1463,11 +1505,12 @@ export class Salesforce implements INodeType { if (options.isPrivate !== undefined) { body.IsPrivate = options.isPrivate as boolean; } + responseData = await salesforceApiRequest.call(this, 'POST', '/sobjects/note', body); } } if (resource === 'customObject') { - if (operation === 'create') { + if (operation === 'create' || operation === 'upsert') { const customObject = this.getNodeParameter('customObject', i) as string; const customFieldsUi = this.getNodeParameter('customFieldsUi', i) as 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') { const recordId = this.getNodeParameter('recordId', i) as string; @@ -1532,7 +1586,7 @@ export class Salesforce implements INodeType { } if (resource === '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 closeDate = this.getNodeParameter('closeDate', 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 if (operation === 'update') { @@ -1702,9 +1767,9 @@ export class Salesforce implements INodeType { } if (resource === 'account') { //https://developer.salesforce.com/docs/api-explorer/sobject/Account/post-account - if (operation === 'create') { - const name = this.getNodeParameter('name', i) as string; + if (operation === 'create' || operation === 'upsert') { const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const name = this.getNodeParameter('name', i) as string; const body: IAccount = { 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 if (operation === 'update') {