From d6b8e65abeb411f86538c1630dcce832ee0846a9 Mon Sep 17 00:00:00 2001 From: Jon <jonathan.bennetts@gmail.com> Date: Mon, 16 Dec 2024 13:44:52 +0000 Subject: [PATCH] feat(MailerLite Node): Update node to support new api (#11933) --- .../credentials/MailerLiteApi.credentials.ts | 40 +- .../nodes/MailerLite/GenericFunctions.ts | 70 ++- .../nodes/MailerLite/MailerLite.node.ts | 220 +-------- .../nodes/MailerLite/MailerLite.svg | 33 ++ .../MailerLite/MailerLiteTrigger.node.ts | 195 +------- .../nodes/MailerLite/mailerLite.png | Bin 893 -> 0 bytes .../MailerLite/tests/GenericFunctions.test.ts | 253 ++++++++++ .../nodes/MailerLite/tests/apiResponses.ts | 411 ++++++++++++++++ .../tests/v1/MailerLite.v1.workflow.json | 460 ++++++++++++++++++ .../tests/v1/MailerLite.v1.workflow.test.ts | 31 ++ .../tests/v2/MailerLite.v2.workflow.json | 370 ++++++++++++++ .../tests/v2/MailerLite.v2.workflow.test.ts | 34 ++ .../MailerLite/v1/MailerLiteTriggerV1.node.ts | 184 +++++++ .../nodes/MailerLite/v1/MailerLiteV1.node.ts | 199 ++++++++ .../{ => v1}/SubscriberDescription.ts | 0 .../MailerLite/v2/MailerLite.Interface.ts | 35 ++ .../MailerLite/v2/MailerLiteTriggerV2.node.ts | 179 +++++++ .../nodes/MailerLite/v2/MailerLiteV2.node.ts | 206 ++++++++ .../MailerLite/v2/SubscriberDescription.ts | 304 ++++++++++++ 19 files changed, 2830 insertions(+), 394 deletions(-) create mode 100644 packages/nodes-base/nodes/MailerLite/MailerLite.svg delete mode 100644 packages/nodes-base/nodes/MailerLite/mailerLite.png create mode 100644 packages/nodes-base/nodes/MailerLite/tests/GenericFunctions.test.ts create mode 100644 packages/nodes-base/nodes/MailerLite/tests/apiResponses.ts create mode 100644 packages/nodes-base/nodes/MailerLite/tests/v1/MailerLite.v1.workflow.json create mode 100644 packages/nodes-base/nodes/MailerLite/tests/v1/MailerLite.v1.workflow.test.ts create mode 100644 packages/nodes-base/nodes/MailerLite/tests/v2/MailerLite.v2.workflow.json create mode 100644 packages/nodes-base/nodes/MailerLite/tests/v2/MailerLite.v2.workflow.test.ts create mode 100644 packages/nodes-base/nodes/MailerLite/v1/MailerLiteTriggerV1.node.ts create mode 100644 packages/nodes-base/nodes/MailerLite/v1/MailerLiteV1.node.ts rename packages/nodes-base/nodes/MailerLite/{ => v1}/SubscriberDescription.ts (100%) create mode 100644 packages/nodes-base/nodes/MailerLite/v2/MailerLite.Interface.ts create mode 100644 packages/nodes-base/nodes/MailerLite/v2/MailerLiteTriggerV2.node.ts create mode 100644 packages/nodes-base/nodes/MailerLite/v2/MailerLiteV2.node.ts create mode 100644 packages/nodes-base/nodes/MailerLite/v2/SubscriberDescription.ts diff --git a/packages/nodes-base/credentials/MailerLiteApi.credentials.ts b/packages/nodes-base/credentials/MailerLiteApi.credentials.ts index 54a97b4cc7..fa5add1b6a 100644 --- a/packages/nodes-base/credentials/MailerLiteApi.credentials.ts +++ b/packages/nodes-base/credentials/MailerLiteApi.credentials.ts @@ -1,4 +1,10 @@ -import type { ICredentialType, INodeProperties } from 'n8n-workflow'; +import type { + ICredentialDataDecryptedObject, + ICredentialTestRequest, + ICredentialType, + IHttpRequestOptions, + INodeProperties, +} from 'n8n-workflow'; export class MailerLiteApi implements ICredentialType { name = 'mailerLiteApi'; @@ -15,5 +21,37 @@ export class MailerLiteApi implements ICredentialType { typeOptions: { password: true }, default: '', }, + { + displayName: 'Classic API', + name: 'classicApi', + type: 'boolean', + default: true, + description: + 'If the Classic API should be used, If this is your first time using this node this should be false.', + }, ]; + + async authenticate( + credentials: ICredentialDataDecryptedObject, + requestOptions: IHttpRequestOptions, + ): Promise<IHttpRequestOptions> { + if (credentials.classicApi === true) { + requestOptions.headers = { + 'X-MailerLite-ApiKey': credentials.apiKey as string, + }; + } else { + requestOptions.headers = { + Authorization: `Bearer ${credentials.apiKey as string}`, + }; + } + return requestOptions; + } + + test: ICredentialTestRequest = { + request: { + baseURL: + '={{$credentials.classicApi ? "https://api.mailerlite.com/api/v2" : "https://connect.mailerlite.com/api"}}', + url: '/groups', + }, + }; } diff --git a/packages/nodes-base/nodes/MailerLite/GenericFunctions.ts b/packages/nodes-base/nodes/MailerLite/GenericFunctions.ts index 2459f349d1..a7f9283d5d 100644 --- a/packages/nodes-base/nodes/MailerLite/GenericFunctions.ts +++ b/packages/nodes-base/nodes/MailerLite/GenericFunctions.ts @@ -3,39 +3,38 @@ import type { IExecuteFunctions, IHookFunctions, ILoadOptionsFunctions, + INodePropertyOptions, JsonObject, - IRequestOptions, + IHttpRequestOptions, IHttpRequestMethods, } from 'n8n-workflow'; import { NodeApiError } from 'n8n-workflow'; +import type { CustomField } from './v2/MailerLite.Interface'; + export async function mailerliteApiRequest( this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions, method: IHttpRequestMethods, path: string, - body: any = {}, qs: IDataObject = {}, _option = {}, ): Promise<any> { - const credentials = await this.getCredentials('mailerLiteApi'); - - const options: IRequestOptions = { - headers: { - 'X-MailerLite-ApiKey': credentials.apiKey, - }, + const options: IHttpRequestOptions = { method, body, qs, - uri: `https://api.mailerlite.com/api/v2${path}`, + url: + this.getNode().typeVersion === 1 + ? `https://api.mailerlite.com/api/v2${path}` + : `https://connect.mailerlite.com/api${path}`, json: true, }; try { if (Object.keys(body as IDataObject).length === 0) { delete options.body; } - //@ts-ignore - return await this.helpers.request.call(this, options); + return await this.helpers.httpRequestWithAuthentication.call(this, 'mailerLiteApi', options); } catch (error) { throw new NodeApiError(this.getNode(), error as JsonObject); } @@ -45,21 +44,56 @@ export async function mailerliteApiRequestAllItems( this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions, method: IHttpRequestMethods, endpoint: string, - body: any = {}, query: IDataObject = {}, ): Promise<any> { const returnData: IDataObject[] = []; - let responseData; query.limit = 1000; query.offset = 0; - do { - responseData = await mailerliteApiRequest.call(this, method, endpoint, body, query); - returnData.push.apply(returnData, responseData as IDataObject[]); - query.offset = query.offset + query.limit; - } while (responseData.length !== 0); + if (this.getNode().typeVersion === 1) { + do { + responseData = await mailerliteApiRequest.call(this, method, endpoint, body, query); + returnData.push(...(responseData as IDataObject[])); + query.offset += query.limit; + } while (responseData.length !== 0); + } else { + do { + responseData = await mailerliteApiRequest.call(this, method, endpoint, body, query); + returnData.push(...(responseData.data as IDataObject[])); + query.cursor = responseData.meta.next_cursor; + } while (responseData.links.next !== null); + } + + return returnData; +} + +export async function getCustomFields( + this: ILoadOptionsFunctions, +): Promise<INodePropertyOptions[]> { + const returnData: INodePropertyOptions[] = []; + const endpoint = '/fields'; + const fieldsResponse = await mailerliteApiRequest.call(this, 'GET', endpoint); + + if (this.getNode().typeVersion === 1) { + const fields = fieldsResponse as CustomField[]; + fields.forEach((field) => { + returnData.push({ + name: field.key, + value: field.key, + }); + }); + } else { + const fields = (fieldsResponse as IDataObject).data as CustomField[]; + fields.forEach((field) => { + returnData.push({ + name: field.name, + value: field.key, + }); + }); + } + return returnData; } diff --git a/packages/nodes-base/nodes/MailerLite/MailerLite.node.ts b/packages/nodes-base/nodes/MailerLite/MailerLite.node.ts index 6fe7c621b1..fbd9c3a125 100644 --- a/packages/nodes-base/nodes/MailerLite/MailerLite.node.ts +++ b/packages/nodes-base/nodes/MailerLite/MailerLite.node.ts @@ -1,206 +1,26 @@ -import type { - IExecuteFunctions, - IDataObject, - ILoadOptionsFunctions, - INodeExecutionData, - INodePropertyOptions, - INodeType, - INodeTypeDescription, -} from 'n8n-workflow'; -import { NodeConnectionType } from 'n8n-workflow'; +import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow'; +import { VersionedNodeType } from 'n8n-workflow'; -import { mailerliteApiRequest, mailerliteApiRequestAllItems } from './GenericFunctions'; +import { MailerLiteV1 } from './v1/MailerLiteV1.node'; +import { MailerLiteV2 } from './v2/MailerLiteV2.node'; -import { subscriberFields, subscriberOperations } from './SubscriberDescription'; +export class MailerLite extends VersionedNodeType { + constructor() { + const baseDescription: INodeTypeBaseDescription = { + displayName: 'MailerLite', + name: 'mailerLite', + icon: 'file:MailerLite.svg', + group: ['input'], + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume MailerLite API', + defaultVersion: 2, + }; -export class MailerLite implements INodeType { - description: INodeTypeDescription = { - displayName: 'MailerLite', - name: 'mailerLite', - // eslint-disable-next-line n8n-nodes-base/node-class-description-icon-not-svg - icon: 'file:mailerLite.png', - group: ['input'], - version: 1, - subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Mailer Lite API', - defaults: { - name: 'MailerLite', - }, - inputs: [NodeConnectionType.Main], - outputs: [NodeConnectionType.Main], - credentials: [ - { - name: 'mailerLiteApi', - required: true, - }, - ], - properties: [ - { - displayName: 'Resource', - name: 'resource', - type: 'options', - noDataExpression: true, - options: [ - { - name: 'Subscriber', - value: 'subscriber', - }, - ], - default: 'subscriber', - }, - ...subscriberOperations, - ...subscriberFields, - ], - }; + const nodeVersions: IVersionedNodeType['nodeVersions'] = { + 1: new MailerLiteV1(baseDescription), + 2: new MailerLiteV2(baseDescription), + }; - methods = { - loadOptions: { - // Get all the available custom fields to display them to user so that they can - // select them easily - async getCustomFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> { - const returnData: INodePropertyOptions[] = []; - const fields = await mailerliteApiRequest.call(this, 'GET', '/fields'); - for (const field of fields) { - returnData.push({ - name: field.key, - value: field.key, - }); - } - return returnData; - }, - }, - }; - - async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { - const items = this.getInputData(); - const returnData: INodeExecutionData[] = []; - const length = items.length; - const qs: IDataObject = {}; - let responseData; - const resource = this.getNodeParameter('resource', 0); - const operation = this.getNodeParameter('operation', 0); - for (let i = 0; i < length; i++) { - try { - if (resource === 'subscriber') { - //https://developers.mailerlite.com/reference#create-a-subscriber - if (operation === 'create') { - const email = this.getNodeParameter('email', i) as string; - - const additionalFields = this.getNodeParameter('additionalFields', i); - - const body: IDataObject = { - email, - fields: [], - }; - - Object.assign(body, additionalFields); - - if (additionalFields.customFieldsUi) { - const customFieldsValues = (additionalFields.customFieldsUi as IDataObject) - .customFieldsValues as IDataObject[]; - - if (customFieldsValues) { - const fields = {}; - - for (const customFieldValue of customFieldsValues) { - //@ts-ignore - fields[customFieldValue.fieldId] = customFieldValue.value; - } - - body.fields = fields; - delete body.customFieldsUi; - } - } - - responseData = await mailerliteApiRequest.call(this, 'POST', '/subscribers', body); - } - //https://developers.mailerlite.com/reference#single-subscriber - if (operation === 'get') { - const subscriberId = this.getNodeParameter('subscriberId', i) as string; - - responseData = await mailerliteApiRequest.call( - this, - 'GET', - `/subscribers/${subscriberId}`, - ); - } - //https://developers.mailerlite.com/reference#subscribers - if (operation === 'getAll') { - const returnAll = this.getNodeParameter('returnAll', i); - - const filters = this.getNodeParameter('filters', i); - - Object.assign(qs, filters); - - if (returnAll) { - responseData = await mailerliteApiRequestAllItems.call( - this, - 'GET', - '/subscribers', - {}, - qs, - ); - } else { - qs.limit = this.getNodeParameter('limit', i); - - responseData = await mailerliteApiRequest.call(this, 'GET', '/subscribers', {}, qs); - } - } - //https://developers.mailerlite.com/reference#update-subscriber - if (operation === 'update') { - const subscriberId = this.getNodeParameter('subscriberId', i) as string; - - const updateFields = this.getNodeParameter('updateFields', i); - - const body: IDataObject = {}; - - Object.assign(body, updateFields); - - if (updateFields.customFieldsUi) { - const customFieldsValues = (updateFields.customFieldsUi as IDataObject) - .customFieldsValues as IDataObject[]; - - if (customFieldsValues) { - const fields = {}; - - for (const customFieldValue of customFieldsValues) { - //@ts-ignore - fields[customFieldValue.fieldId] = customFieldValue.value; - } - - body.fields = fields; - delete body.customFieldsUi; - } - } - - responseData = await mailerliteApiRequest.call( - this, - 'PUT', - `/subscribers/${subscriberId}`, - body, - ); - } - } - } 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; - } - - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData as IDataObject[]), - { itemData: { item: i } }, - ); - - returnData.push(...executionData); - } - - return [returnData]; + super(nodeVersions, baseDescription); } } diff --git a/packages/nodes-base/nodes/MailerLite/MailerLite.svg b/packages/nodes-base/nodes/MailerLite/MailerLite.svg new file mode 100644 index 0000000000..c19029d033 --- /dev/null +++ b/packages/nodes-base/nodes/MailerLite/MailerLite.svg @@ -0,0 +1,33 @@ +<svg version="1.1" id="Layer_1" xmlns:x="ns_extend;" xmlns:i="ns_ai;" xmlns:graph="ns_graphs;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 62.8 50.2" style="enable-background:new 0 0 62.8 50.2;" xml:space="preserve"> + <style type="text/css"> + .st0{fill:#09C269;} + .st1{fill:#FFFFFF;} + </style> + <metadata> + <sfw xmlns="ns_sfw;"> + <slices> + </slices> + <sliceSourceBounds bottomLeftOrigin="true" height="50.2" width="62.8" x="236.9" y="-225.3"> + </sliceSourceBounds> + </sfw> + </metadata> + <g id="mailerlite-light"> + <g> + <g id="lite" transform="translate(137.000000, 0.000000)"> + <path id="Shape-path" class="st0" d="M-81.2,0h-48.9c-3.8,0-6.9,3.1-6.9,6.8v22.8v4.5v16.2l9.5-9.3h46.4c3.8,0,6.9-3.1,6.9-6.8 + V6.8C-74.3,3.1-77.4,0-81.2,0z"> + </path> + <path id="Shape-path-3" class="st1" d="M-90.2,15.8c5.2,0,7.6,4.1,7.6,8c0,1-0.8,1.8-1.8,1.8H-94c0.5,2.3,2.1,3.6,4.7,3.6 + c1.9,0,2.9-0.4,3.9-0.9c0.2-0.1,0.5-0.2,0.7-0.2c0.9,0,1.7,0.7,1.7,1.6c0,0.6-0.4,1.1-1,1.5c-1.3,0.7-2.7,1.4-5.5,1.4 + c-5.2,0-8.3-3.1-8.3-8.4C-97.9,18.1-93.7,15.8-90.2,15.8z M-105.5,13.2c0.6,0,1,0.5,1,1v1.9h2.9c0.9,0,1.7,0.7,1.7,1.6 + c0,0.9-0.7,1.6-1.7,1.6h-2.9V28c0,1.2,0.6,1.3,1.5,1.3c0.5,0,0.8-0.1,1.1-0.1c0.2,0,0.5-0.1,0.7-0.1c0.7,0,1.6,0.6,1.6,1.5 + c0,0.6-0.4,1.1-1,1.4c-0.9,0.4-1.7,0.6-2.7,0.6c-3.2,0-4.9-1.5-4.9-4.4v-8.8h-1.7c-0.6,0-1-0.5-1-1c0-0.3,0.1-0.6,0.4-0.9l4-4 + C-106.3,13.5-106,13.2-105.5,13.2z M-124.2,9.4c1,0,1.8,0.8,1.8,1.8v19.4c0,1-0.8,1.8-1.8,1.8s-1.8-0.8-1.8-1.8V11.2 + C-126,10.2-125.2,9.4-124.2,9.4z M-115.6,16c1,0,1.8,0.8,1.8,1.8v12.8c0,1-0.8,1.8-1.8,1.8c-1,0-1.8-0.8-1.8-1.8V17.8 + C-117.4,16.8-116.6,16-115.6,16z M-90.1,19.1c-1.7,0-3.6,1-3.9,3.5h7.9C-86.6,20.1-88.4,19.1-90.1,19.1z M-115.5,9.9 + c1.1,0,2,0.9,2,2V12c0,1.1-0.9,2-2,2h-0.2c-1.1,0-2-0.9-2-2v-0.1c0-1.1,0.9-2,2-2H-115.5z"> + </path> + </g> + </g> + </g> +</svg> \ No newline at end of file diff --git a/packages/nodes-base/nodes/MailerLite/MailerLiteTrigger.node.ts b/packages/nodes-base/nodes/MailerLite/MailerLiteTrigger.node.ts index 697720aa72..3fcb1d34ac 100644 --- a/packages/nodes-base/nodes/MailerLite/MailerLiteTrigger.node.ts +++ b/packages/nodes-base/nodes/MailerLite/MailerLiteTrigger.node.ts @@ -1,180 +1,25 @@ -import type { - IHookFunctions, - IWebhookFunctions, - IDataObject, - INodeType, - INodeTypeDescription, - IWebhookResponseData, -} from 'n8n-workflow'; -import { NodeConnectionType } from 'n8n-workflow'; +import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow'; +import { VersionedNodeType } from 'n8n-workflow'; -import { mailerliteApiRequest } from './GenericFunctions'; +import { MailerLiteTriggerV1 } from './v1/MailerLiteTriggerV1.node'; +import { MailerLiteTriggerV2 } from './v2/MailerLiteTriggerV2.node'; -export class MailerLiteTrigger implements INodeType { - description: INodeTypeDescription = { - displayName: 'MailerLite Trigger', - name: 'mailerLiteTrigger', - // eslint-disable-next-line n8n-nodes-base/node-class-description-icon-not-svg - icon: 'file:mailerLite.png', - group: ['trigger'], - version: 1, - description: 'Starts the workflow when MailerLite events occur', - defaults: { - name: 'MailerLite Trigger', - }, - inputs: [], - outputs: [NodeConnectionType.Main], - credentials: [ - { - name: 'mailerLiteApi', - required: true, - }, - ], - webhooks: [ - { - name: 'default', - httpMethod: 'POST', - responseMode: 'onReceived', - path: 'webhook', - }, - ], - properties: [ - { - displayName: 'Event', - name: 'event', - type: 'options', - options: [ - { - name: 'Campaign Sent', - value: 'campaign.sent', - description: 'Fired when campaign is sent', - }, - { - name: 'Subscriber Added Throught Webform', - value: 'subscriber.added_through_webform', - description: 'Fired when a subscriber is added though a form', - }, - { - name: 'Subscriber Added to Group', - value: 'subscriber.add_to_group', - description: 'Fired when a subscriber is added to a group', - }, - { - name: 'Subscriber Autonomation Completed', - value: 'subscriber.automation_complete', - description: 'Fired when subscriber finishes automation', - }, - { - name: 'Subscriber Autonomation Triggered', - value: 'subscriber.automation_triggered', - description: 'Fired when subscriber starts automation', - }, - { - name: 'Subscriber Bounced', - value: 'subscriber.bounced', - description: 'Fired when an email address bounces', - }, - { - name: 'Subscriber Complained', - value: 'subscriber.complaint', - description: 'Fired when subscriber marks a campaign as a spam', - }, - { - name: 'Subscriber Created', - value: 'subscriber.create', - description: 'Fired when a new subscriber is added to an account', - }, - { - name: 'Subscriber Removed From Group', - value: 'subscriber.remove_from_group', - description: 'Fired when a subscriber is removed from a group', - }, - { - name: 'Subscriber Unsubscribe', - value: 'subscriber.unsubscribe', - description: 'Fired when a subscriber becomes unsubscribed', - }, - { - name: 'Subscriber Updated', - value: 'subscriber.update', - description: "Fired when any of the subscriber's custom fields are updated", - }, - ], - required: true, - default: [], - description: 'The events to listen to', - }, - ], - }; - - webhookMethods = { - default: { - async checkExists(this: IHookFunctions): Promise<boolean> { - const webhookUrl = this.getNodeWebhookUrl('default'); - const webhookData = this.getWorkflowStaticData('node'); - const event = this.getNodeParameter('event') as string; - // Check all the webhooks which exist already if it is identical to the - // one that is supposed to get created. - const endpoint = '/webhooks'; - const { webhooks } = await mailerliteApiRequest.call(this, 'GET', endpoint, {}); - for (const webhook of webhooks) { - if (webhook.url === webhookUrl && webhook.event === event) { - // Set webhook-id to be sure that it can be deleted - webhookData.webhookId = webhook.id as string; - return true; - } - } - return false; - }, - async create(this: IHookFunctions): Promise<boolean> { - const webhookData = this.getWorkflowStaticData('node'); - const webhookUrl = this.getNodeWebhookUrl('default'); - const event = this.getNodeParameter('event') as string; - - const endpoint = '/webhooks'; - - const body = { - url: webhookUrl, - event, - }; - - const responseData = await mailerliteApiRequest.call(this, 'POST', endpoint, body); - - if (responseData.id === undefined) { - // Required data is missing so was not successful - return false; - } - - webhookData.webhookId = responseData.id as string; - return true; - }, - async delete(this: IHookFunctions): Promise<boolean> { - const webhookData = this.getWorkflowStaticData('node'); - if (webhookData.webhookId !== undefined) { - const endpoint = `/webhooks/${webhookData.webhookId}`; - - try { - await mailerliteApiRequest.call(this, 'DELETE', endpoint); - } catch (error) { - return false; - } - - // Remove from the static workflow data so that it is clear - // that no webhooks are registered anymore - delete webhookData.webhookId; - } - return true; - }, - }, - }; - - async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> { - const body = this.getBodyData(); - - const events = body.events as IDataObject[]; - - return { - workflowData: [this.helpers.returnJsonArray(events)], +export class MailerLiteTrigger extends VersionedNodeType { + constructor() { + const baseDescription: INodeTypeBaseDescription = { + displayName: 'MailerLite Trigger', + name: 'mailerLiteTrigger', + icon: 'file:MailerLite.svg', + group: ['trigger'], + description: 'Starts the workflow when MailerLite events occur', + defaultVersion: 2, }; + + const nodeVersions: IVersionedNodeType['nodeVersions'] = { + 1: new MailerLiteTriggerV1(baseDescription), + 2: new MailerLiteTriggerV2(baseDescription), + }; + + super(nodeVersions, baseDescription); } } diff --git a/packages/nodes-base/nodes/MailerLite/mailerLite.png b/packages/nodes-base/nodes/MailerLite/mailerLite.png deleted file mode 100644 index fde7f6d6b6018e6468e7422728792ce6dd2e8771..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 893 zcmV-@1A_dCP)<h;3K|Lk000e1NJLTq002Ay002A)0{{R34I(K(0002SP)t-s3Bqar z|NjBPX#M{F{QmtL!fXJ)THWyC0KHNGy;Uv5aCy*|jntzk!*2k*PMz7XEW>d6{rlwf z>C)`k0KQns>CynaOa;Pe;PT}FzhCwH_vZEMZ_bc0#Bu<@Ve9tqe9)OB!)+D9Y5>7y z0J%5t`SZ-`)gZ%d0J=Zf@8A!@YWMv3)$QD};lP&ItA^8`f6<#x$bJC4N&vSkW6Oy^ z#&<Wxblvgez~#%5*QklqqG!yESjmIM=g+y~#iQG_0JRYSv;gRSuv-8C0&+=2K~zY` z?Um_rqCgOUZDxc4m1F=>ZV&};<C$}_yYK%}+r%W8XmO-!|M^MvOm*?mJ;>BFu3V`> zPpCxik~>YKW(A^jdZr75roqGtFnQ1vzO_6Mj8!64AZd$n^K0$t)Ovu5B(X5~(saZY zKxj4Kfknb;)A8O03Y8$hU;LBCg^s+!<QtTf*%ThtTj4}z?RS|RFH-Of0ynC1NhV-( zAjfHee6cbC#|viwqvda&B`B#OQ}{LZHGsvRZ5Js1pqAWKBL_h0tOFnvw8^nJ&Jj{7 zn@tS|PBYgrT5mUhv_PxLa|uJ=Gu^q4EfARLudn(HNU4l%-;bFC6HD~`&DOI4^tHO( zZq_=G^2(r#mIMoLA1k}{lEa*_&Qse{4SWR^WW(wM<*RYawlVvxzVrc{o#_%F6=i4- z@3LIqm%D(>4FR*&WDZZmkP;4EHgNC4@UG>l`0v-KOmKKmmCbNG9FApmUPlfoa7bi- zG0~Akg5ouAzO5N?1mh<EHj8AYI4E0jCO?_aK@b3}%=l4qw4`-|O_y$r#AFh1SZsyE zl=UIlZ&afhn}1WgaLrom+5o{}o!!PCeM5Ff*%sYf%s8~QqP8)NY#_&&Wdcddwg~F? zllu{1szAn=UwMj>d|MUBST>J1A|I2_9E3)6xia~znN7)ihU5?6p!rjj&SU}=n7Lxw zJC|v>PWWVJM7^`h=<=l-wu*U?MO^TijBYp<<r2g+MfbbRfw$Cya;p^6X`p*1<BMR5 zP)e?z9(49IQ+kd12)E{v@0cevej=x0%T?>T`RVRk9dv(cXJ`G7?Ox-`mH)s$kPSrn Tqz<W000000NkvXXu0mjf#x=q% diff --git a/packages/nodes-base/nodes/MailerLite/tests/GenericFunctions.test.ts b/packages/nodes-base/nodes/MailerLite/tests/GenericFunctions.test.ts new file mode 100644 index 0000000000..f23963b084 --- /dev/null +++ b/packages/nodes-base/nodes/MailerLite/tests/GenericFunctions.test.ts @@ -0,0 +1,253 @@ +/* eslint-disable n8n-nodes-base/node-param-display-name-miscased */ +import { + type IExecuteFunctions, + type ILoadOptionsFunctions, + type IHookFunctions, + NodeApiError, +} from 'n8n-workflow'; + +import { + getCustomFields, + mailerliteApiRequest, + mailerliteApiRequestAllItems, +} from '../GenericFunctions'; + +describe('MailerLite -> mailerliteApiRequest', () => { + let mockExecuteFunctions: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions; + + const setupMockFunctions = (typeVersion: number) => { + mockExecuteFunctions = { + getNode: jest.fn().mockReturnValue({ typeVersion }), + helpers: { + httpRequestWithAuthentication: jest.fn(), + }, + } as unknown as IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions; + jest.clearAllMocks(); + }; + + beforeEach(() => { + setupMockFunctions(1); + }); + + it('should make a successful API request for type version 1', async () => { + const method = 'GET'; + const path = '/test'; + const body = {}; + const qs = {}; + + const responseData = { success: true }; + + (mockExecuteFunctions.helpers.httpRequestWithAuthentication as jest.Mock).mockResolvedValue( + responseData, + ); + + const result = await mailerliteApiRequest.call(mockExecuteFunctions, method, path, body, qs); + + expect(result).toEqual(responseData); + expect(mockExecuteFunctions.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith( + 'mailerLiteApi', + { + method, + qs, + url: 'https://api.mailerlite.com/api/v2/test', + json: true, + }, + ); + }); + + it('should make a successful API request for type version 2', async () => { + setupMockFunctions(2); + + const method = 'GET'; + const path = '/test'; + const body = {}; + const qs = {}; + + const responseData = { success: true }; + + (mockExecuteFunctions.helpers.httpRequestWithAuthentication as jest.Mock).mockResolvedValue( + responseData, + ); + + const result = await mailerliteApiRequest.call(mockExecuteFunctions, method, path, body, qs); + + expect(result).toEqual(responseData); + expect(mockExecuteFunctions.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith( + 'mailerLiteApi', + { + method, + qs, + url: 'https://connect.mailerlite.com/api/test', + json: true, + }, + ); + }); + + it('should make an API request with an empty body', async () => { + const method = 'GET'; + const path = '/test'; + const body = {}; + const qs = {}; + + const responseData = { success: true }; + + (mockExecuteFunctions.helpers.httpRequestWithAuthentication as jest.Mock).mockResolvedValue( + responseData, + ); + + const result = await mailerliteApiRequest.call(mockExecuteFunctions, method, path, body, qs); + + expect(result).toEqual(responseData); + expect(mockExecuteFunctions.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith( + 'mailerLiteApi', + { + method, + qs, + url: 'https://api.mailerlite.com/api/v2/test', + json: true, + }, + ); + }); + + it('should throw an error if the API request fails', async () => { + const method = 'GET'; + const path = '/test'; + const body = {}; + const qs = {}; + + const errorResponse = { message: 'Error' }; + + (mockExecuteFunctions.helpers.httpRequestWithAuthentication as jest.Mock).mockRejectedValue( + errorResponse, + ); + + await expect( + mailerliteApiRequest.call(mockExecuteFunctions, method, path, body, qs), + ).rejects.toThrow(NodeApiError); + }); +}); + +describe('MailerLite -> mailerliteApiRequestAllItems', () => { + let mockExecuteFunctions: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions; + + const setupMockFunctions = (typeVersion: number) => { + mockExecuteFunctions = { + getNode: jest.fn().mockReturnValue({ typeVersion }), + helpers: { + httpRequestWithAuthentication: jest.fn(), + }, + } as unknown as IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions; + jest.clearAllMocks(); + }; + + beforeEach(() => { + setupMockFunctions(1); + }); + + it('should handle pagination for type version 1', async () => { + const method = 'GET'; + const endpoint = '/test'; + const body = {}; + const query = {}; + + const responseDataPage1 = [{ id: 1 }, { id: 2 }]; + const responseDataPage2 = [{ id: 3 }, { id: 4 }]; + + (mockExecuteFunctions.helpers.httpRequestWithAuthentication as jest.Mock) + .mockResolvedValueOnce(responseDataPage1) + .mockResolvedValueOnce(responseDataPage2) + .mockResolvedValueOnce([]); + + const result = await mailerliteApiRequestAllItems.call( + mockExecuteFunctions, + method, + endpoint, + body, + query, + ); + + expect(result).toEqual([...responseDataPage1, ...responseDataPage2]); + }); + + it('should handle pagination for type version 2', async () => { + setupMockFunctions(2); + + const method = 'GET'; + const endpoint = '/test'; + const body = {}; + const query = {}; + + const responseDataPage1 = { + data: [{ id: 1 }, { id: 2 }], + meta: { next_cursor: 'cursor1' }, + links: { next: 'nextLink1' }, + }; + const responseDataPage2 = { + data: [{ id: 3 }, { id: 4 }], + meta: { next_cursor: null }, + links: { next: null }, + }; + + (mockExecuteFunctions.helpers.httpRequestWithAuthentication as jest.Mock) + .mockResolvedValueOnce(responseDataPage1) + .mockResolvedValueOnce(responseDataPage2); + + const result = await mailerliteApiRequestAllItems.call( + mockExecuteFunctions, + method, + endpoint, + body, + query, + ); + + expect(result).toEqual([...responseDataPage1.data, ...responseDataPage2.data]); + }); +}); + +describe('MailerLite -> getCustomFields', () => { + let mockExecuteFunctions: ILoadOptionsFunctions; + + const v1FieldResponse = [ + { name: 'Field1', key: 'field1' }, + { name: 'Field2', key: 'field2' }, + ]; + + const v2FieldResponse = { + data: v1FieldResponse, + }; + + const setupMockFunctions = (typeVersion: number) => { + mockExecuteFunctions = { + getNode: jest.fn().mockReturnValue({ typeVersion }), + helpers: { + httpRequestWithAuthentication: jest + .fn() + .mockResolvedValue(typeVersion === 1 ? v1FieldResponse : v2FieldResponse), + }, + } as unknown as ILoadOptionsFunctions; + jest.clearAllMocks(); + }; + + beforeEach(() => { + setupMockFunctions(1); + }); + + it('should return custom fields for type version 1', async () => { + const result = await getCustomFields.call(mockExecuteFunctions); + + expect(result).toEqual([ + { name: 'field1', value: 'field1' }, + { name: 'field2', value: 'field2' }, + ]); + }); + + it('should return custom fields for type version 2', async () => { + setupMockFunctions(2); + const result = await getCustomFields.call(mockExecuteFunctions); + + expect(result).toEqual([ + { name: 'Field1', value: 'field1' }, + { name: 'Field2', value: 'field2' }, + ]); + }); +}); diff --git a/packages/nodes-base/nodes/MailerLite/tests/apiResponses.ts b/packages/nodes-base/nodes/MailerLite/tests/apiResponses.ts new file mode 100644 index 0000000000..e1216e1ef5 --- /dev/null +++ b/packages/nodes-base/nodes/MailerLite/tests/apiResponses.ts @@ -0,0 +1,411 @@ +export const getUpdateSubscriberResponseClassic = { + id: 1343965485, + email: 'demo@mailerlite.com', + sent: 0, + opened: 0, + clicked: 0, + type: 'unsubscribed', + fields: [ + { + key: 'email', + value: 'demo@mailerlite.com', + type: 'TEXT', + }, + { + key: 'name', + value: 'Demo', + type: 'TEXT', + }, + { + key: 'last_name', + value: '', + type: 'TEXT', + }, + { + key: 'company', + value: '', + type: 'TEXT', + }, + { + key: 'country', + value: '', + type: 'TEXT', + }, + { + key: 'city', + value: '', + type: 'TEXT', + }, + { + key: 'phone', + value: '', + type: 'TEXT', + }, + { + key: 'state', + value: '', + type: 'TEXT', + }, + { + key: 'zip', + value: '', + type: 'TEXT', + }, + ], + date_subscribe: null, + date_unsubscribe: '2016-04-04 12:07:26', + date_created: '2016-04-04', + date_updated: null, +}; +export const getSubscriberResponseClassic = { + id: 1343965485, + name: 'John', + email: 'demo@mailerlite.com', + sent: 0, + opened: 0, + clicked: 0, + type: 'active', + signup_ip: '127.0.0.1', + signup_timestamp: '2018-01-01 01:01:01', + confirmation_ip: '127.0.0.2', + confirmation_timestamp: '2018-01-01 01:01:02', + fields: [ + { + key: 'email', + value: 'demo@mailerlite.com', + type: 'TEXT', + }, + { + key: 'name', + value: 'John', + type: 'TEXT', + }, + { + key: 'last_name', + value: '', + type: 'TEXT', + }, + { + key: 'company', + value: '', + type: 'TEXT', + }, + { + key: 'country', + value: '', + type: 'TEXT', + }, + { + key: 'city', + value: '', + type: 'TEXT', + }, + { + key: 'phone', + value: '', + type: 'TEXT', + }, + { + key: 'state', + value: '', + type: 'TEXT', + }, + { + key: 'zip', + value: '', + type: 'TEXT', + }, + ], + date_subscribe: null, + date_unsubscribe: null, + date_created: '2016-04-04', + date_updated: null, +}; +export const getCreateResponseClassic = { + id: 1343965485, + name: 'John', + email: 'demo@mailerlite.com', + sent: 0, + opened: 0, + clicked: 0, + type: 'active', + fields: [ + { + key: 'email', + value: 'demo@mailerlite.com', + type: 'TEXT', + }, + { + key: 'name', + value: 'John', + type: 'TEXT', + }, + { + key: 'last_name', + value: '', + type: 'TEXT', + }, + { + key: 'company', + value: 'MailerLite', + type: 'TEXT', + }, + { + key: 'country', + value: '', + type: 'TEXT', + }, + { + key: 'city', + value: '', + type: 'TEXT', + }, + { + key: 'phone', + value: '', + type: 'TEXT', + }, + { + key: 'state', + value: '', + type: 'TEXT', + }, + { + key: 'zip', + value: '', + type: 'TEXT', + }, + ], + date_subscribe: null, + date_unsubscribe: null, + date_created: '2016-04-04 12:00:00', + date_updated: '2016-04-04 12:00:00', +}; +export const getAllSubscribersResponseClassic = [ + { + id: 1343965485, + name: 'John', + email: 'demo@mailerlite.com', + sent: 0, + opened: 0, + clicked: 0, + type: 'active', + fields: [ + { + key: 'email', + value: 'demo@mailerlite.com', + type: 'TEXT', + }, + { + key: 'name', + value: 'John', + type: 'TEXT', + }, + { + key: 'last_name', + value: '', + type: 'TEXT', + }, + { + key: 'company', + value: '', + type: 'TEXT', + }, + { + key: 'country', + value: '', + type: 'TEXT', + }, + { + key: 'city', + value: '', + type: 'TEXT', + }, + { + key: 'phone', + value: '', + type: 'TEXT', + }, + { + key: 'state', + value: '', + type: 'TEXT', + }, + { + key: 'zip', + value: '', + type: 'TEXT', + }, + ], + date_subscribe: null, + date_unsubscribe: null, + date_created: '2016-04-04', + date_updated: null, + }, +]; + +export const getUpdateSubscriberResponseV2 = { + data: { + id: '139872142007207563', + email: 'user@n8n.io', + status: 'junk', + source: 'api', + sent: 0, + opens_count: 0, + clicks_count: 0, + open_rate: 0, + click_rate: 0, + ip_address: null, + subscribed_at: '2024-12-05 09:54:29', + unsubscribed_at: null, + created_at: '2024-12-05 09:54:29', + updated_at: '2024-12-05 10:20:32', + fields: { + name: null, + last_name: null, + company: null, + country: null, + city: null, + phone: null, + state: null, + z_i_p: null, + }, + groups: [], + opted_in_at: null, + optin_ip: '8.8.8.8', + }, +}; +export const getCreateResponseV2 = { + data: { + id: '139872142007207563', + email: 'user@n8n.io', + status: 'junk', + source: 'api', + sent: 0, + opens_count: 0, + clicks_count: 0, + open_rate: 0, + click_rate: 0, + ip_address: null, + subscribed_at: '2024-12-05 09:54:29', + unsubscribed_at: null, + created_at: '2024-12-05 09:54:29', + updated_at: '2024-12-05 10:20:32', + fields: { + name: null, + last_name: null, + company: null, + country: null, + city: null, + phone: null, + state: null, + z_i_p: null, + }, + groups: [], + opted_in_at: null, + optin_ip: '8.8.8.8', + }, +}; +export const getSubscriberResponseV2 = { + data: { + id: '139872142007207563', + email: 'user@n8n.io', + status: 'junk', + source: 'api', + sent: 0, + opens_count: 0, + clicks_count: 0, + open_rate: 0, + click_rate: 0, + ip_address: null, + subscribed_at: '2024-12-05 09:54:29', + unsubscribed_at: null, + created_at: '2024-12-05 09:54:29', + updated_at: '2024-12-05 10:20:32', + fields: { + name: null, + last_name: null, + company: null, + country: null, + city: null, + phone: null, + state: null, + z_i_p: null, + }, + groups: [], + opted_in_at: null, + optin_ip: '8.8.8.8', + }, +}; +export const getAllSubscribersResponseV2 = { + data: [ + { + id: '139872142007207563', + email: 'user@n8n.io', + status: 'junk', + source: 'api', + sent: 0, + opens_count: 0, + clicks_count: 0, + open_rate: 0, + click_rate: 0, + ip_address: null, + subscribed_at: '2024-12-05 09:54:29', + unsubscribed_at: null, + created_at: '2024-12-05 09:54:29', + updated_at: '2024-12-05 10:20:32', + fields: { + name: null, + last_name: null, + company: null, + country: null, + city: null, + phone: null, + state: null, + z_i_p: null, + }, + opted_in_at: null, + optin_ip: '8.8.8.8', + }, + { + id: '139059851540038710', + email: 'nathan@n8n.io', + status: 'junk', + source: 'api', + sent: 0, + opens_count: 0, + clicks_count: 0, + open_rate: 0, + click_rate: 0, + ip_address: null, + subscribed_at: null, + unsubscribed_at: null, + created_at: '2024-11-26 10:43:28', + updated_at: '2024-11-27 10:09:34', + fields: { + name: 'Nathan', + last_name: 'Workflow', + company: null, + country: null, + city: null, + phone: null, + state: null, + z_i_p: null, + }, + opted_in_at: null, + optin_ip: null, + }, + ], + links: { + first: null, + last: null, + prev: null, + next: null, + }, + meta: { + path: 'https://connect.mailerlite.com/api/subscribers', + per_page: 2, + next_cursor: null, + prev_cursor: null, + }, +}; diff --git a/packages/nodes-base/nodes/MailerLite/tests/v1/MailerLite.v1.workflow.json b/packages/nodes-base/nodes/MailerLite/tests/v1/MailerLite.v1.workflow.json new file mode 100644 index 0000000000..46f270f006 --- /dev/null +++ b/packages/nodes-base/nodes/MailerLite/tests/v1/MailerLite.v1.workflow.json @@ -0,0 +1,460 @@ +{ + "name": "[TEST] MailerLite v1 Node", + "nodes": [ + { + "parameters": {}, + "id": "be5a39ea-04bf-49a3-969f-47d4a9496f08", + "name": "When clicking ‘Test workflow’", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [-340, 280] + }, + { + "parameters": { + "email": "demo@mailerlite.com", + "additionalFields": {} + }, + "id": "98d30bbe-cbdd-4313-933e-804cdf322860", + "name": "Create Subscriber", + "type": "n8n-nodes-base.mailerLite", + "typeVersion": 1, + "position": [-140, 40], + "credentials": { + "mailerLiteApi": { + "id": "bm7VHS2C7lRgVOhb", + "name": "Mailer Lite account" + } + } + }, + { + "parameters": {}, + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [80, 40], + "id": "93aa764f-5101-4961-9b51-9fa92f746337", + "name": "No Operation, do nothing" + }, + { + "parameters": {}, + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [80, 220], + "id": "4dccb059-c4f6-4eae-b68a-5c5c36d0b8d4", + "name": "No Operation, do nothing1" + }, + { + "parameters": { + "operation": "get", + "subscriberId": "demo@mailerlite.com" + }, + "type": "n8n-nodes-base.mailerLite", + "typeVersion": 1, + "position": [-140, 220], + "id": "82115adf-edf4-4ce4-9109-3ade129294d1", + "name": "Get Subscriber", + "credentials": { + "mailerLiteApi": { + "id": "bm7VHS2C7lRgVOhb", + "name": "Mailer Lite account" + } + } + }, + { + "parameters": { + "operation": "update", + "subscriberId": "demo@mailerlite.com", + "updateFields": { + "type": "active" + } + }, + "type": "n8n-nodes-base.mailerLite", + "typeVersion": 1, + "position": [-140, 420], + "id": "fae9c6bd-1bd1-4ee8-865d-283b7edb6004", + "name": "Update Subscriber", + "credentials": { + "mailerLiteApi": { + "id": "bm7VHS2C7lRgVOhb", + "name": "Mailer Lite account" + } + } + }, + { + "parameters": {}, + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [80, 420], + "id": "45937d69-3956-434d-b955-a67d77d43d57", + "name": "No Operation, do nothing2" + }, + { + "parameters": { + "operation": "getAll", + "limit": 1, + "filters": {} + }, + "type": "n8n-nodes-base.mailerLite", + "typeVersion": 1, + "position": [-180, 680], + "id": "6491d933-0929-44bd-89cf-977823dde650", + "name": "Get Many Subscrbers", + "credentials": { + "mailerLiteApi": { + "id": "bm7VHS2C7lRgVOhb", + "name": "Mailer Lite account" + } + } + }, + { + "parameters": {}, + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [40, 680], + "id": "6e35d6e1-1ce3-4410-8558-a5d573676d8a", + "name": "No Operation, do nothing3" + } + ], + "pinData": { + "No Operation, do nothing": [ + { + "json": { + "id": 1343965485, + "name": "John", + "email": "demo@mailerlite.com", + "sent": 0, + "opened": 0, + "clicked": 0, + "type": "active", + "fields": [ + { + "key": "email", + "value": "demo@mailerlite.com", + "type": "TEXT" + }, + { + "key": "name", + "value": "John", + "type": "TEXT" + }, + { + "key": "last_name", + "value": "", + "type": "TEXT" + }, + { + "key": "company", + "value": "MailerLite", + "type": "TEXT" + }, + { + "key": "country", + "value": "", + "type": "TEXT" + }, + { + "key": "city", + "value": "", + "type": "TEXT" + }, + { + "key": "phone", + "value": "", + "type": "TEXT" + }, + { + "key": "state", + "value": "", + "type": "TEXT" + }, + { + "key": "zip", + "value": "", + "type": "TEXT" + } + ], + "date_subscribe": null, + "date_unsubscribe": null, + "date_created": "2016-04-04 12:00:00", + "date_updated": "2016-04-04 12:00:00" + } + } + ], + "No Operation, do nothing1": [ + { + "json": { + "id": 1343965485, + "name": "John", + "email": "demo@mailerlite.com", + "sent": 0, + "opened": 0, + "clicked": 0, + "type": "active", + "signup_ip": "127.0.0.1", + "signup_timestamp": "2018-01-01 01:01:01", + "confirmation_ip": "127.0.0.2", + "confirmation_timestamp": "2018-01-01 01:01:02", + "fields": [ + { + "key": "email", + "value": "demo@mailerlite.com", + "type": "TEXT" + }, + { + "key": "name", + "value": "John", + "type": "TEXT" + }, + { + "key": "last_name", + "value": "", + "type": "TEXT" + }, + { + "key": "company", + "value": "", + "type": "TEXT" + }, + { + "key": "country", + "value": "", + "type": "TEXT" + }, + { + "key": "city", + "value": "", + "type": "TEXT" + }, + { + "key": "phone", + "value": "", + "type": "TEXT" + }, + { + "key": "state", + "value": "", + "type": "TEXT" + }, + { + "key": "zip", + "value": "", + "type": "TEXT" + } + ], + "date_subscribe": null, + "date_unsubscribe": null, + "date_created": "2016-04-04", + "date_updated": null + } + } + ], + "No Operation, do nothing2": [ + { + "json": { + "id": 1343965485, + "email": "demo@mailerlite.com", + "sent": 0, + "opened": 0, + "clicked": 0, + "type": "unsubscribed", + "fields": [ + { + "key": "email", + "value": "demo@mailerlite.com", + "type": "TEXT" + }, + { + "key": "name", + "value": "Demo", + "type": "TEXT" + }, + { + "key": "last_name", + "value": "", + "type": "TEXT" + }, + { + "key": "company", + "value": "", + "type": "TEXT" + }, + { + "key": "country", + "value": "", + "type": "TEXT" + }, + { + "key": "city", + "value": "", + "type": "TEXT" + }, + { + "key": "phone", + "value": "", + "type": "TEXT" + }, + { + "key": "state", + "value": "", + "type": "TEXT" + }, + { + "key": "zip", + "value": "", + "type": "TEXT" + } + ], + "date_subscribe": null, + "date_unsubscribe": "2016-04-04 12:07:26", + "date_created": "2016-04-04", + "date_updated": null + } + } + ], + "No Operation, do nothing3": [ + { + "json": { + "id": 1343965485, + "name": "John", + "email": "demo@mailerlite.com", + "sent": 0, + "opened": 0, + "clicked": 0, + "type": "active", + "fields": [ + { + "key": "email", + "value": "demo@mailerlite.com", + "type": "TEXT" + }, + { + "key": "name", + "value": "John", + "type": "TEXT" + }, + { + "key": "last_name", + "value": "", + "type": "TEXT" + }, + { + "key": "company", + "value": "", + "type": "TEXT" + }, + { + "key": "country", + "value": "", + "type": "TEXT" + }, + { + "key": "city", + "value": "", + "type": "TEXT" + }, + { + "key": "phone", + "value": "", + "type": "TEXT" + }, + { + "key": "state", + "value": "", + "type": "TEXT" + }, + { + "key": "zip", + "value": "", + "type": "TEXT" + } + ], + "date_subscribe": null, + "date_unsubscribe": null, + "date_created": "2016-04-04", + "date_updated": null + } + } + ] + }, + "connections": { + "When clicking ‘Test workflow’": { + "main": [ + [ + { + "node": "Create Subscriber", + "type": "main", + "index": 0 + }, + { + "node": "Get Subscriber", + "type": "main", + "index": 0 + }, + { + "node": "Update Subscriber", + "type": "main", + "index": 0 + }, + { + "node": "Get Many Subscrbers", + "type": "main", + "index": 0 + } + ] + ] + }, + "Create Subscriber": { + "main": [ + [ + { + "node": "No Operation, do nothing", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get Subscriber": { + "main": [ + [ + { + "node": "No Operation, do nothing1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Update Subscriber": { + "main": [ + [ + { + "node": "No Operation, do nothing2", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get Many Subscrbers": { + "main": [ + [ + { + "node": "No Operation, do nothing3", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "826c1711-fcea-4564-809b-0258dbdd72f4", + "meta": { + "instanceId": "8c8c5237b8e37b006a7adce87f4369350c58e41f3ca9de16196d3197f69eabcd" + }, + "id": "I0absgO5t7xV2f2V", + "tags": [] +} diff --git a/packages/nodes-base/nodes/MailerLite/tests/v1/MailerLite.v1.workflow.test.ts b/packages/nodes-base/nodes/MailerLite/tests/v1/MailerLite.v1.workflow.test.ts new file mode 100644 index 0000000000..1430ea8ec7 --- /dev/null +++ b/packages/nodes-base/nodes/MailerLite/tests/v1/MailerLite.v1.workflow.test.ts @@ -0,0 +1,31 @@ +import nock from 'nock'; + +import { getWorkflowFilenames, testWorkflows } from '../../../../test/nodes/Helpers'; +import { + getCreateResponseClassic, + getSubscriberResponseClassic, + getAllSubscribersResponseClassic, + getUpdateSubscriberResponseClassic, +} from '../apiResponses'; + +describe('MailerLite', () => { + describe('Run v1 workflow', () => { + beforeAll(() => { + nock.disableNetConnect(); + + const mock = nock('https://api.mailerlite.com/api/v2'); + + mock.post('/subscribers').reply(200, getCreateResponseClassic); + mock.get('/subscribers/demo@mailerlite.com').reply(200, getSubscriberResponseClassic); + mock.get('/subscribers').query({ limit: 1 }).reply(200, getAllSubscribersResponseClassic); + mock.put('/subscribers/demo@mailerlite.com').reply(200, getUpdateSubscriberResponseClassic); + }); + + afterAll(() => { + nock.restore(); + }); + + const workflows = getWorkflowFilenames(__dirname); + testWorkflows(workflows); + }); +}); diff --git a/packages/nodes-base/nodes/MailerLite/tests/v2/MailerLite.v2.workflow.json b/packages/nodes-base/nodes/MailerLite/tests/v2/MailerLite.v2.workflow.json new file mode 100644 index 0000000000..29630950fa --- /dev/null +++ b/packages/nodes-base/nodes/MailerLite/tests/v2/MailerLite.v2.workflow.json @@ -0,0 +1,370 @@ +{ + "name": "[TEST] MailerLite v2 Node", + "nodes": [ + { + "parameters": {}, + "id": "3c72284b-2b88-4d5f-81bc-b1970b14f2af", + "name": "When clicking ‘Test workflow’", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [800, 240] + }, + { + "parameters": { + "email": "user@n8n.io", + "additionalFields": { + "status": "active" + } + }, + "id": "702c6598-cbe8-403e-962e-56621ec727a4", + "name": "Create Subscriber", + "type": "n8n-nodes-base.mailerLite", + "typeVersion": 2, + "position": [1000, 0], + "credentials": { + "mailerLiteApi": { + "id": "bm7VHS2C7lRgVOhb", + "name": "Mailer Lite account" + } + } + }, + { + "parameters": {}, + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [1220, 0], + "id": "540b98b5-b3bf-49a1-a406-acc6872f4b50", + "name": "No Operation, do nothing" + }, + { + "parameters": {}, + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [1220, 180], + "id": "17c0b8e7-a9d7-4a4f-882f-c3fb3f6bc289", + "name": "No Operation, do nothing1" + }, + { + "parameters": { + "operation": "get", + "subscriberId": "user@n8n.io" + }, + "type": "n8n-nodes-base.mailerLite", + "typeVersion": 2, + "position": [1000, 180], + "id": "5598f2b9-4d67-4ad7-a8e4-7b7bf723cd5a", + "name": "Get Subscriber", + "credentials": { + "mailerLiteApi": { + "id": "bm7VHS2C7lRgVOhb", + "name": "Mailer Lite account" + } + } + }, + { + "parameters": { + "operation": "update", + "subscriberId": "user@n8n.io", + "additionalFields": { + "status": "junk", + "optin_ip": "8.8.8.8" + } + }, + "type": "n8n-nodes-base.mailerLite", + "typeVersion": 2, + "position": [1000, 380], + "id": "223e4507-c88e-4066-a122-ccaf9cea7b49", + "name": "Update Subscriber", + "credentials": { + "mailerLiteApi": { + "id": "bm7VHS2C7lRgVOhb", + "name": "Mailer Lite account" + } + } + }, + { + "parameters": {}, + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [1220, 380], + "id": "94d04b52-8809-4670-a8ca-135921139fc9", + "name": "No Operation, do nothing2" + }, + { + "parameters": { + "operation": "getAll", + "limit": 2, + "filters": { + "status": "junk" + } + }, + "type": "n8n-nodes-base.mailerLite", + "typeVersion": 2, + "position": [960, 640], + "id": "30c6e797-ceda-4c84-8f34-b61200ffd9e9", + "name": "Get Many Subscrbers", + "credentials": { + "mailerLiteApi": { + "id": "bm7VHS2C7lRgVOhb", + "name": "Mailer Lite account" + } + } + }, + { + "parameters": {}, + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [1180, 640], + "id": "c8529a30-889b-4ac9-a509-73f5dd8eef4a", + "name": "No Operation, do nothing3" + } + ], + "pinData": { + "No Operation, do nothing": [ + { + "json": { + "id": "139872142007207563", + "email": "user@n8n.io", + "status": "junk", + "source": "api", + "sent": 0, + "opens_count": 0, + "clicks_count": 0, + "open_rate": 0, + "click_rate": 0, + "ip_address": null, + "subscribed_at": "2024-12-05 09:54:29", + "unsubscribed_at": null, + "created_at": "2024-12-05 09:54:29", + "updated_at": "2024-12-05 10:20:32", + "fields": { + "name": null, + "last_name": null, + "company": null, + "country": null, + "city": null, + "phone": null, + "state": null, + "z_i_p": null + }, + "groups": [], + "opted_in_at": null, + "optin_ip": "8.8.8.8" + } + } + ], + "No Operation, do nothing1": [ + { + "json": { + "id": "139872142007207563", + "email": "user@n8n.io", + "status": "junk", + "source": "api", + "sent": 0, + "opens_count": 0, + "clicks_count": 0, + "open_rate": 0, + "click_rate": 0, + "ip_address": null, + "subscribed_at": "2024-12-05 09:54:29", + "unsubscribed_at": null, + "created_at": "2024-12-05 09:54:29", + "updated_at": "2024-12-05 10:20:32", + "fields": { + "name": null, + "last_name": null, + "company": null, + "country": null, + "city": null, + "phone": null, + "state": null, + "z_i_p": null + }, + "groups": [], + "opted_in_at": null, + "optin_ip": "8.8.8.8" + } + } + ], + "No Operation, do nothing2": [ + { + "json": { + "data": { + "id": "139872142007207563", + "email": "user@n8n.io", + "status": "junk", + "source": "api", + "sent": 0, + "opens_count": 0, + "clicks_count": 0, + "open_rate": 0, + "click_rate": 0, + "ip_address": null, + "subscribed_at": "2024-12-05 09:54:29", + "unsubscribed_at": null, + "created_at": "2024-12-05 09:54:29", + "updated_at": "2024-12-05 10:20:32", + "fields": { + "name": null, + "last_name": null, + "company": null, + "country": null, + "city": null, + "phone": null, + "state": null, + "z_i_p": null + }, + "groups": [], + "opted_in_at": null, + "optin_ip": "8.8.8.8" + } + } + } + ], + "No Operation, do nothing3": [ + { + "json": { + "id": "139872142007207563", + "email": "user@n8n.io", + "status": "junk", + "source": "api", + "sent": 0, + "opens_count": 0, + "clicks_count": 0, + "open_rate": 0, + "click_rate": 0, + "ip_address": null, + "subscribed_at": "2024-12-05 09:54:29", + "unsubscribed_at": null, + "created_at": "2024-12-05 09:54:29", + "updated_at": "2024-12-05 10:20:32", + "fields": { + "name": null, + "last_name": null, + "company": null, + "country": null, + "city": null, + "phone": null, + "state": null, + "z_i_p": null + }, + "opted_in_at": null, + "optin_ip": "8.8.8.8" + } + }, + { + "json": { + "id": "139059851540038710", + "email": "nathan@n8n.io", + "status": "junk", + "source": "api", + "sent": 0, + "opens_count": 0, + "clicks_count": 0, + "open_rate": 0, + "click_rate": 0, + "ip_address": null, + "subscribed_at": null, + "unsubscribed_at": null, + "created_at": "2024-11-26 10:43:28", + "updated_at": "2024-11-27 10:09:34", + "fields": { + "name": "Nathan", + "last_name": "Workflow", + "company": null, + "country": null, + "city": null, + "phone": null, + "state": null, + "z_i_p": null + }, + "opted_in_at": null, + "optin_ip": null + } + } + ] + }, + "connections": { + "When clicking ‘Test workflow’": { + "main": [ + [ + { + "node": "Create Subscriber", + "type": "main", + "index": 0 + }, + { + "node": "Get Subscriber", + "type": "main", + "index": 0 + }, + { + "node": "Update Subscriber", + "type": "main", + "index": 0 + }, + { + "node": "Get Many Subscrbers", + "type": "main", + "index": 0 + } + ] + ] + }, + "Create Subscriber": { + "main": [ + [ + { + "node": "No Operation, do nothing", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get Subscriber": { + "main": [ + [ + { + "node": "No Operation, do nothing1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Update Subscriber": { + "main": [ + [ + { + "node": "No Operation, do nothing2", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get Many Subscrbers": { + "main": [ + [ + { + "node": "No Operation, do nothing3", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "338331ef-1b38-47dc-9e2b-45340ea3fe3b", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "8c8c5237b8e37b006a7adce87f4369350c58e41f3ca9de16196d3197f69eabcd" + }, + "id": "0Ov6Vd62DUXrWWQH", + "tags": [] +} diff --git a/packages/nodes-base/nodes/MailerLite/tests/v2/MailerLite.v2.workflow.test.ts b/packages/nodes-base/nodes/MailerLite/tests/v2/MailerLite.v2.workflow.test.ts new file mode 100644 index 0000000000..d723e86908 --- /dev/null +++ b/packages/nodes-base/nodes/MailerLite/tests/v2/MailerLite.v2.workflow.test.ts @@ -0,0 +1,34 @@ +import nock from 'nock'; + +import { getWorkflowFilenames, testWorkflows } from '../../../../test/nodes/Helpers'; +import { + getCreateResponseV2, + getSubscriberResponseV2, + getAllSubscribersResponseV2, + getUpdateSubscriberResponseV2, +} from '../apiResponses'; + +describe('MailerLite', () => { + describe('Run v2 workflow', () => { + beforeAll(() => { + nock.disableNetConnect(); + + const mock = nock('https://connect.mailerlite.com/api'); + + mock.post('/subscribers').reply(200, getCreateResponseV2); + mock.get('/subscribers/user@n8n.io').reply(200, getSubscriberResponseV2); + mock + .get('/subscribers') + .query({ 'filter[status]': 'junk', limit: 2 }) + .reply(200, getAllSubscribersResponseV2); + mock.put('/subscribers/user@n8n.io').reply(200, getUpdateSubscriberResponseV2); + }); + + afterAll(() => { + nock.restore(); + }); + + const workflows = getWorkflowFilenames(__dirname); + testWorkflows(workflows); + }); +}); diff --git a/packages/nodes-base/nodes/MailerLite/v1/MailerLiteTriggerV1.node.ts b/packages/nodes-base/nodes/MailerLite/v1/MailerLiteTriggerV1.node.ts new file mode 100644 index 0000000000..faaa6db8c7 --- /dev/null +++ b/packages/nodes-base/nodes/MailerLite/v1/MailerLiteTriggerV1.node.ts @@ -0,0 +1,184 @@ +import { + type IHookFunctions, + type IWebhookFunctions, + type IDataObject, + type INodeType, + type INodeTypeDescription, + type IWebhookResponseData, + type INodeTypeBaseDescription, + NodeConnectionType, +} from 'n8n-workflow'; + +import { mailerliteApiRequest } from '../GenericFunctions'; + +export class MailerLiteTriggerV1 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + displayName: 'MailerLite Trigger', + name: 'mailerLiteTrigger', + group: ['trigger'], + version: 1, + description: 'Starts the workflow when MailerLite events occur', + defaults: { + name: 'MailerLite Trigger', + }, + inputs: [], + outputs: [NodeConnectionType.Main], + credentials: [ + { + name: 'mailerLiteApi', + required: true, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Event', + name: 'event', + type: 'options', + options: [ + { + name: 'Campaign Sent', + value: 'campaign.sent', + description: 'Fired when campaign is sent', + }, + { + name: 'Subscriber Added Through Webform', + value: 'subscriber.added_through_webform', + description: 'Fired when a subscriber is added though a form', + }, + { + name: 'Subscriber Added to Group', + value: 'subscriber.add_to_group', + description: 'Fired when a subscriber is added to a group', + }, + { + name: 'Subscriber Automation Completed', + value: 'subscriber.automation_complete', + description: 'Fired when subscriber finishes automation', + }, + { + name: 'Subscriber Automation Triggered', + value: 'subscriber.automation_triggered', + description: 'Fired when subscriber starts automation', + }, + { + name: 'Subscriber Bounced', + value: 'subscriber.bounced', + description: 'Fired when an email address bounces', + }, + { + name: 'Subscriber Complained', + value: 'subscriber.complaint', + description: 'Fired when subscriber marks a campaign as a spam', + }, + { + name: 'Subscriber Created', + value: 'subscriber.create', + description: 'Fired when a new subscriber is added to an account', + }, + { + name: 'Subscriber Removed From Group', + value: 'subscriber.remove_from_group', + description: 'Fired when a subscriber is removed from a group', + }, + { + name: 'Subscriber Unsubscribe', + value: 'subscriber.unsubscribe', + description: 'Fired when a subscriber becomes unsubscribed', + }, + { + name: 'Subscriber Updated', + value: 'subscriber.update', + description: "Fired when any of the subscriber's custom fields are updated", + }, + ], + required: true, + default: [], + description: 'The events to listen to', + }, + ], + }; + } + + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise<boolean> { + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + const event = this.getNodeParameter('event') as string; + // Check all the webhooks which exist already if it is identical to the + // one that is supposed to get created. + const endpoint = '/webhooks'; + const { webhooks } = await mailerliteApiRequest.call(this, 'GET', endpoint, {}); + for (const webhook of webhooks) { + if (webhook.url === webhookUrl && webhook.event === event) { + // Set webhook-id to be sure that it can be deleted + webhookData.webhookId = webhook.id as string; + return true; + } + } + return false; + }, + async create(this: IHookFunctions): Promise<boolean> { + const webhookData = this.getWorkflowStaticData('node'); + const webhookUrl = this.getNodeWebhookUrl('default'); + const event = this.getNodeParameter('event') as string; + + const endpoint = '/webhooks'; + + const body = { + url: webhookUrl, + event, + }; + + const responseData = await mailerliteApiRequest.call(this, 'POST', endpoint, body); + + if (responseData.id === undefined) { + // Required data is missing so was not successful + return false; + } + + webhookData.webhookId = responseData.id as string; + return true; + }, + async delete(this: IHookFunctions): Promise<boolean> { + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId !== undefined) { + const endpoint = `/webhooks/${webhookData.webhookId}`; + + try { + await mailerliteApiRequest.call(this, 'DELETE', endpoint); + } catch (error) { + return false; + } + + // Remove from the static workflow data so that it is clear + // that no webhooks are registered anymore + delete webhookData.webhookId; + } + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> { + const body = this.getBodyData(); + + const events = body.events as IDataObject[]; + + return { + workflowData: [this.helpers.returnJsonArray(events)], + }; + } +} diff --git a/packages/nodes-base/nodes/MailerLite/v1/MailerLiteV1.node.ts b/packages/nodes-base/nodes/MailerLite/v1/MailerLiteV1.node.ts new file mode 100644 index 0000000000..357f8f799f --- /dev/null +++ b/packages/nodes-base/nodes/MailerLite/v1/MailerLiteV1.node.ts @@ -0,0 +1,199 @@ +import type { + IExecuteFunctions, + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, + INodeTypeBaseDescription, +} from 'n8n-workflow'; +import { NodeConnectionType } from 'n8n-workflow'; + +import { subscriberFields, subscriberOperations } from './SubscriberDescription'; +import { + getCustomFields, + mailerliteApiRequest, + mailerliteApiRequestAllItems, +} from '../GenericFunctions'; + +export class MailerLiteV1 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + displayName: 'MailerLite', + name: 'mailerLite', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Mailer Lite API', + defaults: { + name: 'MailerLite', + }, + inputs: [NodeConnectionType.Main], + outputs: [NodeConnectionType.Main], + credentials: [ + { + name: 'mailerLiteApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Subscriber', + value: 'subscriber', + }, + ], + default: 'subscriber', + }, + ...subscriberOperations, + ...subscriberFields, + ], + }; + } + + methods = { + loadOptions: { + getCustomFields, + }, + }; + + async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; + const length = items.length; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0); + const operation = this.getNodeParameter('operation', 0); + for (let i = 0; i < length; i++) { + try { + if (resource === 'subscriber') { + //https://developers.mailerlite.com/reference#create-a-subscriber + if (operation === 'create') { + const email = this.getNodeParameter('email', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i); + + const body: IDataObject = { + email, + fields: [], + }; + + Object.assign(body, additionalFields); + + if (additionalFields.customFieldsUi) { + const customFieldsValues = (additionalFields.customFieldsUi as IDataObject) + .customFieldsValues as IDataObject[]; + + if (customFieldsValues) { + const fields = {}; + + for (const customFieldValue of customFieldsValues) { + //@ts-ignore + fields[customFieldValue.fieldId] = customFieldValue.value; + } + + body.fields = fields; + delete body.customFieldsUi; + } + } + + responseData = await mailerliteApiRequest.call(this, 'POST', '/subscribers', body); + } + //https://developers.mailerlite.com/reference#single-subscriber + if (operation === 'get') { + const subscriberId = this.getNodeParameter('subscriberId', i) as string; + + responseData = await mailerliteApiRequest.call( + this, + 'GET', + `/subscribers/${subscriberId}`, + ); + } + //https://developers.mailerlite.com/reference#subscribers + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i); + + const filters = this.getNodeParameter('filters', i); + + Object.assign(qs, filters); + + if (returnAll) { + responseData = await mailerliteApiRequestAllItems.call( + this, + 'GET', + '/subscribers', + {}, + qs, + ); + } else { + qs.limit = this.getNodeParameter('limit', i); + + responseData = await mailerliteApiRequest.call(this, 'GET', '/subscribers', {}, qs); + } + } + //https://developers.mailerlite.com/reference#update-subscriber + if (operation === 'update') { + const subscriberId = this.getNodeParameter('subscriberId', i) as string; + + const updateFields = this.getNodeParameter('updateFields', i); + + const body: IDataObject = {}; + + Object.assign(body, updateFields); + + if (updateFields.customFieldsUi) { + const customFieldsValues = (updateFields.customFieldsUi as IDataObject) + .customFieldsValues as IDataObject[]; + + if (customFieldsValues) { + const fields = {}; + + for (const customFieldValue of customFieldsValues) { + //@ts-ignore + fields[customFieldValue.fieldId] = customFieldValue.value; + } + + body.fields = fields; + delete body.customFieldsUi; + } + } + + responseData = await mailerliteApiRequest.call( + this, + 'PUT', + `/subscribers/${subscriberId}`, + body, + ); + } + } + } 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; + } + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject[]), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } + + return [returnData]; + } +} diff --git a/packages/nodes-base/nodes/MailerLite/SubscriberDescription.ts b/packages/nodes-base/nodes/MailerLite/v1/SubscriberDescription.ts similarity index 100% rename from packages/nodes-base/nodes/MailerLite/SubscriberDescription.ts rename to packages/nodes-base/nodes/MailerLite/v1/SubscriberDescription.ts diff --git a/packages/nodes-base/nodes/MailerLite/v2/MailerLite.Interface.ts b/packages/nodes-base/nodes/MailerLite/v2/MailerLite.Interface.ts new file mode 100644 index 0000000000..f408e75ebb --- /dev/null +++ b/packages/nodes-base/nodes/MailerLite/v2/MailerLite.Interface.ts @@ -0,0 +1,35 @@ +export interface CustomField { + name: string; + key: string; +} + +export interface SubscriberFields { + city: string | null; + company: string | null; + country: string | null; + last_name: string | null; + name: string | null; + phone: string | null; + state: string | null; + z_i_p: string | null; +} + +export interface Subscriber { + id: string; + email: string; + status: string; + source: string; + sent: number; + opens_count: number; + clicks_count: number; + open_rate: number; + click_rate: number; + ip_address: string | null; + subscribed_at: string; + unsubscribed_at: string | null; + created_at: string; + updated_at: string; + fields: SubscriberFields; + opted_in_at: string | null; + optin_ip: string | null; +} diff --git a/packages/nodes-base/nodes/MailerLite/v2/MailerLiteTriggerV2.node.ts b/packages/nodes-base/nodes/MailerLite/v2/MailerLiteTriggerV2.node.ts new file mode 100644 index 0000000000..f7b17316dc --- /dev/null +++ b/packages/nodes-base/nodes/MailerLite/v2/MailerLiteTriggerV2.node.ts @@ -0,0 +1,179 @@ +import { + type IHookFunctions, + type IWebhookFunctions, + type IDataObject, + type INodeType, + type INodeTypeDescription, + type IWebhookResponseData, + type INodeTypeBaseDescription, + NodeConnectionType, +} from 'n8n-workflow'; + +import { mailerliteApiRequest } from '../GenericFunctions'; + +export class MailerLiteTriggerV2 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + displayName: 'MailerLite Trigger', + name: 'mailerLiteTrigger', + group: ['trigger'], + version: [2], + description: 'Starts the workflow when MailerLite events occur', + defaults: { + name: 'MailerLite Trigger', + }, + inputs: [], + outputs: [NodeConnectionType.Main], + credentials: [ + { + name: 'mailerLiteApi', + required: true, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Events', + name: 'events', + type: 'multiOptions', + options: [ + { + name: 'Campaign Sent', + value: 'campaign.sent', + description: 'Fired when campaign is sent', + }, + { + name: 'Subscriber Added to Group', + value: 'subscriber.added_to_group', + description: 'Fired when a subscriber is added to a group', + }, + { + name: 'Subscriber Automation Completed', + value: 'subscriber.automation_completed', + description: 'Fired when subscriber finishes automation', + }, + { + name: 'Subscriber Automation Triggered', + value: 'subscriber.automation_triggered', + description: 'Fired when subscriber starts automation', + }, + { + name: 'Subscriber Bounced', + value: 'subscriber.bounced', + description: 'Fired when an email address bounces', + }, + { + name: 'Subscriber Created', + value: 'subscriber.created', + description: 'Fired when a new subscriber is added to an account', + }, + { + name: 'Subscriber Removed From Group', + value: 'subscriber.removed_from_group', + description: 'Fired when a subscriber is removed from a group', + }, + { + name: 'Subscriber Spam Reported', + value: 'subscriber.spam_reported', + description: 'Fired when subscriber marks a campaign as a spam', + }, + { + name: 'Subscriber Unsubscribe', + value: 'subscriber.unsubscribed', + description: 'Fired when a subscriber becomes unsubscribed', + }, + { + name: 'Subscriber Updated', + value: 'subscriber.updated', + description: "Fired when any of the subscriber's custom fields are updated", + }, + ], + required: true, + default: [], + description: 'The events to listen to', + }, + ], + }; + } + + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise<boolean> { + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + const events = this.getNodeParameter('events') as string[]; + // Check all the webhooks which exist already if it is identical to the + // one that is supposed to get created. + const endpoint = '/webhooks'; + const { data } = await mailerliteApiRequest.call(this, 'GET', endpoint, {}); + for (const webhook of data) { + if (webhook.url === webhookUrl && webhook.events === events) { + // Set webhook-id to be sure that it can be deleted + webhookData.webhookId = webhook.id as string; + return true; + } + } + return false; + }, + async create(this: IHookFunctions): Promise<boolean> { + const webhookData = this.getWorkflowStaticData('node'); + const webhookUrl = this.getNodeWebhookUrl('default'); + const events = this.getNodeParameter('events') as string[]; + + const endpoint = '/webhooks'; + + const body = { + url: webhookUrl, + events, + }; + + const { data } = await mailerliteApiRequest.call(this, 'POST', endpoint, body); + + if (data.id === undefined) { + // Required data is missing so was not successful + return false; + } + + webhookData.webhookId = data.id as string; + return true; + }, + async delete(this: IHookFunctions): Promise<boolean> { + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId !== undefined) { + const endpoint = `/webhooks/${webhookData.webhookId}`; + + try { + await mailerliteApiRequest.call(this, 'DELETE', endpoint); + } catch (error) { + return false; + } + + // Remove from the static workflow data so that it is clear + // that no webhooks are registered anymore + delete webhookData.webhookId; + } + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> { + const body = this.getBodyData(); + + const data = body.fields as IDataObject[]; + + return { + workflowData: [this.helpers.returnJsonArray(data)], + }; + } +} diff --git a/packages/nodes-base/nodes/MailerLite/v2/MailerLiteV2.node.ts b/packages/nodes-base/nodes/MailerLite/v2/MailerLiteV2.node.ts new file mode 100644 index 0000000000..953dc7232e --- /dev/null +++ b/packages/nodes-base/nodes/MailerLite/v2/MailerLiteV2.node.ts @@ -0,0 +1,206 @@ +import type { + IExecuteFunctions, + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, + INodeTypeBaseDescription, +} from 'n8n-workflow'; +import { NodeConnectionType } from 'n8n-workflow'; + +import type { Subscriber } from './MailerLite.Interface'; +import { subscriberFields, subscriberOperations } from './SubscriberDescription'; +import { + getCustomFields, + mailerliteApiRequest, + mailerliteApiRequestAllItems, +} from '../GenericFunctions'; + +export class MailerLiteV2 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + displayName: 'MailerLite', + name: 'mailerLite', + group: ['input'], + version: [2], + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Mailer Lite API', + defaults: { + name: 'MailerLite', + }, + inputs: [NodeConnectionType.Main], + outputs: [NodeConnectionType.Main], + credentials: [ + { + name: 'mailerLiteApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Subscriber', + value: 'subscriber', + }, + ], + default: 'subscriber', + }, + ...subscriberOperations, + ...subscriberFields, + ], + }; + } + + methods = { + loadOptions: { + getCustomFields, + }, + }; + + async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; + const length = items.length; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0); + const operation = this.getNodeParameter('operation', 0); + for (let i = 0; i < length; i++) { + try { + if (resource === 'subscriber') { + //https://developers.mailerlite.com/reference#create-a-subscriber + if (operation === 'create') { + const email = this.getNodeParameter('email', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i); + + const body: IDataObject = { + email, + fields: [], + }; + + Object.assign(body, additionalFields); + + if (additionalFields.customFieldsUi) { + const customFieldsValues = (additionalFields.customFieldsUi as IDataObject) + .customFieldsValues as IDataObject[]; + + if (customFieldsValues) { + const fields = {}; + + for (const customFieldValue of customFieldsValues) { + //@ts-ignore + fields[customFieldValue.fieldId] = customFieldValue.value; + } + + body.fields = fields; + delete body.customFieldsUi; + } + } + + responseData = await mailerliteApiRequest.call(this, 'POST', '/subscribers', body); + responseData = responseData.data; + } + //https://developers.mailerlite.com/reference#single-subscriber + if (operation === 'get') { + const subscriberId = this.getNodeParameter('subscriberId', i) as string; + + responseData = await mailerliteApiRequest.call( + this, + 'GET', + `/subscribers/${subscriberId}`, + ); + + responseData = responseData.data as Subscriber[]; + } + //https://developers.mailerlite.com/reference#subscribers + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i); + + const filters = this.getNodeParameter('filters', i); + + if (filters.status) { + qs['filter[status]'] = filters.status as string; + } + + if (returnAll) { + responseData = await mailerliteApiRequestAllItems.call( + this, + 'GET', + '/subscribers', + {}, + qs, + ); + } else { + qs.limit = this.getNodeParameter('limit', i); + + responseData = await mailerliteApiRequest.call(this, 'GET', '/subscribers', {}, qs); + responseData = responseData.data; + } + } + //https://developers.mailerlite.com/reference#update-subscriber + if (operation === 'update') { + const subscriberId = this.getNodeParameter('subscriberId', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i); + + const body: IDataObject = {}; + + Object.assign(body, additionalFields); + + if (additionalFields.customFieldsUi) { + const customFieldsValues = (additionalFields.customFieldsUi as IDataObject) + .customFieldsValues as IDataObject[]; + + if (customFieldsValues) { + const fields = {}; + + for (const customFieldValue of customFieldsValues) { + //@ts-ignore + fields[customFieldValue.fieldId] = customFieldValue.value; + } + + body.fields = fields; + delete body.customFieldsUi; + } + } + + responseData = await mailerliteApiRequest.call( + this, + 'PUT', + `/subscribers/${subscriberId}`, + body, + ); + } + } + } 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; + } + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject[]), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } + + return [returnData]; + } +} diff --git a/packages/nodes-base/nodes/MailerLite/v2/SubscriberDescription.ts b/packages/nodes-base/nodes/MailerLite/v2/SubscriberDescription.ts new file mode 100644 index 0000000000..10ddfe22f5 --- /dev/null +++ b/packages/nodes-base/nodes/MailerLite/v2/SubscriberDescription.ts @@ -0,0 +1,304 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const subscriberOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['subscriber'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new subscriber', + action: 'Create a subscriber', + }, + { + name: 'Get', + value: 'get', + description: 'Get an subscriber', + action: 'Get a subscriber', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Get many subscribers', + action: 'Get many subscribers', + }, + { + name: 'Update', + value: 'update', + description: 'Update an subscriber', + action: 'Update a subscriber', + }, + ], + default: 'create', + }, +]; + +export const subscriberFields: INodeProperties[] = [ + /* -------------------------------------------------------------------------- */ + /* subscriber:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Email', + name: 'email', + type: 'string', + placeholder: 'name@email.com', + required: true, + default: '', + displayOptions: { + show: { + resource: ['subscriber'], + operation: ['create'], + }, + }, + description: 'Email of new subscriber', + }, + + /* -------------------------------------------------------------------------- */ + /* subscriber:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Subscriber Email', + name: 'subscriberId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: ['subscriber'], + operation: ['update'], + }, + }, + default: '', + description: 'Email of subscriber', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['subscriber'], + operation: ['update', 'create'], + }, + }, + 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 or ID', + name: 'fieldId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCustomFields', + }, + default: '', + description: + 'The ID of the field to add custom field to. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'The value to set on custom field', + }, + ], + }, + ], + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Active', + value: 'active', + }, + { + name: 'Bounced', + value: 'bounced', + }, + { + name: 'Junk', + value: 'junk', + }, + { + name: 'Unconfirmed', + value: 'unconfirmed', + }, + { + name: 'Unsubscribed', + value: 'unsubscribed', + }, + ], + default: '', + }, + { + displayName: 'Subscribed At', + name: 'subscribed_at', + type: 'dateTime', + default: '', + }, + { + displayName: 'IP Address', + name: 'ip_address', + type: 'string', + default: '', + }, + { + displayName: 'Opted In At', + name: 'opted_in_at', + type: 'dateTime', + default: '', + }, + { + displayName: 'Opt In IP', + name: 'optin_ip', + type: 'string', + default: '', + }, + { + displayName: 'Unsubscribed At', + name: 'unsubscribed_at', + type: 'dateTime', + default: '', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* subscriber:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Subscriber Email', + name: 'subscriberId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: ['subscriber'], + operation: ['delete'], + }, + }, + default: '', + description: 'Email of subscriber to delete', + }, + + /* -------------------------------------------------------------------------- */ + /* subscriber:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Subscriber Email', + name: 'subscriberId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: ['subscriber'], + operation: ['get'], + }, + }, + default: '', + description: 'Email of subscriber to get', + }, + /* -------------------------------------------------------------------------- */ + /* subscriber:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['subscriber'], + operation: ['getAll'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['subscriber'], + operation: ['getAll'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'Max number of results to return', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + displayOptions: { + show: { + operation: ['getAll'], + resource: ['subscriber'], + }, + }, + default: {}, + options: [ + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Active', + value: 'active', + }, + { + name: 'Bounced', + value: 'bounced', + }, + { + name: 'Junk', + value: 'junk', + }, + { + name: 'Unconfirmed', + value: 'unconfirmed', + }, + { + name: 'Unsubscribed', + value: 'unsubscribed', + }, + ], + default: '', + }, + ], + }, +];