diff --git a/packages/nodes-base/nodes/Lemlist/GenericFunctions.ts b/packages/nodes-base/nodes/Lemlist/GenericFunctions.ts index e95bebe507..be72356690 100644 --- a/packages/nodes-base/nodes/Lemlist/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Lemlist/GenericFunctions.ts @@ -59,13 +59,23 @@ export async function lemlistApiRequestAllItems( qs.limit = 100; qs.offset = 0; - - do { - responseData = await lemlistApiRequest.call(this, method, endpoint, {}, qs); - returnData.push(...(responseData as IDataObject[])); - qs.offset += qs.limit; - } while (responseData.length !== 0); - return returnData; + //when using v2, the pagination is different + if (qs.version && qs.version === 'v2') { + qs.page = 1; + do { + responseData = await lemlistApiRequest.call(this, method, endpoint, {}, qs); + returnData.push(...(responseData as IDataObject[])); + qs.page++; + } while (responseData.totalPage && qs.page < responseData.totalPage); + return returnData; + } else { + do { + responseData = await lemlistApiRequest.call(this, method, endpoint, {}, qs); + returnData.push(...(responseData as IDataObject[])); + qs.offset += qs.limit; + } while (responseData.length !== 0); + return returnData; + } } export function getEvents() { diff --git a/packages/nodes-base/nodes/Lemlist/Lemlist.node.ts b/packages/nodes-base/nodes/Lemlist/Lemlist.node.ts index 9e55d83759..e9b557ec04 100644 --- a/packages/nodes-base/nodes/Lemlist/Lemlist.node.ts +++ b/packages/nodes-base/nodes/Lemlist/Lemlist.node.ts @@ -1,314 +1,25 @@ -import { - type IExecuteFunctions, - type IDataObject, - type ILoadOptionsFunctions, - type INodeExecutionData, - type INodeType, - type INodeTypeDescription, - NodeConnectionType, -} from 'n8n-workflow'; +import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow'; +import { VersionedNodeType } from 'n8n-workflow'; -import isEmpty from 'lodash/isEmpty'; -import omit from 'lodash/omit'; -import { - activityFields, - activityOperations, - campaignFields, - campaignOperations, - leadFields, - leadOperations, - teamFields, - teamOperations, - unsubscribeFields, - unsubscribeOperations, -} from './descriptions'; +import { LemlistV1 } from './v1/LemlistV1.node'; +import { LemlistV2 } from './v2/LemlistV2.node'; -import { lemlistApiRequest, lemlistApiRequestAllItems } from './GenericFunctions'; +export class Lemlist extends VersionedNodeType { + constructor() { + const baseDescription: INodeTypeBaseDescription = { + displayName: 'Lemlist', + name: 'lemlist', + icon: 'file:lemlist.svg', + group: ['transform'], + defaultVersion: 2, + description: 'Consume the Lemlist API', + }; -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', - }, - inputs: [NodeConnectionType.Main], - outputs: [NodeConnectionType.Main], - credentials: [ - { - name: 'lemlistApi', - required: true, - }, - ], - properties: [ - { - displayName: 'Resource', - name: 'resource', - type: 'options', - noDataExpression: true, - options: [ - { - name: 'Activity', - value: 'activity', - }, - { - name: 'Campaign', - value: 'campaign', - }, - { - name: 'Lead', - value: 'lead', - }, - { - name: 'Team', - value: 'team', - }, - { - name: 'Unsubscribe', - value: 'unsubscribe', - }, - ], - default: 'activity', - }, - ...activityOperations, - ...activityFields, - ...campaignOperations, - ...campaignFields, - ...leadOperations, - ...leadFields, - ...teamOperations, - ...teamFields, - ...unsubscribeOperations, - ...unsubscribeFields, - ], - }; + const nodeVersions: IVersionedNodeType['nodeVersions'] = { + 1: new LemlistV1(baseDescription), + 2: new LemlistV2(baseDescription), + }; - 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); - const operation = this.getNodeParameter('operation', 0); - - let responseData; - const returnData: INodeExecutionData[] = []; - - 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', i); - - const qs = {} as IDataObject; - const filters = this.getNodeParameter('filters', i); - - if (!isEmpty(filters)) { - Object.assign(qs, filters); - } - - if (returnAll) { - responseData = await lemlistApiRequestAllItems.call(this, 'GET', '/activities', qs); - } else { - qs.limit = this.getNodeParameter('limit', i); - responseData = await lemlistApiRequest.call(this, 'GET', '/activities', {}, qs); - } - } - } else if (resource === 'campaign') { - // ********************************************************************* - // campaign - // ********************************************************************* - - if (operation === 'getAll') { - // ---------------------------------- - // campaign: getAll - // ---------------------------------- - - // https://developer.lemlist.com/#list-all-campaigns - - const returnAll = this.getNodeParameter('returnAll', i); - - if (returnAll) { - responseData = await lemlistApiRequestAllItems.call(this, 'GET', '/campaigns', {}); - } else { - const qs = { - limit: this.getNodeParameter('limit', i), - }; - responseData = await lemlistApiRequest.call(this, 'GET', '/campaigns', {}, qs); - } - } - } 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); - - 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), - }; - responseData = await lemlistApiRequest.call(this, 'GET', '/unsubscribes', {}, qs); - } - } - } - } 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/Lemlist/test/GenericFunctions.test.ts b/packages/nodes-base/nodes/Lemlist/test/GenericFunctions.test.ts new file mode 100644 index 0000000000..9b8e1890f5 --- /dev/null +++ b/packages/nodes-base/nodes/Lemlist/test/GenericFunctions.test.ts @@ -0,0 +1,115 @@ +import type { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IDataObject, + IHttpRequestMethods, +} from 'n8n-workflow'; +import { lemlistApiRequest, lemlistApiRequestAllItems, getEvents } from '../GenericFunctions'; + +describe('GenericFunctions', () => { + describe('lemlistApiRequest', () => { + const mockThis = { + helpers: { + requestWithAuthentication: jest.fn(), + }, + } as unknown as IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions; + + it('should make an authenticated API request to Lemlist', async () => { + const method: IHttpRequestMethods = 'GET'; + const endpoint = '/test-endpoint'; + const body: IDataObject = { key: 'value' }; + const qs: IDataObject = { query: 'value' }; + const option: IDataObject = { headers: {} }; + + await lemlistApiRequest.call(mockThis, method, endpoint, body, qs, option); + + expect(mockThis.helpers.requestWithAuthentication).toHaveBeenCalledWith('lemlistApi', { + headers: {}, + method: 'GET', + uri: 'https://api.lemlist.com/api/test-endpoint', + qs: { query: 'value' }, + body: { key: 'value' }, + json: true, + }); + }); + }); + + describe('lemlistApiRequestAllItems', () => { + const mockThis = { + helpers: { + requestWithAuthentication: jest + .fn() + .mockResolvedValue([{ id: 'cam_A1B2C3D4E5F6G7H8I9' }, { id: 'cam_A1B2C3D4E5F6G7H8I8' }]), + }, + } as unknown as IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions; + + it('should return all results', async () => { + const method: IHttpRequestMethods = 'GET'; + const endpoint = '/test-endpoint'; + const qs: IDataObject = {}; + qs.version = 'v2'; + + const result = await lemlistApiRequestAllItems.call(mockThis, method, endpoint, qs); + + expect(result).toEqual([{ id: 'cam_A1B2C3D4E5F6G7H8I9' }, { id: 'cam_A1B2C3D4E5F6G7H8I8' }]); + }); + }); + + describe('getEvents', () => { + it('should return a list of events with capitalized names', () => { + const expectedEvents = [ + { name: '*', value: '*' }, + { name: 'Contacted', value: 'contacted' }, + { name: 'Hooked', value: 'hooked' }, + { name: 'Attracted', value: 'attracted' }, + { name: 'Warmed', value: 'warmed' }, + { name: 'Interested', value: 'interested' }, + { name: 'Skipped', value: 'skipped' }, + { name: 'Not Interested', value: 'notInterested' }, + { name: 'Emails Sent', value: 'emailsSent' }, + { name: 'Emails Opened', value: 'emailsOpened' }, + { name: 'Emails Clicked', value: 'emailsClicked' }, + { name: 'Emails Replied', value: 'emailsReplied' }, + { name: 'Emails Bounced', value: 'emailsBounced' }, + { name: 'Emails Send Failed', value: 'emailsSendFailed' }, + { name: 'Emails Failed', value: 'emailsFailed' }, + { name: 'Emails Unsubscribed', value: 'emailsUnsubscribed' }, + { name: 'Emails Interested', value: 'emailsInterested' }, + { name: 'Emails Not Interested', value: 'emailsNotInterested' }, + { name: 'Opportunities Done', value: 'opportunitiesDone' }, + { name: 'Aircall Created', value: 'aircallCreated' }, + { name: 'Aircall Ended', value: 'aircallEnded' }, + { name: 'Aircall Done', value: 'aircallDone' }, + { name: 'Aircall Interested', value: 'aircallInterested' }, + { name: 'Aircall Not Interested', value: 'aircallNotInterested' }, + { name: 'Api Done', value: 'apiDone' }, + { name: 'Api Interested', value: 'apiInterested' }, + { name: 'Api Not Interested', value: 'apiNotInterested' }, + { name: 'Api Failed', value: 'apiFailed' }, + { name: 'Linkedin Visit Done', value: 'linkedinVisitDone' }, + { name: 'Linkedin Visit Failed', value: 'linkedinVisitFailed' }, + { name: 'Linkedin Invite Done', value: 'linkedinInviteDone' }, + { name: 'Linkedin Invite Failed', value: 'linkedinInviteFailed' }, + { name: 'Linkedin Invite Accepted', value: 'linkedinInviteAccepted' }, + { name: 'Linkedin Replied', value: 'linkedinReplied' }, + { name: 'Linkedin Sent', value: 'linkedinSent' }, + { name: 'Linkedin Voice Note Done', value: 'linkedinVoiceNoteDone' }, + { name: 'Linkedin Voice Note Failed', value: 'linkedinVoiceNoteFailed' }, + { name: 'Linkedin Interested', value: 'linkedinInterested' }, + { name: 'Linkedin Not Interested', value: 'linkedinNotInterested' }, + { name: 'Linkedin Send Failed', value: 'linkedinSendFailed' }, + { name: 'Manual Interested', value: 'manualInterested' }, + { name: 'Manual Not Interested', value: 'manualNotInterested' }, + { name: 'Paused', value: 'paused' }, + { name: 'Resumed', value: 'resumed' }, + { name: 'Custom Domain Errors', value: 'customDomainErrors' }, + { name: 'Connection Issue', value: 'connectionIssue' }, + { name: 'Send Limit Reached', value: 'sendLimitReached' }, + { name: 'Lemwarm Paused', value: 'lemwarmPaused' }, + ]; + const result = getEvents(); + expect(result).toEqual(expectedEvents); + }); + }); +}); diff --git a/packages/nodes-base/nodes/Lemlist/v1/LemlistV1.node.ts b/packages/nodes-base/nodes/Lemlist/v1/LemlistV1.node.ts new file mode 100644 index 0000000000..a3ffc7a21f --- /dev/null +++ b/packages/nodes-base/nodes/Lemlist/v1/LemlistV1.node.ts @@ -0,0 +1,322 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ +import { + type IExecuteFunctions, + type IDataObject, + type ILoadOptionsFunctions, + type INodeExecutionData, + type INodeType, + type INodeTypeDescription, + type INodeTypeBaseDescription, + NodeConnectionType, +} from 'n8n-workflow'; + +import isEmpty from 'lodash/isEmpty'; +import omit from 'lodash/omit'; +import { lemlistApiRequest, lemlistApiRequestAllItems } from '../GenericFunctions'; +import { + activityFields, + activityOperations, + campaignFields, + campaignOperations, + leadFields, + leadOperations, + teamFields, + teamOperations, + unsubscribeFields, + unsubscribeOperations, +} from './descriptions'; +const versionDescription: 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', + }, + inputs: [NodeConnectionType.Main], + outputs: [NodeConnectionType.Main], + credentials: [ + { + name: 'lemlistApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Activity', + value: 'activity', + }, + { + name: 'Campaign', + value: 'campaign', + }, + { + name: 'Lead', + value: 'lead', + }, + { + name: 'Team', + value: 'team', + }, + { + name: 'Unsubscribe', + value: 'unsubscribe', + }, + ], + default: 'activity', + }, + ...activityOperations, + ...activityFields, + ...campaignOperations, + ...campaignFields, + ...leadOperations, + ...leadFields, + ...teamOperations, + ...teamFields, + ...unsubscribeOperations, + ...unsubscribeFields, + ], +}; +export class LemlistV1 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + ...versionDescription, + }; + } + + 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); + const operation = this.getNodeParameter('operation', 0); + + let responseData; + const returnData: INodeExecutionData[] = []; + + 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', i); + + const qs = {} as IDataObject; + const filters = this.getNodeParameter('filters', i); + + if (!isEmpty(filters)) { + Object.assign(qs, filters); + } + + if (returnAll) { + responseData = await lemlistApiRequestAllItems.call(this, 'GET', '/activities', qs); + } else { + qs.limit = this.getNodeParameter('limit', i); + responseData = await lemlistApiRequest.call(this, 'GET', '/activities', {}, qs); + } + } + } else if (resource === 'campaign') { + // ********************************************************************* + // campaign + // ********************************************************************* + + if (operation === 'getAll') { + // ---------------------------------- + // campaign: getAll + // ---------------------------------- + + // https://developer.lemlist.com/#list-all-campaigns + + const returnAll = this.getNodeParameter('returnAll', i); + + if (returnAll) { + responseData = await lemlistApiRequestAllItems.call(this, 'GET', '/campaigns', {}); + } else { + const qs = { + limit: this.getNodeParameter('limit', i), + }; + responseData = await lemlistApiRequest.call(this, 'GET', '/campaigns', {}, qs); + } + } + } 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); + + 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), + }; + responseData = await lemlistApiRequest.call(this, 'GET', '/unsubscribes', {}, qs); + } + } + } + } 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/Lemlist/descriptions/ActivityDescription.ts b/packages/nodes-base/nodes/Lemlist/v1/descriptions/ActivityDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Lemlist/descriptions/ActivityDescription.ts rename to packages/nodes-base/nodes/Lemlist/v1/descriptions/ActivityDescription.ts diff --git a/packages/nodes-base/nodes/Lemlist/descriptions/CampaignDescription.ts b/packages/nodes-base/nodes/Lemlist/v1/descriptions/CampaignDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Lemlist/descriptions/CampaignDescription.ts rename to packages/nodes-base/nodes/Lemlist/v1/descriptions/CampaignDescription.ts diff --git a/packages/nodes-base/nodes/Lemlist/descriptions/LeadDescription.ts b/packages/nodes-base/nodes/Lemlist/v1/descriptions/LeadDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Lemlist/descriptions/LeadDescription.ts rename to packages/nodes-base/nodes/Lemlist/v1/descriptions/LeadDescription.ts diff --git a/packages/nodes-base/nodes/Lemlist/descriptions/TeamDescription.ts b/packages/nodes-base/nodes/Lemlist/v1/descriptions/TeamDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Lemlist/descriptions/TeamDescription.ts rename to packages/nodes-base/nodes/Lemlist/v1/descriptions/TeamDescription.ts diff --git a/packages/nodes-base/nodes/Lemlist/descriptions/UnsubscribeDescription.ts b/packages/nodes-base/nodes/Lemlist/v1/descriptions/UnsubscribeDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Lemlist/descriptions/UnsubscribeDescription.ts rename to packages/nodes-base/nodes/Lemlist/v1/descriptions/UnsubscribeDescription.ts diff --git a/packages/nodes-base/nodes/Lemlist/descriptions/index.ts b/packages/nodes-base/nodes/Lemlist/v1/descriptions/index.ts similarity index 100% rename from packages/nodes-base/nodes/Lemlist/descriptions/index.ts rename to packages/nodes-base/nodes/Lemlist/v1/descriptions/index.ts diff --git a/packages/nodes-base/nodes/Lemlist/v2/LemlistV2.node.ts b/packages/nodes-base/nodes/Lemlist/v2/LemlistV2.node.ts new file mode 100644 index 0000000000..a88dbdbcc9 --- /dev/null +++ b/packages/nodes-base/nodes/Lemlist/v2/LemlistV2.node.ts @@ -0,0 +1,417 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ +import { + type IExecuteFunctions, + type IDataObject, + type ILoadOptionsFunctions, + type INodeExecutionData, + type INodeType, + type INodeTypeDescription, + type INodeTypeBaseDescription, + NodeConnectionType, +} from 'n8n-workflow'; + +import isEmpty from 'lodash/isEmpty'; +import omit from 'lodash/omit'; +import { lemlistApiRequest, lemlistApiRequestAllItems } from '../GenericFunctions'; +import { + activityFields, + activityOperations, + campaignFields, + campaignOperations, + enrichmentFields, + enrichmentOperations, + leadFields, + leadOperations, + teamFields, + teamOperations, + unsubscribeFields, + unsubscribeOperations, +} from './descriptions'; +const versionDescription: INodeTypeDescription = { + displayName: 'Lemlist', + name: 'lemlist', + icon: 'file:lemlist.svg', + group: ['transform'], + version: 2, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume the Lemlist API', + defaults: { + name: 'Lemlist', + }, + inputs: [NodeConnectionType.Main], + outputs: [NodeConnectionType.Main], + credentials: [ + { + name: 'lemlistApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Activity', + value: 'activity', + }, + { + name: 'Campaign', + value: 'campaign', + }, + { + name: 'Enrichment', + value: 'enrich', + }, + { + name: 'Lead', + value: 'lead', + }, + { + name: 'Team', + value: 'team', + }, + { + name: 'Unsubscribe', + value: 'unsubscribe', + }, + ], + default: 'activity', + }, + ...activityOperations, + ...activityFields, + ...campaignOperations, + ...campaignFields, + ...enrichmentOperations, + ...enrichmentFields, + ...leadOperations, + ...leadFields, + ...teamOperations, + ...teamFields, + ...unsubscribeOperations, + ...unsubscribeFields, + ], +}; +export class LemlistV2 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + ...versionDescription, + }; + } + + 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); + const operation = this.getNodeParameter('operation', 0); + + let responseData; + const returnData: INodeExecutionData[] = []; + + 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', i); + + const qs = {} as IDataObject; + const filters = this.getNodeParameter('filters', i); + + if (!isEmpty(filters)) { + Object.assign(qs, filters); + } + + if (returnAll) { + responseData = await lemlistApiRequestAllItems.call(this, 'GET', '/activities', qs); + } else { + qs.limit = this.getNodeParameter('limit', i); + responseData = await lemlistApiRequest.call(this, 'GET', '/activities', {}, qs); + } + } + } else if (resource === 'campaign') { + // ********************************************************************* + // campaign + // ********************************************************************* + + if (operation === 'getAll') { + // ---------------------------------- + // campaign: getAll + // ---------------------------------- + + // https://developer.lemlist.com/#32ab1bf9-9b2f-40ed-9bbd-0b8370fed3d9 + const qs = {} as IDataObject; + const filters = this.getNodeParameter('filters', i); + + if (!isEmpty(filters)) { + Object.assign(qs, filters); + } + const returnAll = this.getNodeParameter('returnAll', i); + + if (returnAll) { + responseData = await lemlistApiRequestAllItems.call(this, 'GET', '/campaigns', {}); + } else { + qs.limit = this.getNodeParameter('limit', i); + responseData = await lemlistApiRequest.call(this, 'GET', '/campaigns', {}, qs); + } + } else if (operation === 'getStats') { + // ---------------------------------- + // campaign: getStats + // ---------------------------------- + + // https://developer.lemlist.com/#0b5cc72c-c1c8-47d0-a086-32b1b63522e3 + const qs = {} as IDataObject; + + const campaignId = this.getNodeParameter('campaignId', i); + + qs.startDate = this.getNodeParameter('startDate', i); + qs.endDate = this.getNodeParameter('endDate', i); + qs.timezone = this.getNodeParameter('timezone', i); + responseData = await lemlistApiRequest.call( + this, + 'GET', + `/campaigns/${campaignId}/stats`, + {}, + qs, + ); + } + } 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); + + 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 (operation === 'getCredits') { + // ---------------------------------- + // team: getCredits + // ---------------------------------- + + // https://developer.lemlist.com/#c9af1cf3-8d3d-469e-a548-268b579d2cb3 + + responseData = await lemlistApiRequest.call(this, 'GET', '/team/credits'); + } + } 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), + }; + responseData = await lemlistApiRequest.call(this, 'GET', '/unsubscribes', {}, qs); + } + } + } else if (resource === 'enrich') { + // ********************************************************************* + // enrichment + // ********************************************************************* + + if (operation === 'get') { + // ---------------------------------- + // enrichment: get + // ---------------------------------- + + // https://developer.lemlist.com/#71b74cc3-8098-4389-b3c2-67a027df9407 + + const enrichId = this.getNodeParameter('enrichId', i); + + responseData = await lemlistApiRequest.call(this, 'GET', `/enrich/${enrichId}`); + } else if (operation === 'enrichLead') { + // https://developer.lemlist.com/#fe2a52fc-fa73-46d0-8b7d-395d9653bfd5 + const findEmail = this.getNodeParameter('findEmail', i); + const verifyEmail = this.getNodeParameter('verifyEmail', i); + const linkedinEnrichment = this.getNodeParameter('linkedinEnrichment', i); + const findPhone = this.getNodeParameter('findPhone', i); + const qs = {} as IDataObject; + + qs.findEmail = findEmail; + qs.verifyEmail = verifyEmail; + qs.linkedinEnrichment = linkedinEnrichment; + qs.findPhone = findPhone; + + const body = {} as IDataObject; + + const leadId = this.getNodeParameter('leadId', i); + const endpoint = `/leads/${leadId}/enrich/`; + + responseData = await lemlistApiRequest.call(this, 'POST', endpoint, body, qs); + } else if (operation === 'enrichPerson') { + // https://developer.lemlist.com/#4ba3d505-0bfa-4f36-8549-f3cb343786bf + const findEmail = this.getNodeParameter('findEmail', i); + const verifyEmail = this.getNodeParameter('verifyEmail', i); + const linkedinEnrichment = this.getNodeParameter('linkedinEnrichment', i); + const findPhone = this.getNodeParameter('findPhone', i); + const additionalFields = this.getNodeParameter('additionalFields', i); + const qs = {} as IDataObject; + if (!isEmpty(additionalFields)) { + Object.assign(qs, additionalFields); + } + qs.findEmail = findEmail; + qs.verifyEmail = verifyEmail; + qs.linkedinEnrichment = linkedinEnrichment; + qs.findPhone = findPhone; + + const body = {} as IDataObject; + + const endpoint = '/enrich/'; + + responseData = await lemlistApiRequest.call(this, 'POST', endpoint, body, qs); + } + } + } 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/Lemlist/v2/descriptions/ActivityDescription.ts b/packages/nodes-base/nodes/Lemlist/v2/descriptions/ActivityDescription.ts new file mode 100644 index 0000000000..98c06a1d53 --- /dev/null +++ b/packages/nodes-base/nodes/Lemlist/v2/descriptions/ActivityDescription.ts @@ -0,0 +1,301 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const activityOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + default: 'getAll', + options: [ + { + name: 'Get Many', + value: 'getAll', + action: 'Get many activities', + }, + ], + displayOptions: { + show: { + resource: ['activity'], + }, + }, + }, +]; + +export const activityFields: INodeProperties[] = [ + // ---------------------------------- + // activity: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + displayOptions: { + show: { + resource: ['activity'], + operation: ['getAll'], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 5, + description: 'Max 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 Name or ID', + name: 'campaignId', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + description: + 'ID of the campaign to retrieve activity for. Choose from the list, or specify an ID using an expression.', + }, + { + displayName: 'Is First', + name: 'isFirst', + type: 'boolean', + default: false, + }, + { + displayName: 'Lead ID', + name: 'leadId', + type: 'string', + default: '', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + default: 'emailsOpened', + description: 'Type of activity to retrieve', + options: [ + { + name: 'Aircall Created', + value: 'aircallCreated', + }, + { + name: 'Aircall Done', + value: 'aircallDone', + }, + { + name: 'Aircall Ended', + value: 'aircallEnded', + }, + { + name: 'Aircall Interested', + value: 'aircallInterested', + }, + { + name: 'Aircall Not Interested', + value: 'aircallNotInterested', + }, + { + name: 'Api Done', + value: 'apiDone', + }, + { + name: 'Api Failed', + value: 'apiFailed', + }, + { + name: 'Api Interested', + value: 'apiInterested', + }, + { + name: 'Api Not Interested', + value: 'apiNotInterested', + }, + { + name: 'Attracted', + value: 'attracted', + }, + { + name: 'Connection Issue', + value: 'connectionIssue', + }, + { + name: 'Contacted', + value: 'contacted', + }, + { + name: 'Custom Domain Errors', + value: 'customDomainErrors', + }, + { + name: 'Emails Bounced', + value: 'emailsBounced', + }, + { + name: 'Emails Clicked', + value: 'emailsClicked', + }, + { + name: 'Emails Failed', + value: 'emailsFailed', + }, + { + name: 'Emails Interested', + value: 'emailsInterested', + }, + { + name: 'Emails Not Interested', + value: 'emailsNotInterested', + }, + { + 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', + }, + { + name: 'Hooked', + value: 'hooked', + }, + { + name: 'Interested', + value: 'interested', + }, + { + name: 'Lemwarm Paused', + value: 'lemwarmPaused', + }, + { + name: 'LinkedIn Interested', + value: 'linkedinInterested', + }, + { + name: 'LinkedIn Invite Accepted', + value: 'linkedinInviteAccepted', + }, + { + name: 'LinkedIn Invite Done', + value: 'linkedinInviteDone', + }, + { + name: 'LinkedIn Invite Failed', + value: 'linkedinInviteFailed', + }, + { + name: 'LinkedIn Not Interested', + value: 'linkedinNotInterested', + }, + { + name: 'LinkedIn Replied', + value: 'linkedinReplied', + }, + { + name: 'LinkedIn Send Failed', + value: 'linkedinSendFailed', + }, + { + name: 'LinkedIn Sent', + value: 'linkedinSent', + }, + { + name: 'LinkedIn Visit Done', + value: 'linkedinVisitDone', + }, + { + name: 'LinkedIn Visit Failed', + value: 'linkedinVisitFailed', + }, + { + name: 'LinkedIn Voice Note Done', + value: 'linkedinVoiceNoteDone', + }, + { + name: 'LinkedIn Voice Note Failed', + value: 'linkedinVoiceNoteFailed', + }, + { + name: 'Manual Interested', + value: 'manualInterested', + }, + { + name: 'Manual Not Interested', + value: 'manualNotInterested', + }, + { + name: 'Not Interested', + value: 'notInterested', + }, + { + name: 'Opportunities Done', + value: 'opportunitiesDone', + }, + { + name: 'Paused', + value: 'paused', + }, + { + name: 'Resumed', + value: 'resumed', + }, + { + name: 'Send Limit Reached', + value: 'sendLimitReached', + }, + { + name: 'Skipped', + value: 'skipped', + }, + { + name: 'Warmed', + value: 'warmed', + }, + ], + }, + { + displayName: 'Version', + name: 'version', + type: 'string', + default: 'v2', + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Lemlist/v2/descriptions/CampaignDescription.ts b/packages/nodes-base/nodes/Lemlist/v2/descriptions/CampaignDescription.ts new file mode 100644 index 0000000000..169a1620d2 --- /dev/null +++ b/packages/nodes-base/nodes/Lemlist/v2/descriptions/CampaignDescription.ts @@ -0,0 +1,149 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const campaignOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + default: 'getAll', + options: [ + { + name: 'Get Many', + value: 'getAll', + action: 'Get many campaigns', + }, + { + name: 'Get Stats', + value: 'getStats', + action: 'Get campaign stats', + }, + ], + displayOptions: { + show: { + resource: ['campaign'], + }, + }, + }, +]; + +export const campaignFields: INodeProperties[] = [ + // ---------------------------------- + // campaign: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + displayOptions: { + show: { + resource: ['campaign'], + operation: ['getAll'], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 5, + description: 'Max number of results to return', + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + displayOptions: { + show: { + resource: ['campaign'], + operation: ['getAll'], + returnAll: [false], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: ['campaign'], + operation: ['getAll'], + }, + }, + options: [ + { + displayName: 'Version', + name: 'version', + type: 'string', + default: 'v2', + }, + ], + }, + // ---------------------------------- + // campaign: getStats + // ---------------------------------- + { + displayName: 'Campaign Name or ID', + name: 'campaignId', + type: 'options', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + description: + 'ID of the campaign to get stats for. Choose from the list, or specify an ID using an expression.', + displayOptions: { + show: { + resource: ['campaign'], + operation: ['getStats'], + }, + }, + }, + { + displayName: 'Start Date', + name: 'startDate', + type: 'dateTime', + default: '', + required: true, + placeholder: 'e.g. 2024-09-03 00:00:00Z', + displayOptions: { + show: { + resource: ['campaign'], + operation: ['getStats'], + }, + }, + }, + { + displayName: 'End Date', + name: 'endDate', + type: 'dateTime', + default: '', + placeholder: 'e.g. 2024-09-03 00:00:00Z', + required: true, + displayOptions: { + show: { + resource: ['campaign'], + operation: ['getStats'], + }, + }, + }, + { + displayName: 'Timezone', + name: 'timezone', + type: 'string', + default: '', + required: true, + placeholder: 'e.g. Europe/Paris', + displayOptions: { + show: { + resource: ['campaign'], + operation: ['getStats'], + }, + }, + }, +]; diff --git a/packages/nodes-base/nodes/Lemlist/v2/descriptions/EnrichmentDescription.ts b/packages/nodes-base/nodes/Lemlist/v2/descriptions/EnrichmentDescription.ts new file mode 100644 index 0000000000..da3c6e93b3 --- /dev/null +++ b/packages/nodes-base/nodes/Lemlist/v2/descriptions/EnrichmentDescription.ts @@ -0,0 +1,172 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const enrichmentOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + default: 'get', + options: [ + { + name: 'Get', + value: 'get', + action: 'Fetches a previously completed enrichment', + }, + { + name: 'Enrich Lead', + value: 'enrichLead', + action: 'Enrich a lead using an email or LinkedIn URL', + }, + { + name: 'Enrich Person', + value: 'enrichPerson', + action: 'Enrich a person using an email or LinkedIn URL', + }, + ], + displayOptions: { + show: { + resource: ['enrich'], + }, + }, + }, +]; + +export const enrichmentFields: INodeProperties[] = [ + // ---------------------------------- + // enrichment: get + // ---------------------------------- + { + displayName: 'Enrichment ID', + name: 'enrichId', + type: 'string', + default: '', + required: true, + description: 'ID of the enrichment to retrieve', + displayOptions: { + show: { + resource: ['enrich'], + operation: ['get'], + }, + }, + }, + // ---------------------------------- + // enrichment: enrichLead + // ---------------------------------- + { + displayName: 'Lead ID', + name: 'leadId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: ['enrich'], + operation: ['enrichLead'], + }, + }, + }, + { + displayName: 'Find Email', + name: 'findEmail', + type: 'boolean', + default: false, + displayOptions: { + show: { + resource: ['enrich'], + operation: ['enrichLead', 'enrichPerson'], + }, + }, + }, + { + displayName: 'Verify Email', + name: 'verifyEmail', + type: 'boolean', + default: false, + displayOptions: { + show: { + resource: ['enrich'], + operation: ['enrichLead', 'enrichPerson'], + }, + }, + }, + { + displayName: 'Linkedin Enrichment', + name: 'linkedinEnrichment', + type: 'boolean', + default: false, + displayOptions: { + show: { + resource: ['enrich'], + operation: ['enrichLead', 'enrichPerson'], + }, + }, + }, + { + displayName: 'Find Phone', + name: 'findPhone', + type: 'boolean', + default: false, + displayOptions: { + show: { + resource: ['enrich'], + operation: ['enrichLead', 'enrichPerson'], + }, + }, + }, + // ---------------------------------- + // enrichment: enrichPerson + // ---------------------------------- + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['enrich'], + operation: ['enrichPerson'], + }, + }, + options: [ + { + displayName: 'Email', + name: 'email', + type: 'string', + placeholder: 'name@email.com', + default: '', + }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + }, + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + default: '', + }, + { + displayName: 'Linkedin Url', + name: 'linkedinUrl', + type: 'string', + default: '', + }, + { + displayName: 'Company Name', + name: 'companyName', + type: 'string', + default: '', + }, + { + displayName: 'Company Domain', + name: 'companyDomain', + type: 'string', + default: '', + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Lemlist/v2/descriptions/LeadDescription.ts b/packages/nodes-base/nodes/Lemlist/v2/descriptions/LeadDescription.ts new file mode 100644 index 0000000000..abb5b22bfc --- /dev/null +++ b/packages/nodes-base/nodes/Lemlist/v2/descriptions/LeadDescription.ts @@ -0,0 +1,281 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const leadOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + default: 'create', + options: [ + { + name: 'Create', + value: 'create', + action: 'Create a lead', + }, + { + name: 'Delete', + value: 'delete', + action: 'Delete a lead', + }, + { + name: 'Get', + value: 'get', + action: 'Get a lead', + }, + { + name: 'Unsubscribe', + value: 'unsubscribe', + action: 'Unsubscribe a lead', + }, + ], + displayOptions: { + show: { + resource: ['lead'], + }, + }, + }, +]; + +export const leadFields: INodeProperties[] = [ + // ---------------------------------- + // lead: create + // ---------------------------------- + { + displayName: 'Campaign Name or ID', + name: 'campaignId', + type: 'options', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + description: + 'ID of the campaign to create the lead under. Choose from the list, or specify an ID using an expression.', + displayOptions: { + show: { + resource: ['lead'], + operation: ['create'], + }, + }, + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + placeholder: 'name@email.com', + 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: 'Company Domain', + name: 'companyDomain', + type: 'string', + default: '', + description: 'Company domain of the lead to create', + }, + { + displayName: 'Deduplicate', + name: 'deduplicate', + type: 'boolean', + default: false, + description: + 'Whether to do not insert if this email is already present in another campaign', + }, + { + displayName: 'Find Email', + name: 'findEmail', + type: 'boolean', + default: false, + description: 'Whether to find verified email', + }, + { + displayName: 'Find Phone', + name: 'findPhone', + type: 'boolean', + default: false, + description: 'Whether to find phone number', + }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: 'First name of the lead to create', + }, + { + displayName: 'Icebreaker', + name: 'icebreaker', + type: 'string', + default: '', + description: 'Icebreaker of the lead to create', + }, + { + displayName: 'Job Title', + name: 'jobTitle', + type: 'string', + default: '', + description: 'Job title of the lead to create', + }, + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + default: '', + description: 'Last name of the lead to create', + }, + { + displayName: 'Linkedin Enrichment', + name: 'linkedinEnrichment', + type: 'boolean', + default: false, + description: 'Whether to run the LinkedIn enrichment', + }, + + { + displayName: 'LinkedIn URL', + name: 'linkedinUrl', + type: 'string', + default: '', + description: 'LinkedIn URL of the lead to create', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + description: 'Phone number of the lead to create', + }, + { + displayName: 'Picture URL', + name: 'picture', + type: 'string', + default: '', + description: 'Picture URL of the lead to create', + }, + { + displayName: 'Verify Email', + name: 'verifyEmail', + type: 'boolean', + default: false, + description: 'Whether to verify existing email (debounce)', + }, + ], + }, + + // ---------------------------------- + // lead: delete + // ---------------------------------- + { + displayName: 'Campaign Name or ID', + name: 'campaignId', + type: 'options', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + description: + 'ID of the campaign to remove the lead from. Choose from the list, or specify an ID using an expression.', + displayOptions: { + show: { + resource: ['lead'], + operation: ['delete'], + }, + }, + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + placeholder: 'name@email.com', + default: '', + description: 'Email of the lead to delete', + displayOptions: { + show: { + resource: ['lead'], + operation: ['delete'], + }, + }, + }, + + // ---------------------------------- + // lead: get + // ---------------------------------- + { + displayName: 'Email', + name: 'email', + type: 'string', + placeholder: 'name@email.com', + default: '', + description: 'Email of the lead to retrieve', + displayOptions: { + show: { + resource: ['lead'], + operation: ['get'], + }, + }, + }, + + // ---------------------------------- + // lead: unsubscribe + // ---------------------------------- + { + displayName: 'Campaign Name or ID', + name: 'campaignId', + type: 'options', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + description: + 'ID of the campaign to unsubscribe the lead from. Choose from the list, or specify an ID using an expression.', + displayOptions: { + show: { + resource: ['lead'], + operation: ['unsubscribe'], + }, + }, + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + placeholder: 'name@email.com', + default: '', + description: 'Email of the lead to unsubscribe', + displayOptions: { + show: { + resource: ['lead'], + operation: ['unsubscribe'], + }, + }, + }, +]; diff --git a/packages/nodes-base/nodes/Lemlist/v2/descriptions/TeamDescription.ts b/packages/nodes-base/nodes/Lemlist/v2/descriptions/TeamDescription.ts new file mode 100644 index 0000000000..51b8da1189 --- /dev/null +++ b/packages/nodes-base/nodes/Lemlist/v2/descriptions/TeamDescription.ts @@ -0,0 +1,34 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const teamOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + default: 'get', + options: [ + { + name: 'Get', + value: 'get', + action: 'Get a team', + }, + { + name: 'Get Credits', + value: 'getCredits', + action: 'Get a team', + }, + ], + displayOptions: { + show: { + resource: ['team'], + }, + }, + }, +]; + +export const teamFields: INodeProperties[] = [ + // ---------------------------------- + // team: get + // ---------------------------------- +]; diff --git a/packages/nodes-base/nodes/Lemlist/v2/descriptions/UnsubscribeDescription.ts b/packages/nodes-base/nodes/Lemlist/v2/descriptions/UnsubscribeDescription.ts new file mode 100644 index 0000000000..fc87c69d1d --- /dev/null +++ b/packages/nodes-base/nodes/Lemlist/v2/descriptions/UnsubscribeDescription.ts @@ -0,0 +1,106 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const unsubscribeOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + default: 'add', + options: [ + { + name: 'Add', + value: 'add', + action: 'Add an email to an unsubscribe list', + }, + { + name: 'Delete', + value: 'delete', + action: 'Delete an email from an unsubscribe list', + }, + { + name: 'Get Many', + value: 'getAll', + action: 'Get many unsubscribed emails', + }, + ], + displayOptions: { + show: { + resource: ['unsubscribe'], + }, + }, + }, +]; + +export const unsubscribeFields: INodeProperties[] = [ + // ---------------------------------- + // unsubscribe: add + // ---------------------------------- + { + displayName: 'Email', + name: 'email', + type: 'string', + placeholder: 'name@email.com', + default: '', + description: 'Email to add to the unsubscribes', + displayOptions: { + show: { + resource: ['unsubscribe'], + operation: ['add'], + }, + }, + }, + + // ---------------------------------- + // unsubscribe: delete + // ---------------------------------- + { + displayName: 'Email', + name: 'email', + type: 'string', + placeholder: 'name@email.com', + 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: 'Whether to return all results or only up to a given limit', + displayOptions: { + show: { + resource: ['unsubscribe'], + operation: ['getAll'], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 5, + description: 'Max number of results to return', + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + displayOptions: { + show: { + resource: ['unsubscribe'], + operation: ['getAll'], + returnAll: [false], + }, + }, + }, +]; diff --git a/packages/nodes-base/nodes/Lemlist/v2/descriptions/index.ts b/packages/nodes-base/nodes/Lemlist/v2/descriptions/index.ts new file mode 100644 index 0000000000..42ccef0ec6 --- /dev/null +++ b/packages/nodes-base/nodes/Lemlist/v2/descriptions/index.ts @@ -0,0 +1,6 @@ +export * from './ActivityDescription'; +export * from './CampaignDescription'; +export * from './EnrichmentDescription'; +export * from './LeadDescription'; +export * from './TeamDescription'; +export * from './UnsubscribeDescription';