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
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:
@@ -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 |
Reference in New Issue
Block a user