diff --git a/packages/nodes-base/credentials/MailerLiteApi.credentials.ts b/packages/nodes-base/credentials/MailerLiteApi.credentials.ts new file mode 100644 index 000000000..24f5f8909 --- /dev/null +++ b/packages/nodes-base/credentials/MailerLiteApi.credentials.ts @@ -0,0 +1,17 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class MailerLiteApi implements ICredentialType { + name = 'mailerLiteApi'; + displayName = 'Mailer Lite API'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/MailerLite/GenericFunctions.ts b/packages/nodes-base/nodes/MailerLite/GenericFunctions.ts new file mode 100644 index 000000000..cf9b7eada --- /dev/null +++ b/packages/nodes-base/nodes/MailerLite/GenericFunctions.ts @@ -0,0 +1,66 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function mailerliteApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IHookFunctions, method: string, path: string, body: any = {}, qs: IDataObject = {}, option = {}): Promise { // tslint:disable-line:no-any + + const credentials = this.getCredentials('mailerLiteApi') as IDataObject; + + const options: OptionsWithUri = { + headers: { + 'X-MailerLite-ApiKey': credentials.apiKey, + }, + method, + body, + qs, + uri: `https://api.mailerlite.com/api/v2${path}`, + json: true, + }; + try { + if (Object.keys(body).length === 0) { + delete options.body; + } + //@ts-ignore + return await this.helpers.request.call(this, options); + } catch (error) { + if (error.response && error.response.body && error.response.body.error) { + + const message = error.response.body.error.message; + + // Try to return the error prettier + throw new Error( + `Mailer Lite error response [${error.statusCode}]: ${message}` + ); + } + throw error; + } +} + +export async function mailerliteApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-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); + query.offset = query.offset + query.limit; + } while ( + responseData.length !== 0 + ); + return returnData; +} diff --git a/packages/nodes-base/nodes/MailerLite/MailerLite.node.ts b/packages/nodes-base/nodes/MailerLite/MailerLite.node.ts new file mode 100644 index 000000000..974b50871 --- /dev/null +++ b/packages/nodes-base/nodes/MailerLite/MailerLite.node.ts @@ -0,0 +1,185 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + mailerliteApiRequest, + mailerliteApiRequestAllItems, +} from './GenericFunctions'; + +import { + subscriberFields, + subscriberOperations, +} from './SubscriberDescription'; + +export class MailerLite implements INodeType { + description: INodeTypeDescription = { + displayName: 'MailerLite', + name: 'marilerLite', + icon: 'file:mailerLite.png', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Mailer Lite API.', + defaults: { + name: 'MailerLite', + color: '#58be72', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'mailerLiteApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Subscriber', + value: 'subscriber', + }, + ], + default: 'subscriber', + description: 'The resource to operate on.' + }, + ...subscriberOperations, + ...subscriberFields, + ], + }; + + methods = { + loadOptions: { + // Get all the available custom fields to display them to user so that he can + // select them easily + async getCustomFields(this: ILoadOptionsFunctions): Promise { + 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 { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = (items.length as unknown) as number; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < length; i++) { + + 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) as IDataObject; + + 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) as boolean; + + const filters = this.getNodeParameter('filters', i) as IDataObject; + + Object.assign(qs, filters); + + if (returnAll) { + + responseData = await mailerliteApiRequestAllItems.call(this, 'GET', `/subscribers`, {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + + 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) as IDataObject; + + 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); + } + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/MailerLite/MailerLiteTrigger.node.ts b/packages/nodes-base/nodes/MailerLite/MailerLiteTrigger.node.ts new file mode 100644 index 000000000..e53a1867c --- /dev/null +++ b/packages/nodes-base/nodes/MailerLite/MailerLiteTrigger.node.ts @@ -0,0 +1,189 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeType, + INodeTypeDescription, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + mailerliteApiRequest, +} from './GenericFunctions'; + +export class MailerLiteTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'MailerLite Trigger', + name: 'mailerLiteTrigger', + icon: 'file:mailerLite.png', + group: ['trigger'], + version: 1, + description: 'Starts the workflow when a MailerLite events occurs.', + defaults: { + name: 'MailerLite Trigger', + color: '#58be72', + }, + inputs: [], + outputs: ['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 to Group', + value: 'subscriber.add_to_group', + description: `Fired when a subscriber is added to a group.`, + }, + { + name: 'Subscriber Added Throught Webform', + value: 'subscriber.added_through_webform', + description: `Fired when a subscriber is added though a form.`, + }, + { + 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 Created', + value: 'subscriber.create', + description: 'Fired when a new subscriber is added to an account.', + }, + { + name: 'Subscriber Complained', + value: 'subscriber.complaint', + description: `Fired when subscriber marks a campaign as a spam.`, + }, + { + 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.', + }, + ], + }; + + // @ts-ignore (because of request) + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + 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 { + 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 { + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId !== undefined) { + + const endpoint = `/webhooks/${webhookData.webhookId}`; + + try { + await mailerliteApiRequest.call(this, 'DELETE', endpoint); + } catch (e) { + return false; + } + + // Remove from the static workflow data so that it is clear + // that no webhooks are registred anymore + delete webhookData.webhookId; + } + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const body = this.getBodyData() as IDataObject; + + const events = body.events as IDataObject[]; + + return { + workflowData: [ + this.helpers.returnJsonArray(events) + ], + }; + } +} diff --git a/packages/nodes-base/nodes/MailerLite/SubscriberDescription.ts b/packages/nodes-base/nodes/MailerLite/SubscriberDescription.ts new file mode 100644 index 000000000..cf66794e1 --- /dev/null +++ b/packages/nodes-base/nodes/MailerLite/SubscriberDescription.ts @@ -0,0 +1,412 @@ +import { + INodeProperties, +} from "n8n-workflow"; + +export const subscriberOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'subscriber', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new subscriber', + }, + { + name: 'Get', + value: 'get', + description: 'Get an subscriber', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all subscribers', + }, + { + name: 'Update', + value: 'update', + description: 'Update an subscriber', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const subscriberFields = [ + +/* -------------------------------------------------------------------------- */ +/* subscriber:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Email', + name: 'email', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'subscriber', + ], + operation: [ + 'create' + ] + }, + }, + description: 'email of new subscriber', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'subscriber', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Confirmation Timestamp', + name: 'confirmation_timestamp', + type: 'string', + default: '', + }, + { + displayName: 'Confirmation IP', + name: 'confirmation_ip', + type: 'string', + default: '', + }, + { + 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 ID', + name: 'fieldId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCustomFields', + }, + default: '', + description: 'The ID of the field to add custom field to.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'The value to set on custom field.', + }, + ], + }, + ], + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + }, + { + displayName: 'Resubscribe', + name: 'resubscribe', + type: 'boolean', + default: false, + description: 'reactivate subscriber if value is true', + }, + { + displayName: 'Signup IP', + name: 'signup_ip', + type: 'string', + default: '', + }, + { + displayName: 'Signup Timestamp', + name: 'signup_timestamp', + type: 'string', + default: '', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Active', + value: 'active', + }, + { + name: 'Unsubscribed', + value: 'unsubscribed', + }, + { + name: 'Unconfirmed', + value: 'unconfirmed', + }, + ], + default: '', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* subscriber:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Subscriber ID', + name: 'subscriberId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'subscriber', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + description: 'ID or email of subscriber', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'subscriber', + ], + operation: [ + 'update', + ], + }, + }, + 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 ID', + name: 'fieldId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCustomFields', + }, + default: '', + description: 'The ID of the field to add custom field to.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'The value to set on custom field.', + }, + ], + }, + ], + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + }, + { + displayName: 'Resend Autoresponders', + name: 'resend_autoresponders', + type: 'boolean', + default: false, + description: 'Defines if it is needed to resend autoresponders', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Active', + value: 'active', + }, + { + name: 'Unsubscribed', + value: 'unsubscribed', + }, + { + name: 'Unconfirmed', + value: 'unconfirmed', + }, + ], + default: '', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* subscriber:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Subscriber ID', + name: 'subscriberId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'subscriber', + ], + operation: [ + 'delete', + ], + }, + }, + default: '', + description: 'ID or email of subscriber', + }, +/* -------------------------------------------------------------------------- */ +/* subscriber:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Subscriber ID', + name: 'subscriberId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'subscriber', + ], + operation: [ + 'get', + ], + }, + }, + default: '', + description: 'ID or email of subscriber', + }, +/* -------------------------------------------------------------------------- */ +/* subscriber:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'subscriber', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned 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: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'subscriber', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Active', + value: 'active', + }, + { + name: 'Unsubscribed', + value: 'unsubscribed', + }, + { + name: 'Unconfirmed', + value: 'unconfirmed', + }, + ], + default: '', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/MailerLite/mailerLite.png b/packages/nodes-base/nodes/MailerLite/mailerLite.png new file mode 100644 index 000000000..fde7f6d6b Binary files /dev/null and b/packages/nodes-base/nodes/MailerLite/mailerLite.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 9dfdac592..49e930fa7 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -103,6 +103,7 @@ "dist/credentials/JotFormApi.credentials.js", "dist/credentials/KeapOAuth2Api.credentials.js", "dist/credentials/LinkedInOAuth2Api.credentials.js", + "dist/credentials/MailerLiteApi.credentials.js", "dist/credentials/MailchimpApi.credentials.js", "dist/credentials/MailchimpOAuth2Api.credentials.js", "dist/credentials/MailgunApi.credentials.js", @@ -300,6 +301,8 @@ "dist/nodes/Keap/Keap.node.js", "dist/nodes/Keap/KeapTrigger.node.js", "dist/nodes/LinkedIn/LinkedIn.node.js", + "dist/nodes/MailerLite/MailerLite.node.js", + "dist/nodes/MailerLite/MailerLiteTrigger.node.js", "dist/nodes/Mailchimp/Mailchimp.node.js", "dist/nodes/Mailchimp/MailchimpTrigger.node.js", "dist/nodes/Mailgun/Mailgun.node.js",