first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,375 @@
import type { INodeProperties } from 'n8n-workflow';
export const campaignOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
default: 'get',
noDataExpression: true,
options: [
{
name: 'Add Contact',
value: 'addContact',
action: 'Add a contact to a campaign',
},
{
name: 'Create',
value: 'create',
action: 'Create a campaign',
},
{
name: 'Duplicate',
value: 'duplicate',
action: 'Duplicate a campaign',
},
{
name: 'Get',
value: 'get',
action: 'Get a campaign',
},
{
name: 'Get Many',
value: 'getAll',
action: 'Get many campaigns',
},
{
name: 'Pause',
value: 'pause',
action: 'Pause a campaign',
},
{
name: 'Start',
value: 'start',
action: 'Start a campaign',
},
],
displayOptions: {
show: {
resource: ['campaign'],
},
},
},
];
export const campaignFields: INodeProperties[] = [
// ----------------------------------
// campaign: addContact
// ----------------------------------
{
displayName: 'Campaign Name or ID',
name: 'campaignId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getCampaigns',
},
default: [],
required: true,
description:
'The ID of the campaign to add the contact to. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
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 Contacted',
name: 'lastContacted',
type: 'dateTime',
default: '',
description: 'Last contacted date of the contact to add',
},
{
displayName: 'Last Name',
name: 'lastName',
type: 'string',
default: '',
description: 'Last name 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',
},
],
},
// ----------------------------------
// 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: 'Whether to return all results or only up to a given limit',
displayOptions: {
show: {
resource: ['campaign'],
operation: ['getAll'],
},
},
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
default: 100,
description: 'Max 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'],
},
},
},
// ----------------------------------
// campaign: duplicate
// ----------------------------------
{
displayName: 'Campaign Name or ID',
name: 'campaignId',
type: 'options',
default: '',
required: true,
description:
'The ID of the campaign to duplicate. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
typeOptions: {
loadOptionsMethod: 'getCampaigns',
},
displayOptions: {
show: {
resource: ['campaign'],
operation: ['duplicate'],
},
},
},
{
displayName: 'New Campaign Name',
name: 'campaignName',
type: 'string',
required: true,
default: '',
description: 'The name of the new campaign to create',
displayOptions: {
show: {
resource: ['campaign'],
operation: ['duplicate'],
},
},
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
default: {},
placeholder: 'Add Field',
displayOptions: {
show: {
operation: ['duplicate'],
resource: ['campaign'],
},
},
options: [
{
displayName: 'Copy Contacts',
name: 'copyContacts',
type: 'boolean',
default: false,
description: 'Whether to copy all the contacts from the original campaign',
},
{
displayName: 'Copy Email Provider',
name: 'copyProvider',
type: 'boolean',
default: true,
description: 'Whether to set the same email provider than the original campaign',
},
{
displayName: 'Copy Email Sequence',
name: 'copyMails',
type: 'boolean',
default: true,
description:
'Whether to copy all the steps of the email sequence from the original campaign',
},
{
displayName: 'Copy Global Settings',
name: 'copySettings',
type: 'boolean',
default: true,
description: 'Whether to copy all the general settings from the original campaign',
},
],
},
];
@@ -0,0 +1,198 @@
import type { INodeProperties } from 'n8n-workflow';
export const contactListOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
default: 'getAll',
noDataExpression: true,
options: [
{
name: 'Add',
value: 'add',
action: 'Add a contact list',
},
{
name: 'Get Many',
value: 'getAll',
action: 'Get many contact lists',
},
],
displayOptions: {
show: {
resource: ['contactList'],
},
},
},
];
export const contactListFields: INodeProperties[] = [
// ----------------------------------
// contactList: add
// ----------------------------------
{
displayName: 'Contact List Name or ID',
name: 'contactListId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getContactLists',
},
default: [],
required: true,
description:
'The ID of the contact list to add the contact to. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
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 Contacted',
name: 'lastContacted',
type: 'dateTime',
default: '',
description: 'Last contacted date of the contact to add',
},
{
displayName: 'Last Name',
name: 'lastName',
type: 'string',
default: '',
description: 'Last name 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: 'Whether to return all results or only up to a given limit',
displayOptions: {
show: {
resource: ['contactList'],
operation: ['getAll'],
},
},
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
default: 100,
description: 'Max number of results to return',
typeOptions: {
minValue: 1,
maxValue: 100,
},
displayOptions: {
show: {
resource: ['contactList'],
operation: ['getAll'],
returnAll: [false],
},
},
},
];
@@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.emelia",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Communication", "Marketing"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/emelia/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.emelia/"
}
]
}
}
@@ -0,0 +1,445 @@
import isEmpty from 'lodash/isEmpty';
import {
type IExecuteFunctions,
type IDataObject,
type ILoadOptionsFunctions,
type INodeExecutionData,
type INodeType,
type INodeTypeDescription,
NodeConnectionTypes,
} from 'n8n-workflow';
import { campaignFields, campaignOperations } from './CampaignDescription';
import { contactListFields, contactListOperations } from './ContactListDescription';
import { emeliaApiTest, emeliaGraphqlRequest, loadResource } from './GenericFunctions';
export class Emelia implements INodeType {
description: INodeTypeDescription = {
displayName: 'Emelia',
name: 'emelia',
icon: 'file:emelia.svg',
group: ['input'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume the Emelia API',
defaults: {
name: 'Emelia',
},
usableAsTool: true,
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: [
{
name: 'emeliaApi',
required: true,
testedBy: 'emeliaApiTest',
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Campaign',
value: 'campaign',
},
{
name: 'Contact List',
value: 'contactList',
},
],
default: 'campaign',
required: true,
},
...campaignOperations,
...campaignFields,
...contactListOperations,
...contactListFields,
],
};
methods = {
credentialTest: {
emeliaApiTest,
},
loadOptions: {
async getCampaigns(this: ILoadOptionsFunctions) {
return await loadResource.call(this, 'campaign');
},
async getContactLists(this: ILoadOptionsFunctions) {
return await loadResource.call(this, 'contactList');
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
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);
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,
},
});
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({
contactId: responseData.data.addContactToCampaignHook,
}),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} 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),
},
});
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData.data.createCampaign as IDataObject),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} 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),
},
});
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData.data.campaign as IDataObject),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} 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);
campaigns = campaigns.slice(0, limit);
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(campaigns as IDataObject[]),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} 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),
},
});
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true }),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} 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),
},
});
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true }),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} else if (operation === 'duplicate') {
// ----------------------------------
// campaign: duplicate
// ----------------------------------
const options = this.getNodeParameter('options', i);
const variables = {
fromId: this.getNodeParameter('campaignId', i),
name: this.getNodeParameter('campaignName', i),
copySettings: true,
copyMails: true,
copyContacts: false,
copyProvider: true,
...options,
};
const {
data: { duplicateCampaign },
} = await emeliaGraphqlRequest.call(this, {
query: `
mutation duplicateCampaign(
$fromId: ID!
$name: String!
$copySettings: Boolean!
$copyMails: Boolean!
$copyContacts: Boolean!
$copyProvider: Boolean!
) {
duplicateCampaign(
fromId: $fromId
name: $name
copySettings: $copySettings
copyMails: $copyMails
copyContacts: $copyContacts
copyProvider: $copyProvider
)
}`,
operationName: 'duplicateCampaign',
variables,
});
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ _id: duplicateCampaign }),
{ itemData: { item: i } },
);
returnData.push(...executionData);
}
} 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);
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,
},
});
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ contactId: responseData.data.addContactsToListHook }),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} 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);
contactLists = contactLists.slice(0, limit);
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(contactLists as IDataObject[]),
{ itemData: { item: i } },
);
returnData.push(...executionData);
}
}
} catch (error) {
if (this.continueOnFail()) {
const executionErrorData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ error: error.message }),
{ itemData: { item: i } },
);
returnData.push(...executionErrorData);
continue;
}
throw error;
}
}
return [returnData];
}
}
@@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.emeliaTrigger",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Communication", "Marketing"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/emelia/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.emeliatrigger/"
}
]
}
}
@@ -0,0 +1,182 @@
import {
NodeConnectionTypes,
type IDataObject,
type IHookFunctions,
type ILoadOptionsFunctions,
type INodePropertyOptions,
type INodeType,
type INodeTypeDescription,
type IWebhookFunctions,
type IWebhookResponseData,
} from 'n8n-workflow';
import { emeliaApiRequest, emeliaApiTest, 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',
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
group: ['trigger'],
version: 1,
description: 'Handle Emelia campaign activity events via webhooks',
defaults: {
name: 'Emelia Trigger',
},
inputs: [],
outputs: [NodeConnectionTypes.Main],
credentials: [
{
name: 'emeliaApi',
required: true,
testedBy: 'emeliaApiTest',
},
],
webhooks: [
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
path: 'webhook',
},
],
properties: [
{
displayName: 'Campaign Name or ID',
name: 'campaignId',
type: 'options',
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
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 = {
credentialTest: {
emeliaApiTest,
},
loadOptions: {
async getCampaigns(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
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<boolean> {
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<boolean> {
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<boolean> {
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<IWebhookResponseData> {
const req = this.getRequestObject();
return {
workflowData: [this.helpers.returnJsonArray(req.body as IDataObject)],
};
}
}
@@ -0,0 +1,146 @@
import type {
ICredentialsDecrypted,
ICredentialTestFunctions,
IExecuteFunctions,
ILoadOptionsFunctions,
IHookFunctions,
INodeCredentialTestResult,
INodePropertyOptions,
JsonObject,
IRequestOptions,
IHttpRequestMethods,
IDataObject,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
/**
* Make an authenticated REST API request to Emelia, used for trigger node.
*/
export async function emeliaApiRequest(
this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions,
method: IHttpRequestMethods,
endpoint: string,
body: object = {},
qs: IDataObject = {},
) {
const { apiKey } = await this.getCredentials<{ apiKey: string }>('emeliaApi');
const options: IRequestOptions = {
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) {
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
/**
* 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 NodeApiError(this.getNode(), response as JsonObject);
}
return response;
}
/**
* Load resources so that the user can select them easily.
*/
export async function loadResource(
this: ILoadOptionsFunctions,
resource: 'campaign' | 'contactList',
): Promise<INodePropertyOptions[]> {
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,
}),
);
}
export async function emeliaApiTest(
this: ICredentialTestFunctions,
credential: ICredentialsDecrypted,
): Promise<INodeCredentialTestResult> {
const credentials = credential.data;
const body = {
query: `
query all_campaigns {
all_campaigns {
_id
name
status
createdAt
stats {
mailsSent
}
}
}`,
operationName: 'all_campaigns',
};
const options = {
headers: {
Authorization: credentials?.apiKey,
},
method: 'POST',
body,
uri: 'https://graphql.emelia.io/graphql',
json: true,
};
try {
await this.helpers.request(options);
} catch (error) {
return {
status: 'Error',
message: `Connection details not valid: ${(error as JsonObject).message}`,
};
}
return {
status: 'OK',
message: 'Authentication successful!',
};
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 2 45 45"><path fill="#f4b454" d="M29.714 6.539H9.078a2.11 2.11 0 0 0-2.007 2.762l5.484 16.863 4.219-1.817a3 3 0 0 1-.024-.34 2.641 2.641 0 1 1 1.023 2.081 2417 2417 0 0 1-4.415 2.545l5.156 15.852a.865.865 0 0 0 1.645 0l11.48-35.293a2.026 2.026 0 0 0-1.925-2.653" opacity=".9"/><path fill="#ef6d4a" d="M37.154 21.261 1.071 12.577a.865.865 0 0 0-.861 1.4l10.909 12.8 5.656-2.436a3 3 0 0 1-.024-.34 2.641 2.641 0 1 1 1.023 2.081 2178 2178 0 0 1-4.86 2.8l11.291 13.255a2.11 2.11 0 0 0 3.4-.264l10.8-17.586a2.026 2.026 0 0 0-1.251-3.026" opacity=".9"/><path fill="#20354c" d="M37.636 12.577 1.553 21.261A2.025 2.025 0 0 0 .301 24.29l3.472 5.656 13-5.6a3 3 0 0 1-.024-.34 2.641 2.641 0 1 1 1.023 2.081c-3.1 1.791-9.073 5.234-12.108 6.94l5.432 8.848a2.11 2.11 0 0 0 3.4.264l23.995-28.161a.865.865 0 0 0-.855-1.401" opacity=".9"/></svg>

After

Width:  |  Height:  |  Size: 880 B