From 7be61e2f23e10c19a58d40cf6c19ee5069700e37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 4 Mar 2021 06:25:47 -0300 Subject: [PATCH] :sparkles: Add Lemlist node (#1506) * :tada: Register node and credentials * :zap: Add credentials file * :art: Add SVG icon * :zap: Add generic functions file * :zap: Add preliminary node stub * :zap: Add resource description stubs * :zap: Extract get CSV from getAll * :zap: Implement lead:create * :zap: Implement all lead operations * :zap: Implement unsubscribe:create and delete * :zap: Preload campaigns * :fire: Remove logging * :fire: Remove operation per feedback * :art: Prettify error message * :zap: Implement unsubscribe:getAll * :zap: Add trigger and small improvements * :zap: Minor improvement and fixes Co-authored-by: ricardo Co-authored-by: Jan Oberhauser --- .../credentials/LemlistApi.credentials.ts | 18 + .../nodes/Lemlist/GenericFunctions.ts | 93 +++++ .../nodes-base/nodes/Lemlist/Lemlist.node.ts | 337 ++++++++++++++++++ .../nodes/Lemlist/LemlistTrigger.node.ts | 175 +++++++++ .../descriptions/ActivityDescription.ts | 140 ++++++++ .../descriptions/CampaignDescription.ts | 73 ++++ .../Lemlist/descriptions/LeadDescription.ts | 234 ++++++++++++ .../Lemlist/descriptions/TeamDescription.ts | 33 ++ .../descriptions/UnsubscribeDescription.ts | 123 +++++++ .../nodes/Lemlist/descriptions/index.ts | 5 + packages/nodes-base/nodes/Lemlist/lemlist.svg | 23 ++ packages/nodes-base/package.json | 3 + 12 files changed, 1257 insertions(+) create mode 100644 packages/nodes-base/credentials/LemlistApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Lemlist/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Lemlist/Lemlist.node.ts create mode 100644 packages/nodes-base/nodes/Lemlist/LemlistTrigger.node.ts create mode 100644 packages/nodes-base/nodes/Lemlist/descriptions/ActivityDescription.ts create mode 100644 packages/nodes-base/nodes/Lemlist/descriptions/CampaignDescription.ts create mode 100644 packages/nodes-base/nodes/Lemlist/descriptions/LeadDescription.ts create mode 100644 packages/nodes-base/nodes/Lemlist/descriptions/TeamDescription.ts create mode 100644 packages/nodes-base/nodes/Lemlist/descriptions/UnsubscribeDescription.ts create mode 100644 packages/nodes-base/nodes/Lemlist/descriptions/index.ts create mode 100644 packages/nodes-base/nodes/Lemlist/lemlist.svg diff --git a/packages/nodes-base/credentials/LemlistApi.credentials.ts b/packages/nodes-base/credentials/LemlistApi.credentials.ts new file mode 100644 index 0000000000..348530f758 --- /dev/null +++ b/packages/nodes-base/credentials/LemlistApi.credentials.ts @@ -0,0 +1,18 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class LemlistApi implements ICredentialType { + name = 'lemlistApi'; + displayName = 'Lemlist API'; + documentationUrl = 'lemlist'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Lemlist/GenericFunctions.ts b/packages/nodes-base/nodes/Lemlist/GenericFunctions.ts new file mode 100644 index 0000000000..1618f50b07 --- /dev/null +++ b/packages/nodes-base/nodes/Lemlist/GenericFunctions.ts @@ -0,0 +1,93 @@ +import { + IExecuteFunctions, + IHookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, +} from 'n8n-workflow'; + +import { + OptionsWithUri, +} from 'request'; + +/** + * Make an authenticated API request to Lemlist. + */ +export async function lemlistApiRequest( + this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, + method: string, + endpoint: string, + body: IDataObject = {}, + qs: IDataObject = {}, + option: IDataObject = {}, +) { + + const { apiKey } = this.getCredentials('lemlistApi') as { + apiKey: string, + }; + + const encodedApiKey = Buffer.from(':' + apiKey).toString('base64'); + + const options: OptionsWithUri = { + headers: { + 'user-agent': 'n8n', + 'Authorization': `Basic ${encodedApiKey}`, + }, + method, + uri: `https://api.lemlist.com/api${endpoint}`, + qs, + body, + json: true, + }; + + if (!Object.keys(body).length) { + delete options.body; + } + + if (!Object.keys(qs).length) { + delete options.qs; + } + + if (Object.keys(option)) { + Object.assign(options, option); + } + + try { + return await this.helpers.request!(options); + } catch (error) { + + if (error?.response?.body) { + throw new Error(`Lemlist error response [${error.statusCode}]: ${error?.response?.body}`); + } + + throw error; + } +} + +/** + * Make an authenticated API request to Lemlist and return all results. + */ +export async function lemlistApiRequestAllItems( + this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions, + method: string, + endpoint: string, +) { + const returnData: IDataObject[] = []; + + let responseData; + const qs: IDataObject = {}; + + qs.limit = 100; + qs.offset = 0; + + do { + responseData = await lemlistApiRequest.call(this, method, endpoint, {}, qs); + returnData.push(...responseData); + qs.offset += qs.limit; + } while ( + responseData.length !== 0 + ); + return returnData; +} diff --git a/packages/nodes-base/nodes/Lemlist/Lemlist.node.ts b/packages/nodes-base/nodes/Lemlist/Lemlist.node.ts new file mode 100644 index 0000000000..8f5ec3d859 --- /dev/null +++ b/packages/nodes-base/nodes/Lemlist/Lemlist.node.ts @@ -0,0 +1,337 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + activityFields, + activityOperations, + campaignFields, + campaignOperations, + leadFields, + leadOperations, + teamFields, + teamOperations, + unsubscribeFields, + unsubscribeOperations, +} from './descriptions'; + +import { + lemlistApiRequest, + lemlistApiRequestAllItems, +} from './GenericFunctions'; + +import { + isEmpty, + omit, +} from 'lodash'; + +export class Lemlist implements INodeType { + description: INodeTypeDescription = { + displayName: 'Lemlist', + name: 'lemlist', + icon: 'file:lemlist.svg', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume the Lemlist API', + defaults: { + name: 'Lemlist', + color: '#4d19ff', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'lemlistApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Activity', + value: 'activity', + }, + { + name: 'Campaign', + value: 'campaign', + }, + { + name: 'Lead', + value: 'lead', + }, + { + name: 'Team', + value: 'team', + }, + { + name: 'Unsubscribes', + value: 'unsubscribe', + }, + ], + default: 'activity', + description: 'Resource to consume', + }, + ...activityOperations, + ...activityFields, + ...campaignOperations, + ...campaignFields, + ...leadOperations, + ...leadFields, + ...teamOperations, + ...teamFields, + ...unsubscribeOperations, + ...unsubscribeFields, + ], + }; + + methods = { + loadOptions: { + async getCampaigns(this: ILoadOptionsFunctions) { + const campaigns = await lemlistApiRequest.call(this, 'GET', '/campaigns'); + return campaigns.map(({ _id, name }: { _id: string, name: string }) => ({ + name, + value: _id, + })); + }, + }, + }; + + async execute(this: IExecuteFunctions) { + const items = this.getInputData(); + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + let responseData; + const returnData: IDataObject[] = []; + + for (let i = 0; i < items.length; i++) { + + try { + + if (resource === 'activity') { + + // ********************************************************************* + // activity + // ********************************************************************* + + if (operation === 'getAll') { + + // ---------------------------------- + // activity: getAll + // ---------------------------------- + + // https://developer.lemlist.com/#activities + + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + + const qs = {} as IDataObject; + const filters = this.getNodeParameter('filters', i); + + if (!isEmpty(filters)) { + Object.assign(qs, filters); + } + + responseData = await lemlistApiRequest.call(this, 'GET', '/activities', {}, qs); + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', 0) as number; + responseData = responseData.slice(0, limit); + } + + } + + } else if (resource === 'campaign') { + + // ********************************************************************* + // campaign + // ********************************************************************* + + if (operation === 'getAll') { + + // ---------------------------------- + // campaign: getAll + // ---------------------------------- + + // https://developer.lemlist.com/#list-all-campaigns + + responseData = await lemlistApiRequest.call(this, 'GET', '/campaigns'); + + const returnAll = this.getNodeParameter('returnAll', i); + + if (!returnAll) { + const limit = this.getNodeParameter('limit', i); + responseData = responseData.slice(0, limit); + } + } + + } else if (resource === 'lead') { + + // ********************************************************************* + // lead + // ********************************************************************* + + if (operation === 'create') { + + // ---------------------------------- + // lead: create + // ---------------------------------- + + // https://developer.lemlist.com/#add-a-lead-in-a-campaign + + const qs = {} as IDataObject; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (additionalFields.deduplicate !== undefined) { + qs.deduplicate = additionalFields.deduplicate; + } + + const body = {} as IDataObject; + + const remainingAdditionalFields = omit(additionalFields, 'deduplicate'); + + if (!isEmpty(remainingAdditionalFields)) { + Object.assign(body, remainingAdditionalFields); + } + + const campaignId = this.getNodeParameter('campaignId', i); + const email = this.getNodeParameter('email', i); + const endpoint = `/campaigns/${campaignId}/leads/${email}`; + + responseData = await lemlistApiRequest.call(this, 'POST', endpoint, body, qs); + + } else if (operation === 'delete') { + + // ---------------------------------- + // lead: delete + // ---------------------------------- + + // https://developer.lemlist.com/#delete-a-lead-from-a-campaign + + const campaignId = this.getNodeParameter('campaignId', i); + const email = this.getNodeParameter('email', i); + const endpoint = `/campaigns/${campaignId}/leads/${email}`; + responseData = await lemlistApiRequest.call(this, 'DELETE', endpoint, {}, { action: 'remove' }); + + } else if (operation === 'get') { + + // ---------------------------------- + // lead: get + // ---------------------------------- + + // https://developer.lemlist.com/#get-a-specific-lead-by-email + + const email = this.getNodeParameter('email', i); + responseData = await lemlistApiRequest.call(this, 'GET', `/leads/${email}`); + + } else if (operation === 'unsubscribe') { + + // ---------------------------------- + // lead: unsubscribe + // ---------------------------------- + + // https://developer.lemlist.com/#unsubscribe-a-lead-from-a-campaign + + const campaignId = this.getNodeParameter('campaignId', i); + const email = this.getNodeParameter('email', i); + const endpoint = `/campaigns/${campaignId}/leads/${email}`; + responseData = await lemlistApiRequest.call(this, 'DELETE', endpoint); + + } + + } else if (resource === 'team') { + + // ********************************************************************* + // team + // ********************************************************************* + + if (operation === 'get') { + + // ---------------------------------- + // team: get + // ---------------------------------- + + // https://developer.lemlist.com/#team + + responseData = await lemlistApiRequest.call(this, 'GET', '/team'); + + } + + } else if (resource === 'unsubscribe') { + + // ********************************************************************* + // unsubscribe + // ********************************************************************* + + if (operation === 'add') { + + // ---------------------------------- + // unsubscribe: Add + // ---------------------------------- + + // https://developer.lemlist.com/#add-an-email-address-in-the-unsubscribes + + const email = this.getNodeParameter('email', i); + responseData = await lemlistApiRequest.call(this, 'POST', `/unsubscribes/${email}`); + + } else if (operation === 'delete') { + + // ---------------------------------- + // unsubscribe: delete + // ---------------------------------- + + // https://developer.lemlist.com/#delete-an-email-address-from-the-unsubscribes + + const email = this.getNodeParameter('email', i); + responseData = await lemlistApiRequest.call(this, 'DELETE', `/unsubscribes/${email}`); + + } else if (operation === 'getAll') { + + // ---------------------------------- + // unsubscribe: getAll + // ---------------------------------- + + // https://developer.lemlist.com/#list-all-unsubscribes + + const returnAll = this.getNodeParameter('returnAll', i); + + if (returnAll) { + responseData = await lemlistApiRequestAllItems.call(this, 'GET', '/unsubscribes'); + } else { + const qs = { + limit: this.getNodeParameter('limit', i) as number, + }; + responseData = await lemlistApiRequest.call(this, 'GET', '/unsubscribes', {}, qs); + } + } + } + + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.toString() }); + continue; + } + + throw error; + } + + Array.isArray(responseData) + ? returnData.push(...responseData) + : returnData.push(responseData); + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Lemlist/LemlistTrigger.node.ts b/packages/nodes-base/nodes/Lemlist/LemlistTrigger.node.ts new file mode 100644 index 0000000000..bf671c1d5b --- /dev/null +++ b/packages/nodes-base/nodes/Lemlist/LemlistTrigger.node.ts @@ -0,0 +1,175 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeType, + INodeTypeDescription, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + lemlistApiRequest, +} from './GenericFunctions'; + +export class LemlistTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Lemlist Trigger', + name: 'lemlistTrigger', + icon: 'file:lemlist.svg', + group: ['trigger'], + version: 1, + subtitle: '={{$parameter["event"]}}', + description: 'Handle Lemlist events via webhooks', + defaults: { + name: 'Lemlist Trigger', + color: '#4d19ff', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'lemlistApi', + required: true, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Event', + name: 'event', + type: 'options', + required: true, + default: '', + options: [ + { + name: 'Email Bounced', + value: 'emailsBounced', + }, + { + name: 'Email Clicked', + value: 'emailsClicked', + }, + { + name: 'Email Opened', + value: 'emailsOpened', + }, + { + name: 'Email Replied', + value: 'emailsReplied', + }, + { + name: 'Email Send Failed', + value: 'emailsSendFailed', + }, + { + name: 'Email Sent', + value: 'emailsSent', + }, + { + name: 'Email Unsubscribed', + value: 'emailsUnsubscribed', + }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Campaing ID', + name: 'campaignId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + default: '', + description: ` We'll call this hook only for this campaignId.`, + }, + { + displayName: 'Is First', + name: 'isFirst', + type: 'boolean', + default: false, + description: `We'll call this hook only the first time this activity happened.`, + }, + ], + }, + ], + }; + + methods = { + loadOptions: { + async getCampaigns(this: ILoadOptionsFunctions) { + const campaigns = await lemlistApiRequest.call(this, 'GET', '/campaigns'); + return campaigns.map(({ _id, name }: { _id: string, name: string }) => ({ + name, + value: _id, + })); + }, + }, + }; + // @ts-ignore + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhooks = await lemlistApiRequest.call(this, 'GET', '/hooks'); + for (const webhook of webhooks) { + if (webhook.targetUrl === webhookUrl) { + await lemlistApiRequest.call(this, 'DELETE', `/hooks/${webhookData.webhookId}`); + return false; + } + } + return false; + }, + async create(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + const options = this.getNodeParameter('options') as IDataObject; + const event = this.getNodeParameter('event') as string[]; + const body: IDataObject = { + targetUrl: webhookUrl, + event, + }; + Object.assign(body, options); + const webhook = await lemlistApiRequest.call(this, 'POST', '/hooks', body); + webhookData.webhookId = webhook._id; + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + try { + await lemlistApiRequest.call(this, 'DELETE', `/hooks/${webhookData.webhookId}`); + } catch (error) { + return false; + } + delete webhookData.webhookId; + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const req = this.getRequestObject(); + return { + workflowData: [ + this.helpers.returnJsonArray(req.body), + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Lemlist/descriptions/ActivityDescription.ts b/packages/nodes-base/nodes/Lemlist/descriptions/ActivityDescription.ts new file mode 100644 index 0000000000..c55086c024 --- /dev/null +++ b/packages/nodes-base/nodes/Lemlist/descriptions/ActivityDescription.ts @@ -0,0 +1,140 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const activityOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'getAll', + description: 'Operation to perform', + options: [ + { + name: 'Get All', + value: 'getAll', + }, + ], + displayOptions: { + show: { + resource: [ + 'activity', + ], + }, + }, + }, +] as INodeProperties[]; + +export const activityFields = [ + // ---------------------------------- + // activity: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'activity', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 5, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + displayOptions: { + show: { + resource: [ + 'activity', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'activity', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Campaign ID', + name: 'campaignId', + type: 'options', + required: true, + default: '', + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + description: 'ID of the campaign to retrieve activity for.', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + default: 'emailsOpened', + description: 'Type of activity to retrieve.', + options: [ + { + name: 'Emails Bounced', + value: 'emailsBounced', + }, + { + name: 'Emails Clicked', + value: 'emailsClicked', + }, + { + name: 'Emails Opened', + value: 'emailsOpened', + }, + { + name: 'Emails Replied', + value: 'emailsReplied', + }, + { + name: 'Emails Send Failed', + value: 'emailsSendFailed', + }, + { + name: 'Emails Sent', + value: 'emailsSent', + }, + { + name: 'Emails Unsubscribed', + value: 'emailsUnsubscribed', + }, + ], + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Lemlist/descriptions/CampaignDescription.ts b/packages/nodes-base/nodes/Lemlist/descriptions/CampaignDescription.ts new file mode 100644 index 0000000000..f1702efdcc --- /dev/null +++ b/packages/nodes-base/nodes/Lemlist/descriptions/CampaignDescription.ts @@ -0,0 +1,73 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const campaignOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'getAll', + description: 'Operation to perform', + options: [ + { + name: 'Get All', + value: 'getAll', + }, + ], + displayOptions: { + show: { + resource: [ + 'campaign', + ], + }, + }, + }, +] as INodeProperties[]; + +export const campaignFields = [ + // ---------------------------------- + // campaign: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'campaign', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 5, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + displayOptions: { + show: { + resource: [ + 'campaign', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Lemlist/descriptions/LeadDescription.ts b/packages/nodes-base/nodes/Lemlist/descriptions/LeadDescription.ts new file mode 100644 index 0000000000..9c4fe039e8 --- /dev/null +++ b/packages/nodes-base/nodes/Lemlist/descriptions/LeadDescription.ts @@ -0,0 +1,234 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const leadOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'create', + description: 'Operation to perform', + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Delete', + value: 'delete', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Unsubscribe', + value: 'unsubscribe', + }, + ], + displayOptions: { + show: { + resource: [ + 'lead', + ], + }, + }, + }, +] as INodeProperties[]; + +export const leadFields = [ + // ---------------------------------- + // lead: create + // ---------------------------------- + { + displayName: 'Campaign ID', + name: 'campaignId', + type: 'options', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + description: 'ID of the campaign to create the lead under.', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'Email of the lead to create.', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Company Name', + name: 'companyName', + type: 'string', + default: '', + description: 'Company name of the lead to create.', + }, + { + displayName: 'Deduplicate', + name: 'deduplicate', + type: 'boolean', + default: false, + description: 'Do not insert if this email is already present in another campaign.', + }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: 'First name of the lead to create.', + }, + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + default: '', + description: 'Last name of the lead to create.', + }, + ], + }, + + // ---------------------------------- + // lead: delete + // ---------------------------------- + { + displayName: 'Campaign ID', + name: 'campaignId', + type: 'options', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + description: 'ID of the campaign to remove the lead from.', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'delete', + ], + }, + }, + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'Email of the lead to delete.', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------- + // lead: get + // ---------------------------------- + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'Email of the lead to retrieve.', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------- + // lead: unsubscribe + // ---------------------------------- + { + displayName: 'Campaign ID', + name: 'campaignId', + type: 'options', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + description: 'ID of the campaign to unsubscribe the lead from.', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'unsubscribe', + ], + }, + }, + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'Email of the lead to unsubscribe.', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'unsubscribe', + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Lemlist/descriptions/TeamDescription.ts b/packages/nodes-base/nodes/Lemlist/descriptions/TeamDescription.ts new file mode 100644 index 0000000000..c09f51a582 --- /dev/null +++ b/packages/nodes-base/nodes/Lemlist/descriptions/TeamDescription.ts @@ -0,0 +1,33 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const teamOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Get', + value: 'get', + }, + ], + displayOptions: { + show: { + resource: [ + 'team', + ], + }, + }, + }, +] as INodeProperties[]; + +export const teamFields = [ + // ---------------------------------- + // team: get + // ---------------------------------- + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Lemlist/descriptions/UnsubscribeDescription.ts b/packages/nodes-base/nodes/Lemlist/descriptions/UnsubscribeDescription.ts new file mode 100644 index 0000000000..d35ab0e9ce --- /dev/null +++ b/packages/nodes-base/nodes/Lemlist/descriptions/UnsubscribeDescription.ts @@ -0,0 +1,123 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const unsubscribeOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'add', + description: 'Operation to perform', + options: [ + { + name: 'Add', + value: 'add', + }, + { + name: 'Delete', + value: 'delete', + }, + { + name: 'Get All', + value: 'getAll', + }, + ], + displayOptions: { + show: { + resource: [ + 'unsubscribe', + ], + }, + }, + }, +] as INodeProperties[]; + +export const unsubscribeFields = [ + // ---------------------------------- + // unsubscribe: add + // ---------------------------------- + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'Email to add to the unsubscribes.', + displayOptions: { + show: { + resource: [ + 'unsubscribe', + ], + operation: [ + 'add', + ], + }, + }, + }, + + // ---------------------------------- + // unsubscribe: delete + // ---------------------------------- + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'Email to delete from the unsubscribes.', + displayOptions: { + show: { + resource: [ + 'unsubscribe', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------- + // unsubscribe: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'unsubscribe', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 5, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + displayOptions: { + show: { + resource: [ + 'unsubscribe', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Lemlist/descriptions/index.ts b/packages/nodes-base/nodes/Lemlist/descriptions/index.ts new file mode 100644 index 0000000000..81c322f129 --- /dev/null +++ b/packages/nodes-base/nodes/Lemlist/descriptions/index.ts @@ -0,0 +1,5 @@ +export * from './ActivityDescription'; +export * from './CampaignDescription'; +export * from './LeadDescription'; +export * from './TeamDescription'; +export * from './UnsubscribeDescription'; diff --git a/packages/nodes-base/nodes/Lemlist/lemlist.svg b/packages/nodes-base/nodes/Lemlist/lemlist.svg new file mode 100644 index 0000000000..667de83eae --- /dev/null +++ b/packages/nodes-base/nodes/Lemlist/lemlist.svg @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 79f355abf3..15324739b5 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -128,6 +128,7 @@ "dist/credentials/JotFormApi.credentials.js", "dist/credentials/Kafka.credentials.js", "dist/credentials/KeapOAuth2Api.credentials.js", + "dist/credentials/LemlistApi.credentials.js", "dist/credentials/LineNotifyOAuth2Api.credentials.js", "dist/credentials/LingvaNexApi.credentials.js", "dist/credentials/LinkedInOAuth2Api.credentials.js", @@ -387,6 +388,8 @@ "dist/nodes/Kafka/KafkaTrigger.node.js", "dist/nodes/Keap/Keap.node.js", "dist/nodes/Keap/KeapTrigger.node.js", + "dist/nodes/Lemlist/Lemlist.node.js", + "dist/nodes/Lemlist/LemlistTrigger.node.js", "dist/nodes/Line/Line.node.js", "dist/nodes/LingvaNex/LingvaNex.node.js", "dist/nodes/LinkedIn/LinkedIn.node.js",