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",