From 07123346795133c5830e5c22580a333b7a984fc9 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Thu, 11 Mar 2021 07:36:55 -0500 Subject: [PATCH] :sparkles: Add Autopilot Node & Trigger (#1516) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :sparkles: Autopilot Node & Trigger * :art: Replace PNG with SVG icon * :truck: Rename description file * :hammer: Fix contact operations default * :pencil2: Edit contact property descriptions * :pencil2: Edit journey property descriptions * :pencil2: Edit contact list property descriptions * :pencil2: Edit list property descriptions * :bug: Fix issue with a wrong named resource * :zap: Fix Trigger-Node name and minor improvements * :hammer: Remove 404 from contactList:exist Co-authored-by: Iván Ovejero Co-authored-by: Jan Oberhauser --- .../credentials/AutopilotApi.credentials.ts | 18 + .../nodes/Autopilot/Autopilot.node.ts | 378 ++++++++++++++++++ .../nodes/Autopilot/AutopilotTrigger.node.ts | 140 +++++++ .../nodes/Autopilot/ContactDescription.ts | 369 +++++++++++++++++ .../Autopilot/ContactJourneyDescription.ts | 73 ++++ .../nodes/Autopilot/ContactListDescription.ts | 138 +++++++ .../nodes/Autopilot/GenericFunctions.ts | 72 ++++ .../nodes/Autopilot/ListDescription.ts | 102 +++++ .../nodes-base/nodes/Autopilot/autopilot.svg | 8 + packages/nodes-base/package.json | 3 + 10 files changed, 1301 insertions(+) create mode 100644 packages/nodes-base/credentials/AutopilotApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Autopilot/Autopilot.node.ts create mode 100644 packages/nodes-base/nodes/Autopilot/AutopilotTrigger.node.ts create mode 100644 packages/nodes-base/nodes/Autopilot/ContactDescription.ts create mode 100644 packages/nodes-base/nodes/Autopilot/ContactJourneyDescription.ts create mode 100644 packages/nodes-base/nodes/Autopilot/ContactListDescription.ts create mode 100644 packages/nodes-base/nodes/Autopilot/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Autopilot/ListDescription.ts create mode 100644 packages/nodes-base/nodes/Autopilot/autopilot.svg diff --git a/packages/nodes-base/credentials/AutopilotApi.credentials.ts b/packages/nodes-base/credentials/AutopilotApi.credentials.ts new file mode 100644 index 0000000000..6c947f9474 --- /dev/null +++ b/packages/nodes-base/credentials/AutopilotApi.credentials.ts @@ -0,0 +1,18 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class AutopilotApi implements ICredentialType { + name = 'autopilotApi'; + displayName = 'Autopilot API'; + documentationUrl = 'autopilot'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Autopilot/Autopilot.node.ts b/packages/nodes-base/nodes/Autopilot/Autopilot.node.ts new file mode 100644 index 0000000000..1b4f312216 --- /dev/null +++ b/packages/nodes-base/nodes/Autopilot/Autopilot.node.ts @@ -0,0 +1,378 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + autopilotApiRequest, + autopilotApiRequestAllItems, +} from './GenericFunctions'; + +import { + contactFields, + contactOperations, +} from './ContactDescription'; + +import { + contactJourneyFields, + contactJourneyOperations, +} from './ContactJourneyDescription'; + +import { + contactListFields, + contactListOperations, +} from './ContactListDescription'; + +import { + listFields, + listOperations, +} from './ListDescription'; + +export class Autopilot implements INodeType { + description: INodeTypeDescription = { + displayName: 'Autopilot', + name: 'autopilot', + icon: 'file:autopilot.svg', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Autopilot API', + defaults: { + name: 'Autopilot', + color: '#6ad7b9', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'autopilotApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Contact', + value: 'contact', + }, + { + name: 'Contact Journey', + value: 'contactJourney', + }, + { + name: 'Contact List', + value: 'contactList', + }, + { + name: 'List', + value: 'list', + }, + ], + default: 'contact', + description: 'The resource to operate on.', + }, + + ...contactOperations, + ...contactFields, + ...contactJourneyOperations, + ...contactJourneyFields, + ...contactListOperations, + ...contactListFields, + ...listOperations, + ...listFields, + ], + }; + + methods = { + loadOptions: { + async getCustomFields( + this: ILoadOptionsFunctions, + ): Promise { + const returnData: INodePropertyOptions[] = []; + const customFields = await autopilotApiRequest.call( + this, + 'GET', + '/contacts/custom_fields', + ); + for (const customField of customFields) { + returnData.push({ + name: customField.name, + value: `${customField.name}-${customField.fieldType}`, + }); + } + return returnData; + }, + async getLists( + this: ILoadOptionsFunctions, + ): Promise { + const returnData: INodePropertyOptions[] = []; + const { lists } = await autopilotApiRequest.call( + this, + 'GET', + '/lists', + ); + for (const list of lists) { + returnData.push({ + name: list.title, + value: list.list_id, + }); + } + return returnData; + }, + async getTriggers( + this: ILoadOptionsFunctions, + ): Promise { + const returnData: INodePropertyOptions[] = []; + const { triggers } = await autopilotApiRequest.call( + this, + 'GET', + '/triggers', + ); + for (const trigger of triggers) { + returnData.push({ + name: trigger.journey, + value: trigger.trigger_id, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = (items.length as unknown) as number; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < length; i++) { + try { + if (resource === 'contact') { + if (operation === 'upsert') { + const email = this.getNodeParameter('email', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = { + Email: email, + }; + + Object.assign(body, additionalFields); + + if (body.customFieldsUi) { + const customFieldsValues = (body.customFieldsUi as IDataObject).customFieldsValues as IDataObject[]; + + body.custom = {}; + + for (const customField of customFieldsValues) { + const [name, fieldType] = (customField.key as string).split('-'); + + const fieldName = name.replace(/\s/g, '--'); + + //@ts-ignore + body.custom[`${fieldType}--${fieldName}`] = customField.value; + } + delete body.customFieldsUi; + } + + if (body.autopilotList) { + body._autopilot_list = body.autopilotList; + delete body.autopilotList; + } + + if (body.autopilotSessionId) { + body._autopilot_session_id = body.autopilotSessionId; + delete body.autopilotSessionId; + } + + if (body.newEmail) { + body._NewEmail = body.newEmail; + delete body.newEmail; + } + + responseData = await autopilotApiRequest.call( + this, + 'POST', + `/contact`, + { contact: body }, + ); + } + + if (operation === 'delete') { + const contactId = this.getNodeParameter('contactId', i) as string; + + responseData = await autopilotApiRequest.call( + this, + 'DELETE', + `/contact/${contactId}`, + ); + + responseData = { success: true }; + } + + if (operation === 'get') { + const contactId = this.getNodeParameter('contactId', i) as string; + + responseData = await autopilotApiRequest.call( + this, + 'GET', + `/contact/${contactId}`, + ); + } + + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + if (returnAll === false) { + qs.limit = this.getNodeParameter('limit', i) as number; + } + responseData = await autopilotApiRequestAllItems.call( + this, + 'contacts', + 'GET', + `/contacts`, + {}, + qs, + ); + + if (returnAll === false) { + responseData = responseData.splice(0, qs.limit); + } + } + } + if (resource === 'contactJourney') { + if (operation === 'add') { + + const triggerId = this.getNodeParameter('triggerId', i) as string; + + const contactId = this.getNodeParameter('contactId', i) as string; + + responseData = await autopilotApiRequest.call( + this, + 'POST', + `/trigger/${triggerId}/contact/${contactId}`, + ); + + responseData = { success: true }; + } + } + if (resource === 'contactList') { + if (['add', 'remove', 'exist'].includes(operation)) { + + const listId = this.getNodeParameter('listId', i) as string; + + const contactId = this.getNodeParameter('contactId', i) as string; + + const method: { [key: string]: string } = { + 'add': 'POST', + 'remove': 'DELETE', + 'exist': 'GET', + }; + + const endpoint = `/list/${listId}/contact/${contactId}`; + + if (operation === 'exist') { + try { + await autopilotApiRequest.call(this, method[operation], endpoint); + responseData = { exist: true }; + } catch (error) { + responseData = { exist: false }; + } + } else if (operation === 'add' || operation === 'remove') { + responseData = await autopilotApiRequest.call(this, method[operation], endpoint); + responseData['success'] = true; + } + } + + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + const listId = this.getNodeParameter('listId', i) as string; + + if (returnAll === false) { + qs.limit = this.getNodeParameter('limit', i) as number; + } + responseData = await autopilotApiRequestAllItems.call( + this, + 'contacts', + 'GET', + `/list/${listId}/contacts`, + {}, + qs, + ); + + if (returnAll === false) { + responseData = responseData.splice(0, qs.limit); + } + } + } + if (resource === 'list') { + if (operation === 'create') { + + const name = this.getNodeParameter('name', i) as string; + + const body: IDataObject = { + name, + }; + + responseData = await autopilotApiRequest.call( + this, + 'POST', + `/list`, + body, + ); + } + + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + if (returnAll === false) { + qs.limit = this.getNodeParameter('limit', i) as number; + } + responseData = await autopilotApiRequest.call( + this, + 'GET', + '/lists', + ); + + responseData = responseData.lists; + + if (returnAll === false) { + responseData = responseData.splice(0, qs.limit); + } + } + } + + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } + + } catch (error) { + + if (this.continueOnFail()) { + returnData.push({ error: error.toString() }); + continue; + } + + throw error; + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Autopilot/AutopilotTrigger.node.ts b/packages/nodes-base/nodes/Autopilot/AutopilotTrigger.node.ts new file mode 100644 index 0000000000..7bb9be4341 --- /dev/null +++ b/packages/nodes-base/nodes/Autopilot/AutopilotTrigger.node.ts @@ -0,0 +1,140 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeType, + INodeTypeDescription, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + autopilotApiRequest, +} from './GenericFunctions'; + +import { + snakeCase, +} from 'change-case'; + +export class AutopilotTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Autopilot Trigger', + name: 'autopilotTrigger', + icon: 'file:autopilot.svg', + group: ['trigger'], + version: 1, + subtitle: '={{$parameter["event"]}}', + description: 'Handle Autopilot events via webhooks', + defaults: { + name: 'Autopilot Trigger', + color: '#6ad7b9', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'autopilotApi', + required: true, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Event', + name: 'event', + type: 'options', + required: true, + default: '', + options: [ + { + name: 'Contact Added', + value: 'contactAdded', + }, + { + name: 'Contact Added To List', + value: 'contactAddedToList', + }, + { + name: 'Contact Entered Segment', + value: 'contactEnteredSegment', + }, + { + name: 'Contact Left Segment', + value: 'contactLeftSegment', + }, + { + name: 'Contact Removed From List', + value: 'contactRemovedFromList', + }, + { + name: 'Contact Unsubscribed', + value: 'contactUnsubscribed', + }, + { + name: 'Contact Updated', + value: 'contactUpdated', + }, + ], + }, + ], + }; + + // @ts-ignore + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const webhookUrl = this.getNodeWebhookUrl('default'); + const event = this.getNodeParameter('event') as string; + const { hooks: webhooks } = await autopilotApiRequest.call(this, 'GET', '/hooks'); + for (const webhook of webhooks) { + if (webhook.target_url === webhookUrl && webhook.event === snakeCase(event)) { + webhookData.webhookId = webhook.hook_id; + return true; + } + } + return false; + }, + async create(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + const event = this.getNodeParameter('event') as string; + const body: IDataObject = { + event: snakeCase(event), + target_url: webhookUrl, + }; + const webhook = await autopilotApiRequest.call(this, 'POST', '/hook', body); + webhookData.webhookId = webhook.hook_id; + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + try { + await autopilotApiRequest.call(this, 'DELETE', `/hook/${webhookData.webhookId}`); + } catch (error) { + return false; + } + delete webhookData.webhookId; + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const req = this.getRequestObject(); + return { + workflowData: [ + this.helpers.returnJsonArray(req.body), + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Autopilot/ContactDescription.ts b/packages/nodes-base/nodes/Autopilot/ContactDescription.ts new file mode 100644 index 0000000000..d00c47c7bd --- /dev/null +++ b/packages/nodes-base/nodes/Autopilot/ContactDescription.ts @@ -0,0 +1,369 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const contactOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'contact', + ], + }, + }, + options: [ + { + name: 'Create/Update', + value: 'upsert', + description: 'Create/Update a contact', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a contact', + }, + { + name: 'Get', + value: 'get', + description: 'Get a contact', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all contacts', + }, + ], + default: 'upsert', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const contactFields = [ + + /* -------------------------------------------------------------------------- */ + /* contact:upsert */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Email', + name: 'email', + required: true, + type: 'string', + displayOptions: { + show: { + operation: [ + 'upsert', + ], + resource: [ + 'contact', + ], + }, + }, + default: '', + description: 'Email address of the contact.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + displayOptions: { + show: { + operation: [ + 'upsert', + ], + resource: [ + 'contact', + ], + }, + }, + default: {}, + placeholder: 'Add Field', + options: [ + { + displayName: 'Company', + name: 'Company', + type: 'string', + default: '', + }, + { + displayName: 'Custom Fields', + name: 'customFieldsUi', + type: 'fixedCollection', + default: '', + placeholder: 'Add Custom Field', + typeOptions: { + multipleValues: true, + loadOptionsMethod: 'getCustomFields', + }, + options: [ + { + name: 'customFieldsValues', + displayName: 'Custom Field', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCustomFields', + }, + description: 'User-specified key of user-defined data.', + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + description: 'User-specified value of user-defined data.', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Fax', + name: 'Fax', + type: 'string', + default: '', + }, + { + displayName: 'First Name', + name: 'FirstName', + type: 'string', + default: '', + }, + { + displayName: 'Industry', + name: 'Industry', + type: 'string', + default: '', + }, + { + displayName: 'Last Name', + name: 'LastName', + type: 'string', + default: '', + }, + { + displayName: 'Lead Source', + name: 'LeadSource', + type: 'string', + default: '', + }, + { + displayName: 'LinkedIn URL', + name: 'LinkedIn', + type: 'string', + default: '', + }, + { + displayName: 'List ID', + name: 'autopilotList', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLists', + }, + default: '', + description: 'List to which this contact will be added on creation.', + }, + { + displayName: 'Mailing Country', + name: 'MailingCountry', + type: 'string', + default: '', + }, + { + displayName: 'Mailing Postal Code', + name: 'MailingPostalCode', + type: 'string', + default: '', + }, + { + displayName: 'Mailing State', + name: 'MailingState', + type: 'string', + default: '', + }, + { + displayName: 'Mailing Street', + name: 'MailingStreet', + type: 'string', + default: '', + }, + { + displayName: 'Mailing City', + name: 'MailingCity', + type: 'string', + default: '', + }, + { + displayName: 'Mobile Phone', + name: 'MobilePhone', + type: 'string', + default: '', + }, + { + displayName: 'New Email', + name: 'newEmail', + type: 'string', + default: '', + description: 'If provided, will change the email address of the contact identified by the Email field.', + }, + { + displayName: 'Notify', + name: 'notify', + type: 'boolean', + default: true, + description: `By default Autopilot notifies registered REST hook endpoints for contact_added/contact_updated events when
a new contact is added or an existing contact is updated via API. Disable to skip notifications.`, + }, + { + displayName: 'Number of Employees', + name: 'NumberOfEmployees', + type: 'number', + default: 0, + }, + { + displayName: 'Owner Name', + name: 'owner_name', + type: 'string', + default: '', + }, + { + displayName: 'Phone', + name: 'Phone', + type: 'string', + default: '', + }, + { + displayName: 'Salutation', + name: 'Salutation', + type: 'string', + default: '', + }, + { + displayName: 'Session ID', + name: 'autopilotSessionId', + type: 'string', + default: '', + description: 'Used to associate a contact with a session.', + }, + { + displayName: 'Status', + name: 'Status', + type: 'string', + default: '', + }, + { + displayName: 'Title', + name: 'Title', + type: 'string', + default: '', + }, + { + displayName: 'Subscribe', + name: 'unsubscribed', + type: 'boolean', + default: false, + description: 'Whether to subscribe or un-subscribe a contact.', + }, + { + displayName: 'Website URL', + name: 'Website', + type: 'string', + default: '', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* contact:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Contact ID', + name: 'contactId', + required: true, + type: 'string', + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'contact', + ], + }, + }, + default: '', + description: 'Can be ID or email.', + }, + + /* -------------------------------------------------------------------------- */ + /* contact:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Contact ID', + name: 'contactId', + required: true, + type: 'string', + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'contact', + ], + }, + }, + default: '', + description: 'Can be ID or email.', + }, + + /* -------------------------------------------------------------------------- */ + /* contact:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'contact', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'contact', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Autopilot/ContactJourneyDescription.ts b/packages/nodes-base/nodes/Autopilot/ContactJourneyDescription.ts new file mode 100644 index 0000000000..85c38a251f --- /dev/null +++ b/packages/nodes-base/nodes/Autopilot/ContactJourneyDescription.ts @@ -0,0 +1,73 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const contactJourneyOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'contactJourney', + ], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + description: 'Add contact to list', + }, + ], + default: 'add', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const contactJourneyFields = [ + + /* -------------------------------------------------------------------------- */ + /* contactJourney:add */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Trigger ID', + name: 'triggerId', + required: true, + typeOptions: { + loadOptionsMethod: 'getTriggers', + }, + type: 'options', + displayOptions: { + show: { + operation: [ + 'add', + ], + resource: [ + 'contactJourney', + ], + }, + }, + default: '', + description: 'List ID.', + }, + { + displayName: 'Contact ID', + name: 'contactId', + required: true, + type: 'string', + displayOptions: { + show: { + operation: [ + 'add', + ], + resource: [ + 'contactJourney', + ], + }, + }, + default: '', + description: 'Can be ID or email.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Autopilot/ContactListDescription.ts b/packages/nodes-base/nodes/Autopilot/ContactListDescription.ts new file mode 100644 index 0000000000..1e0a9d0ef2 --- /dev/null +++ b/packages/nodes-base/nodes/Autopilot/ContactListDescription.ts @@ -0,0 +1,138 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const contactListOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'contactList', + ], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + description: 'Add contact to list.', + }, + { + name: 'Exist', + value: 'exist', + description: 'Check if contact is on list.', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all contacts on list.', + }, + { + name: 'Remove', + value: 'remove', + description: 'Remove a contact from a list.', + }, + ], + default: 'add', + description: 'Operation to perform.', + }, +] as INodeProperties[]; + +export const contactListFields = [ + + /* -------------------------------------------------------------------------- */ + /* contactList:add */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'List ID', + name: 'listId', + required: true, + typeOptions: { + loadOptionsMethod: 'getLists', + }, + type: 'options', + displayOptions: { + show: { + operation: [ + 'add', + 'remove', + 'exist', + 'getAll', + ], + resource: [ + 'contactList', + ], + }, + }, + default: '', + description: 'ID of the list to operate on.', + }, + { + displayName: 'Contact ID', + name: 'contactId', + required: true, + type: 'string', + displayOptions: { + show: { + operation: [ + 'add', + 'remove', + 'exist', + ], + resource: [ + 'contactList', + ], + }, + }, + default: '', + description: 'Can be ID or email.', + }, + + /* -------------------------------------------------------------------------- */ + /* contactList:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'contactList', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'contactList', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Autopilot/GenericFunctions.ts b/packages/nodes-base/nodes/Autopilot/GenericFunctions.ts new file mode 100644 index 0000000000..b4477f09b1 --- /dev/null +++ b/packages/nodes-base/nodes/Autopilot/GenericFunctions.ts @@ -0,0 +1,72 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, + IHookFunctions, + IWebhookFunctions, +} from 'n8n-workflow'; + +export async function autopilotApiRequest(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const credentials = this.getCredentials('autopilotApi') as IDataObject; + + const apiKey = `${credentials.apiKey}`; + + const endpoint = 'https://api2.autopilothq.com/v1'; + + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + autopilotapikey: apiKey, + }, + method, + body, + qs: query, + uri: uri || `${endpoint}${resource}`, + json: true, + }; + if (!Object.keys(body).length) { + delete options.body; + } + if (!Object.keys(query).length) { + delete options.qs; + } + + try { + return await this.helpers.request!(options); + } catch (error) { + if (error.response) { + const errorMessage = error.response.body.message || error.response.body.description || error.message; + throw new Error(`Autopilot error response [${error.statusCode}]: ${errorMessage}`); + } + throw error; + } +} + +export async function autopilotApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + const returnAll = this.getNodeParameter('returnAll', 0, false) as boolean; + + const base = endpoint; + + let responseData; + do { + responseData = await autopilotApiRequest.call(this, method, endpoint, body, query); + endpoint = `${base}/${responseData.bookmark}`; + returnData.push.apply(returnData, responseData[propertyName]); + if (query.limit && returnData.length >= query.limit && returnAll === false) { + return returnData; + } + } while ( + responseData.bookmark !== undefined + ); + return returnData; +} diff --git a/packages/nodes-base/nodes/Autopilot/ListDescription.ts b/packages/nodes-base/nodes/Autopilot/ListDescription.ts new file mode 100644 index 0000000000..17fcf56e6d --- /dev/null +++ b/packages/nodes-base/nodes/Autopilot/ListDescription.ts @@ -0,0 +1,102 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const listOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'list', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a list.', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all lists.', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const listFields = [ + + /* -------------------------------------------------------------------------- */ + /* list:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Name', + name: 'name', + required: true, + type: 'string', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'list', + ], + }, + }, + default: '', + description: 'Name of the list to create.', + }, + + /* -------------------------------------------------------------------------- */ + /* list:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'list', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'list', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Autopilot/autopilot.svg b/packages/nodes-base/nodes/Autopilot/autopilot.svg new file mode 100644 index 0000000000..84edb88d2a --- /dev/null +++ b/packages/nodes-base/nodes/Autopilot/autopilot.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index eb8287839b..0a28bdc505 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -37,6 +37,7 @@ "dist/credentials/AsanaOAuth2Api.credentials.js", "dist/credentials/ApiTemplateIoApi.credentials.js", "dist/credentials/AutomizyApi.credentials.js", + "dist/credentials/AutopilotApi.credentials.js", "dist/credentials/Aws.credentials.js", "dist/credentials/AffinityApi.credentials.js", "dist/credentials/BannerbearApi.credentials.js", @@ -276,6 +277,8 @@ "dist/nodes/Affinity/Affinity.node.js", "dist/nodes/Affinity/AffinityTrigger.node.js", "dist/nodes/Automizy/Automizy.node.js", + "dist/nodes/Autopilot/Autopilot.node.js", + "dist/nodes/Autopilot/AutopilotTrigger.node.js", "dist/nodes/Aws/AwsLambda.node.js", "dist/nodes/Aws/Comprehend/AwsComprehend.node.js", "dist/nodes/Aws/Rekognition/AwsRekognition.node.js",