From 4d4ab7943bdb0f7127178c36a33b14509f9412f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Sat, 20 Feb 2021 17:12:55 -0300 Subject: [PATCH] :sparkles: Add Emelia nodes (#1455) * Emelia node added * Minor improvements on Emelia nodes * Fix nodes and credentials listing * Fix multi-line imports * Apply cosmetic changes to node description * Apply cosmetic changes to node execute method * Fix linting details * Apply cosmetic changes to trigger node * Replace PNG with SVG icon * Bring generic functions in line with codebase * Refactor resources and add operations * Fix typo in GraphQL call function * Add campaign description * Add contact list description * :zap: Improvements * :zap: Minor improvements to Emelia Nodes Co-authored-by: Charles LECALIER Co-authored-by: ricardo Co-authored-by: Jan Oberhauser --- .../credentials/EmeliaApi.credentials.ts | 18 + .../nodes/Emelia/CampaignDescription.ts | 326 +++++++++++++++ .../nodes/Emelia/ContactListDescription.ts | 221 ++++++++++ .../nodes-base/nodes/Emelia/Emelia.node.ts | 387 ++++++++++++++++++ .../nodes/Emelia/EmeliaTrigger.node.ts | 180 ++++++++ .../nodes/Emelia/GenericFunctions.ts | 104 +++++ packages/nodes-base/nodes/Emelia/emelia.svg | 1 + packages/nodes-base/package.json | 3 + 8 files changed, 1240 insertions(+) create mode 100644 packages/nodes-base/credentials/EmeliaApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Emelia/CampaignDescription.ts create mode 100644 packages/nodes-base/nodes/Emelia/ContactListDescription.ts create mode 100644 packages/nodes-base/nodes/Emelia/Emelia.node.ts create mode 100644 packages/nodes-base/nodes/Emelia/EmeliaTrigger.node.ts create mode 100644 packages/nodes-base/nodes/Emelia/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Emelia/emelia.svg diff --git a/packages/nodes-base/credentials/EmeliaApi.credentials.ts b/packages/nodes-base/credentials/EmeliaApi.credentials.ts new file mode 100644 index 0000000000..c953bafd70 --- /dev/null +++ b/packages/nodes-base/credentials/EmeliaApi.credentials.ts @@ -0,0 +1,18 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class EmeliaApi implements ICredentialType { + name = 'emeliaApi'; + displayName = 'Emelia API'; + documentationUrl = 'emelia'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Emelia/CampaignDescription.ts b/packages/nodes-base/nodes/Emelia/CampaignDescription.ts new file mode 100644 index 0000000000..c7d6475976 --- /dev/null +++ b/packages/nodes-base/nodes/Emelia/CampaignDescription.ts @@ -0,0 +1,326 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const campaignOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Add Contact', + value: 'addContact', + }, + { + name: 'Create', + value: 'create', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Pause', + value: 'pause', + }, + { + name: 'Start', + value: 'start', + }, + ], + displayOptions: { + show: { + resource: [ + 'campaign', + ], + }, + }, + }, +] as INodeProperties[]; + +export const campaignFields = [ + // ---------------------------------- + // campaign: addContact + // ---------------------------------- + { + displayName: 'Campaign ID', + name: 'campaignId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + default: [], + required: true, + description: 'The ID of the campaign to add the contact to.', + displayOptions: { + show: { + resource: [ + 'campaign', + ], + operation: [ + 'addContact', + ], + }, + }, + }, + { + displayName: 'Contact Email', + name: 'contactEmail', + type: 'string', + required: true, + default: '', + description: 'The email of the contact to add to the campaign.', + displayOptions: { + show: { + resource: [ + 'campaign', + ], + operation: [ + 'addContact', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'campaign', + ], + operation: [ + 'addContact', + ], + }, + }, + options: [ + { + displayName: 'Custom Fields', + name: 'customFieldsUi', + placeholder: 'Add Custom Field', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + description: 'Filter by custom fields ', + default: {}, + options: [ + { + name: 'customFieldsValues', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field Name', + name: 'fieldName', + type: 'string', + default: '', + description: 'The name of the field to add custom field to.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'The value to set on custom field.', + }, + ], + }, + ], + }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: 'First name of the contact to add.', + }, + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + default: '', + description: 'Last name of the contact to add.', + }, + { + displayName: 'Last Contacted', + name: 'lastContacted', + type: 'string', + default: '', + description: 'Last contacted date of the contact to add.', + }, + { + displayName: 'Last Open', + name: 'lastOpen', + type: 'string', + default: '', + description: 'Last opened date of the contact to add.', + }, + { + displayName: 'Last Replied', + name: 'lastReplied', + type: 'string', + default: '', + description: 'Last replied date of the contact to add.', + }, + { + displayName: 'Mails Sent', + name: 'mailsSent', + type: 'number', + default: 0, + description: 'Number of emails sent to the contact to add.', + }, + { + displayName: 'Phone Number', + name: 'phoneNumber', + type: 'string', + default: '', + description: 'Phone number of the contact to add.', + }, + ], + }, + + // ---------------------------------- + // campaign: create + // ---------------------------------- + { + displayName: 'Campaign Name', + name: 'campaignName', + type: 'string', + required: true, + default: '', + description: 'The name of the campaign to create.', + displayOptions: { + show: { + resource: [ + 'campaign', + ], + operation: [ + 'create', + ], + }, + }, + }, + + // ---------------------------------- + // campaign: get + // ---------------------------------- + { + displayName: 'Campaign ID', + name: 'campaignId', + type: 'string', + default: '', + required: true, + description: 'The ID of the campaign to retrieve.', + displayOptions: { + show: { + resource: [ + 'campaign', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------- + // campaign: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'campaign', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 100, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 100, + }, + displayOptions: { + show: { + resource: [ + 'campaign', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + + // ---------------------------------- + // campaign: pause + // ---------------------------------- + { + displayName: 'Campaign ID', + name: 'campaignId', + type: 'string', + default: '', + required: true, + description: 'The ID of the campaign to pause.
The campaign must be in RUNNING mode.', + displayOptions: { + show: { + resource: [ + 'campaign', + ], + operation: [ + 'pause', + ], + }, + }, + }, + + // ---------------------------------- + // campaign: start + // ---------------------------------- + { + displayName: 'Campaign ID', + name: 'campaignId', + type: 'string', + default: '', + required: true, + description: 'The ID of the campaign to start.
Email provider and contacts must be set.', + displayOptions: { + show: { + resource: [ + 'campaign', + ], + operation: [ + 'start', + ], + }, + }, + }, + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Emelia/ContactListDescription.ts b/packages/nodes-base/nodes/Emelia/ContactListDescription.ts new file mode 100644 index 0000000000..5512f8734d --- /dev/null +++ b/packages/nodes-base/nodes/Emelia/ContactListDescription.ts @@ -0,0 +1,221 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const contactListOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Add', + value: 'add', + }, + { + name: 'Get All', + value: 'getAll', + }, + ], + displayOptions: { + show: { + resource: [ + 'contactList', + ], + }, + }, + }, +] as INodeProperties[]; + +export const contactListFields = [ + // ---------------------------------- + // contactList: add + // ---------------------------------- + { + displayName: 'Contact List ID', + name: 'contactListId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getContactLists', + }, + default: [], + required: true, + description: 'The ID of the contact list to add the contact to.', + displayOptions: { + show: { + resource: [ + 'contactList', + ], + operation: [ + 'add', + ], + }, + }, + }, + { + displayName: 'Contact Email', + name: 'contactEmail', + type: 'string', + required: true, + default: '', + description: 'The email of the contact to add to the contact list.', + displayOptions: { + show: { + resource: [ + 'contactList', + ], + operation: [ + 'add', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contactList', + ], + operation: [ + 'add', + ], + }, + }, + options: [ + { + displayName: 'Custom Fields', + name: 'customFieldsUi', + placeholder: 'Add Custom Field', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + description: 'Filter by custom fields ', + default: {}, + options: [ + { + name: 'customFieldsValues', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field Name', + name: 'fieldName', + type: 'string', + default: '', + description: 'The name of the field to add custom field to.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'The value to set on custom field.', + }, + ], + }, + ], + }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: 'First name of the contact to add.', + }, + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + default: '', + description: 'Last name of the contact to add.', + }, + { + displayName: 'Last Contacted', + name: 'lastContacted', + type: 'dateTime', + default: '', + description: 'Last contacted date of the contact to add.', + }, + { + displayName: 'Last Open', + name: 'lastOpen', + type: 'dateTime', + default: '', + description: 'Last opened date of the contact to add.', + }, + { + displayName: 'Last Replied', + name: 'lastReplied', + type: 'dateTime', + default: '', + description: 'Last replied date of the contact to add.', + }, + { + displayName: 'Mails Sent', + name: 'mailsSent', + type: 'number', + default: 0, + description: 'Number of emails sent to the contact to add.', + }, + { + displayName: 'Phone Number', + name: 'phoneNumber', + type: 'string', + default: '', + description: 'Phone number of the contact to add.', + }, + ], + }, + + // ---------------------------------- + // contactList: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'contactList', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 100, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 100, + }, + displayOptions: { + show: { + resource: [ + 'contactList', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Emelia/Emelia.node.ts b/packages/nodes-base/nodes/Emelia/Emelia.node.ts new file mode 100644 index 0000000000..55b6ebac70 --- /dev/null +++ b/packages/nodes-base/nodes/Emelia/Emelia.node.ts @@ -0,0 +1,387 @@ +import { + IExecuteFunctions +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodeType, + INodeTypeDescription +} from 'n8n-workflow'; + +import { + emeliaGraphqlRequest, + loadResource, +} from './GenericFunctions'; + +import { + campaignFields, + campaignOperations, +} from './CampaignDescription'; + +import { + contactListFields, + contactListOperations, +} from './ContactListDescription'; + +import { + isEmpty, +} from 'lodash'; + +export class Emelia implements INodeType { + description: INodeTypeDescription = { + displayName: 'Emelia', + name: 'emelia', + icon: 'file:emelia.svg', + group: ['input'], + version: 1, + subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}', + description: 'Consume the Emelia API', + defaults: { + name: 'Emelia', + color: '#e18063', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'emeliaApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Campaign', + value: 'campaign', + }, + { + name: 'Contact List', + value: 'contactList', + }, + ], + default: 'campaign', + required: true, + description: 'The resource to operate on.', + }, + ...campaignOperations, + ...campaignFields, + ...contactListOperations, + ...contactListFields, + ], + }; + + methods = { + loadOptions: { + async getCampaigns(this: ILoadOptionsFunctions) { + return loadResource.call(this, 'campaign'); + }, + + async getContactLists(this: ILoadOptionsFunctions) { + return loadResource.call(this, 'contactList'); + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + + const resource = this.getNodeParameter('resource', 0); + const operation = this.getNodeParameter('operation', 0); + + for (let i = 0; i < items.length; i++) { + + try { + + if (resource === 'campaign') { + + // ********************************** + // campaign + // ********************************** + + if (operation === 'addContact') { + + // ---------------------------------- + // campaign: addContact + // ---------------------------------- + + const contact = { + email: this.getNodeParameter('contactEmail', i) as string, + }; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (!isEmpty(additionalFields)) { + Object.assign(contact, additionalFields); + } + + if (additionalFields.customFieldsUi) { + const customFields = (additionalFields.customFieldsUi as IDataObject || {}).customFieldsValues as IDataObject[] || []; + const data = customFields.reduce((obj, value) => Object.assign(obj, { [`${value.fieldName}`]: value.value }), {}); + Object.assign(contact, data); + //@ts-ignore + delete contact.customFieldsUi; + } + + const responseData = await emeliaGraphqlRequest.call(this, { + query: ` + mutation AddContactToCampaignHook($id: ID!, $contact: JSON!) { + addContactToCampaignHook(id: $id, contact: $contact) + }`, + operationName: 'AddContactToCampaignHook', + variables: { + id: this.getNodeParameter('campaignId', i), + contact, + }, + }); + + returnData.push({ contactId: responseData.data.addContactToCampaignHook }); + + } else if (operation === 'create') { + + // ---------------------------------- + // campaign: create + // ---------------------------------- + + const responseData = await emeliaGraphqlRequest.call(this, { + operationName: 'createCampaign', + query: ` + mutation createCampaign($name: String!) { + createCampaign(name: $name) { + _id + name + status + createdAt + provider + startAt + estimatedEnd + } + }`, + variables: { + name: this.getNodeParameter('campaignName', i), + }, + }); + + returnData.push(responseData.data.createCampaign); + + } else if (operation === 'get') { + + // ---------------------------------- + // campaign: get + // ---------------------------------- + + const responseData = await emeliaGraphqlRequest.call(this, { + query: ` + query campaign($id: ID!){ + campaign(id: $id){ + _id + name + status + createdAt + schedule{ + dailyContact + dailyLimit + minInterval + maxInterval + trackLinks + trackOpens + timeZone + days + start + end + eventToStopMails + } + provider + startAt + recipients{ + total_count + } + estimatedEnd + } + }`, + operationName: 'campaign', + variables: { + id: this.getNodeParameter('campaignId', i), + }, + }); + + returnData.push(responseData.data.campaign); + + } else if (operation === 'getAll') { + + // ---------------------------------- + // campaign: getAll + // ---------------------------------- + + const responseData = await emeliaGraphqlRequest.call(this, { + query: ` + query all_campaigns { + all_campaigns { + _id + name + status + createdAt + stats { + mailsSent + uniqueOpensPercent + opens + linkClickedPercent + repliedPercent + bouncedPercent + unsubscribePercent + progressPercent + } + } + }`, + operationName: 'all_campaigns', + }); + + let campaigns = responseData.data.all_campaigns; + + const returnAll = this.getNodeParameter('returnAll', i); + + if (!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + campaigns = campaigns.slice(0, limit); + } + + returnData.push(...campaigns); + + } else if (operation === 'pause') { + + // ---------------------------------- + // campaign: pause + // ---------------------------------- + + await emeliaGraphqlRequest.call(this, { + query: ` + mutation pauseCampaign($id: ID!) { + pauseCampaign(id: $id) + }`, + operationName: 'pauseCampaign', + variables: { + id: this.getNodeParameter('campaignId', i), + }, + }); + + returnData.push({ success: true }); + + } else if (operation === 'start') { + + // ---------------------------------- + // campaign: start + // ---------------------------------- + + await emeliaGraphqlRequest.call(this, { + query: ` + mutation startCampaign($id: ID!) { + startCampaign(id: $id) + }`, + operationName: 'startCampaign', + variables: { + id: this.getNodeParameter('campaignId', i), + }, + }); + + returnData.push({ success: true }); + + } + + } else if (resource === 'contactList') { + + // ********************************** + // ContactList + // ********************************** + + if (operation === 'add') { + + // ---------------------------------- + // contactList: add + // ---------------------------------- + + const contact = { + email: this.getNodeParameter('contactEmail', i) as string, + }; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (!isEmpty(additionalFields)) { + Object.assign(contact, additionalFields); + } + + if (additionalFields.customFieldsUi) { + const customFields = (additionalFields.customFieldsUi as IDataObject || {}).customFieldsValues as IDataObject[] || []; + const data = customFields.reduce((obj, value) => Object.assign(obj, { [`${value.fieldName}`]: value.value }), {}); + Object.assign(contact, data); + //@ts-ignore + delete contact.customFieldsUi; + } + + const responseData = await emeliaGraphqlRequest.call(this, { + query: ` + mutation AddContactsToListHook($id: ID!, $contact: JSON!) { + addContactsToListHook(id: $id, contact: $contact) + }`, + operationName: 'AddContactsToListHook', + variables: { + id: this.getNodeParameter('contactListId', i), + contact, + }, + }); + + returnData.push({ contactId: responseData.data.addContactsToListHook }); + + } else if (operation === 'getAll') { + + // ---------------------------------- + // contactList: getAll + // ---------------------------------- + + const responseData = await emeliaGraphqlRequest.call(this, { + query: ` + query contact_lists{ + contact_lists{ + _id + name + contactCount + fields + usedInCampaign + } + }`, + operationName: 'contact_lists', + }); + + let contactLists = responseData.data.contact_lists; + + const returnAll = this.getNodeParameter('returnAll', i); + + if (!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + contactLists = contactLists.slice(0, limit); + } + + returnData.push(...contactLists); + } + + } + + } catch (error) { + + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + + throw error; + + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Emelia/EmeliaTrigger.node.ts b/packages/nodes-base/nodes/Emelia/EmeliaTrigger.node.ts new file mode 100644 index 0000000000..b2c8aa4c56 --- /dev/null +++ b/packages/nodes-base/nodes/Emelia/EmeliaTrigger.node.ts @@ -0,0 +1,180 @@ +import { + IHookFunctions, + ILoadOptionsFunctions, + INodePropertyOptions, + INodeType, + INodeTypeDescription, + IWebhookFunctions, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + emeliaApiRequest, + emeliaGraphqlRequest, +} from './GenericFunctions'; + +interface Campaign { + _id: string; + name: string; +} + +export class EmeliaTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Emelia Trigger', + name: 'emeliaTrigger', + icon: 'file:emelia.svg', + group: ['trigger'], + version: 1, + description: 'Handle Emelia campaign activity events via webhooks', + defaults: { + name: 'Emelia Trigger', + color: '#e18063', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'emeliaApi', + required: true, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Campaign', + name: 'campaignId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + required: true, + default: '', + }, + { + displayName: 'Events', + name: 'events', + type: 'multiOptions', + required: true, + default: [], + options: [ + { + name: 'Email Bounced', + value: 'bounced', + }, + { + name: 'Email Opened', + value: 'opened', + }, + { + name: 'Email Replied', + value: 'replied', + }, + { + name: 'Email Sent', + value: 'sent', + }, + { + name: 'Link Clicked', + value: 'clicked', + }, + { + name: 'Unsubscribed Contact', + value: 'unsubscribed', + }, + ], + }, + ], + }; + + methods = { + loadOptions: { + async getCampaigns(this: ILoadOptionsFunctions): Promise { + const responseData = await emeliaGraphqlRequest.call(this, { + query: ` + query GetCampaigns { + campaigns { + _id + name + } + }`, + operationName: 'GetCampaigns', + variables: '{}', + }); + + return responseData.data.campaigns.map( + (campaign: Campaign) => ({ + name: campaign.name, + value: campaign._id, + }), + ); + }, + }, + }; + + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default') as string; + const campaignId = this.getNodeParameter('campaignId') as string; + const { webhooks } = await emeliaApiRequest.call(this, 'GET', '/webhook'); + for (const webhook of webhooks) { + if (webhook.url === webhookUrl && webhook.campaignId === campaignId) { + return true; + } + } + + return false; + }, + async create(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default') as string; + const webhookData = this.getWorkflowStaticData('node'); + const events = this.getNodeParameter('events') as string[]; + + const campaignId = this.getNodeParameter('campaignId') as string; + const body = { + hookUrl: webhookUrl, + events: events.map(e => e.toUpperCase()), + campaignId, + }; + + const { webhookId } = await emeliaApiRequest.call(this, 'POST', '/webhook/webhook', body); + webhookData.webhookId = webhookId; + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const webhookUrl = this.getNodeWebhookUrl('default') as string; + const campaignId = this.getNodeParameter('campaignId') as string; + + try { + const body = { + hookUrl: webhookUrl, + campaignId, + }; + await emeliaApiRequest.call(this, 'DELETE', '/webhook/webhook', body); + } 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/Emelia/GenericFunctions.ts b/packages/nodes-base/nodes/Emelia/GenericFunctions.ts new file mode 100644 index 0000000000..e5577b9680 --- /dev/null +++ b/packages/nodes-base/nodes/Emelia/GenericFunctions.ts @@ -0,0 +1,104 @@ +import { + IExecuteFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IHookFunctions, + INodePropertyOptions, +} from 'n8n-workflow'; + +/** + * Make an authenticated GraphQL request to Emelia. + */ +export async function emeliaGraphqlRequest( + this: IExecuteFunctions | ILoadOptionsFunctions, + body: object = {}, +) { + const response = await emeliaApiRequest.call(this, 'POST', '/graphql', body); + + if (response.errors) { + throw new Error(`Emelia error message: ${response.errors[0].message}`); + } + + return response; +} + +/** + * Make an authenticated REST API request to Emelia, used for trigger node. + */ +export async function emeliaApiRequest( + this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions, + method: string, + endpoint: string, + body: object = {}, + qs: object = {}, +) { + const { apiKey } = this.getCredentials('emeliaApi') as { apiKey: string }; + + const options = { + headers: { + Authorization: apiKey, + }, + method, + body, + qs, + uri: `https://graphql.emelia.io${endpoint}`, + json: true, + }; + + try { + + return await this.helpers.request!.call(this, options); + + } catch (error) { + + if (error?.response?.body?.error) { + const { error: errorMessage } = error.response.body; + throw new Error( + `Emelia error response [${error.statusCode}]: ${errorMessage}`, + ); + } + + throw error; + } +} + +/** + * Load resources so that the user can select them easily. + */ +export async function loadResource( + this: ILoadOptionsFunctions, + resource: 'campaign' | 'contactList', +): Promise { + const mapping: { [key in 'campaign' | 'contactList']: { query: string, key: string } } = { + campaign: { + query: ` + query GetCampaigns { + campaigns { + _id + name + } + }`, + key: 'campaigns', + }, + contactList: { + query: ` + query GetContactLists { + contact_lists { + _id + name + } + }`, + key: 'contact_lists', + }, + }; + + const responseData = await emeliaGraphqlRequest.call(this, { query: mapping[resource].query }); + + return responseData.data[mapping[resource].key].map((campaign: { name: string, _id: string }) => ({ + name: campaign.name, + value: campaign._id, + })); + +} diff --git a/packages/nodes-base/nodes/Emelia/emelia.svg b/packages/nodes-base/nodes/Emelia/emelia.svg new file mode 100644 index 0000000000..2344b9b6a3 --- /dev/null +++ b/packages/nodes-base/nodes/Emelia/emelia.svg @@ -0,0 +1 @@ + diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index d1d13c629b..133910b561 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -70,6 +70,7 @@ "dist/credentials/DropboxApi.credentials.js", "dist/credentials/DropboxOAuth2Api.credentials.js", "dist/credentials/EgoiApi.credentials.js", + "dist/credentials/EmeliaApi.credentials.js", "dist/credentials/EventbriteApi.credentials.js", "dist/credentials/EventbriteOAuth2Api.credentials.js", "dist/credentials/FacebookGraphApi.credentials.js", @@ -317,6 +318,8 @@ "dist/nodes/Egoi/Egoi.node.js", "dist/nodes/EmailReadImap.node.js", "dist/nodes/EmailSend.node.js", + "dist/nodes/Emelia/Emelia.node.js", + "dist/nodes/Emelia/EmeliaTrigger.node.js", "dist/nodes/ErrorTrigger.node.js", "dist/nodes/Eventbrite/EventbriteTrigger.node.js", "dist/nodes/ExecuteCommand.node.js",