From 4b854333d49c661fe11f19a176a147dbf28e697f Mon Sep 17 00:00:00 2001 From: Yann ALEMAN Date: Tue, 23 May 2023 13:52:54 +0200 Subject: [PATCH] feat(LoneScale Node): Add LoneScale node and Trigger node (#5146) --- .../credentials/LoneScaleApi.credentials.ts | 38 ++ .../nodes/LoneScale/GenericFunctions.ts | 46 ++ .../nodes/LoneScale/LoneScale.node.json | 18 + .../nodes/LoneScale/LoneScaleList.node.ts | 482 ++++++++++++++++++ .../LoneScale/LoneScaleTrigger.node.json | 18 + .../nodes/LoneScale/LoneScaleTrigger.node.ts | 131 +++++ .../nodes-base/nodes/LoneScale/constants.ts | 1 + .../nodes/LoneScale/lonescale-logo.svg | 8 + packages/nodes-base/package.json | 3 + 9 files changed, 745 insertions(+) create mode 100644 packages/nodes-base/credentials/LoneScaleApi.credentials.ts create mode 100644 packages/nodes-base/nodes/LoneScale/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/LoneScale/LoneScale.node.json create mode 100644 packages/nodes-base/nodes/LoneScale/LoneScaleList.node.ts create mode 100644 packages/nodes-base/nodes/LoneScale/LoneScaleTrigger.node.json create mode 100644 packages/nodes-base/nodes/LoneScale/LoneScaleTrigger.node.ts create mode 100644 packages/nodes-base/nodes/LoneScale/constants.ts create mode 100644 packages/nodes-base/nodes/LoneScale/lonescale-logo.svg diff --git a/packages/nodes-base/credentials/LoneScaleApi.credentials.ts b/packages/nodes-base/credentials/LoneScaleApi.credentials.ts new file mode 100644 index 0000000000..24787e347f --- /dev/null +++ b/packages/nodes-base/credentials/LoneScaleApi.credentials.ts @@ -0,0 +1,38 @@ +import type { + IAuthenticateGeneric, + ICredentialTestRequest, + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; + +export class LoneScaleApi implements ICredentialType { + name = 'loneScaleApi'; + + displayName = 'LoneScale API'; + + properties: INodeProperties[] = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string', + typeOptions: { password: true }, + default: '', + }, + ]; + + authenticate: IAuthenticateGeneric = { + type: 'generic', + properties: { + headers: { + Authorization: '={{$credentials.apiKey}}', + }, + }, + }; + + test: ICredentialTestRequest = { + request: { + baseURL: 'https://public-api.lonescale.com', + url: '/users', + }, + }; +} diff --git a/packages/nodes-base/nodes/LoneScale/GenericFunctions.ts b/packages/nodes-base/nodes/LoneScale/GenericFunctions.ts new file mode 100644 index 0000000000..872e749287 --- /dev/null +++ b/packages/nodes-base/nodes/LoneScale/GenericFunctions.ts @@ -0,0 +1,46 @@ +import type { OptionsWithUri } from 'request'; + +import type { IExecuteFunctions, ILoadOptionsFunctions } from 'n8n-core'; + +import type { IDataObject, IHookFunctions, IWebhookFunctions } from 'n8n-workflow'; +import { BASE_URL } from './constants'; + +export async function lonescaleApiRequest( + this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, + method: string, + resource: string, + body: IDataObject = {}, + query: IDataObject = {}, + uri?: string, +) { + const endpoint = `${BASE_URL}`; + const credentials = await this.getCredentials('loneScaleApi'); + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + 'X-API-KEY': credentials?.apiKey, + }, + method, + body, + qs: query, + uri: uri || `${endpoint}${resource}`, + json: true, + }; + if (!Object.keys(body).length) { + delete options.body; + } + if (!Object.keys(query).length) { + delete options.qs; + } + + try { + return await this.helpers.requestWithAuthentication.call(this, 'loneScaleApi', options); + } catch (error) { + if (error.response) { + const errorMessage = + error.response.body.message || error.response.body.description || error.message; + throw new Error(`Autopilot error response [${error.statusCode}]: ${errorMessage}`); + } + throw error; + } +} diff --git a/packages/nodes-base/nodes/LoneScale/LoneScale.node.json b/packages/nodes-base/nodes/LoneScale/LoneScale.node.json new file mode 100644 index 0000000000..06cc5f1531 --- /dev/null +++ b/packages/nodes-base/nodes/LoneScale/LoneScale.node.json @@ -0,0 +1,18 @@ +{ + "node": "n8n-nodes-base.lonescale", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": ["Sales"], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/lonescale" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.lonescale/" + } + ] + } +} diff --git a/packages/nodes-base/nodes/LoneScale/LoneScaleList.node.ts b/packages/nodes-base/nodes/LoneScale/LoneScaleList.node.ts new file mode 100644 index 0000000000..daca4616a5 --- /dev/null +++ b/packages/nodes-base/nodes/LoneScale/LoneScaleList.node.ts @@ -0,0 +1,482 @@ +import type { + IDataObject, + IExecuteFunctions, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { lonescaleApiRequest } from './GenericFunctions'; + +export class LoneScaleList implements INodeType { + description: INodeTypeDescription = { + displayName: 'LoneScale List', + name: 'loneScaleList', + group: ['transform'], + icon: 'file:lonescale-logo.svg', + version: 1, + description: 'Create List, add / delete items', + subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}', + defaults: { + name: 'LoneScale List', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'loneScaleApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'List', + value: 'list', + description: 'Manipulate list', + }, + { + name: 'Item', + value: 'item', + description: 'Manipulate item', + }, + ], + default: 'list', + noDataExpression: true, + required: true, + description: 'Create a new list', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: ['list'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a list', + action: 'Create a list', + }, + ], + default: 'create', + noDataExpression: true, + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: ['item'], + }, + }, + options: [ + { + name: 'Create', + value: 'add', + description: 'Create an item', + action: 'Create a item', + }, + ], + default: 'add', + noDataExpression: true, + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + required: true, + displayOptions: { + show: { + resource: ['item'], + }, + }, + options: [ + { + name: 'Company', + value: 'COMPANY', + description: 'List of company', + }, + { + name: 'Contact', + value: 'PEOPLE', + description: 'List of contact', + }, + ], + default: 'PEOPLE', + description: 'Type of your list', + noDataExpression: true, + }, + { + displayName: 'List Name or ID', + name: 'list', + type: 'options', + displayOptions: { + show: { + resource: ['item'], + }, + }, + typeOptions: { + loadOptionsMethod: 'getLists', + loadOptionsDependsOn: ['type'], + }, + default: '', + description: + 'Choose from the list, or specify an ID using an expression', + required: true, + }, + { + displayName: 'First Name', + name: 'first_name', + type: 'string', + displayOptions: { + show: { + operation: ['add'], + resource: ['item'], + type: ['PEOPLE'], + }, + }, + default: '', + description: 'Contact first name', + required: true, + }, + { + displayName: 'Last Name', + name: 'last_name', + type: 'string', + displayOptions: { + show: { + operation: ['add'], + resource: ['item'], + type: ['PEOPLE'], + }, + }, + default: '', + description: 'Contact last name', + required: true, + }, + + { + displayName: 'Company Name', + name: 'company_name', + type: 'string', + displayOptions: { + show: { + operation: ['add'], + resource: ['item'], + type: ['COMPANY'], + }, + }, + default: '', + description: 'Contact company name', + }, + + { + displayName: 'Additional Fields', + name: 'peopleAdditionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: ['add'], + resource: ['item'], + type: ['PEOPLE'], + }, + }, + options: [ + { + displayName: 'Full Name', + name: 'full_name', + type: 'string', + default: '', + description: 'Contact full name', + }, + { + displayName: 'Contact Email', + name: 'email', + type: 'string', + placeholder: 'name@email.com', + default: '', + description: 'Contact email', + }, + { + displayName: 'Company Name', + name: 'company_name', + type: 'string', + default: '', + description: 'Contact company name', + }, + { + displayName: 'Current Position', + name: 'current_position', + type: 'string', + default: '', + description: 'Contact current position', + }, + { + displayName: 'Company Domain', + name: 'domain', + type: 'string', + default: '', + description: 'Contact company domain', + }, + { + displayName: 'Linkedin Url', + name: 'linkedin_url', + type: 'string', + default: '', + description: 'Contact Linkedin URL', + }, + { + displayName: 'Contact Location', + name: 'location', + type: 'string', + default: '', + }, + { + displayName: 'Contact ID', + name: 'contact_id', + type: 'string', + default: '', + description: 'Contact ID from your source', + }, + ], + }, + { + displayName: 'Additional Fields', + name: 'companyAdditionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: ['add'], + resource: ['item'], + type: ['COMPANY'], + }, + }, + options: [ + { + displayName: 'Linkedin Url', + name: 'linkedin_url', + type: 'string', + default: '', + description: 'Company Linkedin URL', + }, + { + displayName: 'Company Domain', + name: 'domain', + type: 'string', + default: '', + description: 'Company company domain', + }, + { + displayName: 'Contact Location', + name: 'location', + type: 'string', + default: '', + }, + { + displayName: 'Contact ID', + name: 'contact_id', + type: 'string', + default: '', + description: 'Contact ID from your source', + }, + ], + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + required: true, + displayOptions: { + show: { + operation: ['create'], + resource: ['list'], + }, + }, + default: '', + placeholder: 'list name', + description: 'Name of your list', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + required: true, + displayOptions: { + show: { + operation: ['create'], + resource: ['list'], + }, + }, + options: [ + { + name: 'Company', + value: 'COMPANY', + description: 'Create a list of companies', + action: 'Create a list of companies', + }, + { + name: 'Contact', + value: 'PEOPLE', + description: 'Create a list of contacts', + action: 'Create a list of contacts', + }, + ], + default: 'COMPANY', + description: 'Type of your list', + noDataExpression: true, + }, + ], + }; + + methods = { + loadOptions: { + async getLists(this: ILoadOptionsFunctions): Promise { + const type = this.getNodeParameter('type') as string; + const data = await lonescaleApiRequest.call(this, 'GET', '/lists', {}, { entity: type }); + return (data as { list: Array<{ name: string; id: string; entity: string }> })?.list + ?.filter((l) => l.entity === type) + .map((d) => ({ + name: d.name, + value: d.id, + })); + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + let responseData; + const returnData = []; + const resource = this.getNodeParameter('resource', 0); + const operation = this.getNodeParameter('operation', 0); + + for (let i = 0; i < items.length; i++) { + try { + if (resource === 'list') { + if (operation === 'create') { + const name = this.getNodeParameter('name', i) as string; + const entity = this.getNodeParameter('type', i) as string; + const body: IDataObject = { + name, + entity, + }; + + responseData = await lonescaleApiRequest.call(this, 'POST', '/lists', body); + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData), + { itemData: { item: i } }, + ); + returnData.push(...executionData); + } + } + if (resource === 'item') { + if (operation === 'add') { + let firstName = ''; + let lastName = ''; + let currentPosition = ''; + let fullName = ''; + let email = ''; + let linkedinUrl = ''; + let companyName = ''; + let domain = ''; + let location = ''; + let contactId = ''; + + const entity = this.getNodeParameter('type', i) as string; + const listId = this.getNodeParameter('list', i) as string; + if (entity === 'PEOPLE') { + const peopleAdditionalFields = this.getNodeParameter('peopleAdditionalFields', i) as { + email: string; + full_name: string; + current_position: string; + linkedin_url: string; + company_name: string; + domain: string; + location: string; + contact_id: string; + }; + firstName = this.getNodeParameter('first_name', i) as string; + lastName = this.getNodeParameter('last_name', i) as string; + fullName = peopleAdditionalFields?.full_name; + currentPosition = peopleAdditionalFields?.current_position; + email = peopleAdditionalFields?.email; + linkedinUrl = peopleAdditionalFields?.linkedin_url; + companyName = peopleAdditionalFields?.company_name; + domain = peopleAdditionalFields?.domain; + location = peopleAdditionalFields?.location; + contactId = peopleAdditionalFields?.contact_id; + } + if (entity === 'COMPANY') { + const companyAdditionalFields = this.getNodeParameter( + 'companyAdditionalFields', + i, + ) as { + linkedin_url: string; + domain: string; + location: string; + contact_id: string; + }; + companyName = this.getNodeParameter('company_name', i) as string; + linkedinUrl = companyAdditionalFields?.linkedin_url; + domain = companyAdditionalFields?.domain; + location = companyAdditionalFields?.location; + contactId = companyAdditionalFields?.contact_id; + } + + const body: IDataObject = { + ...(firstName && { first_name: firstName }), + ...(lastName && { last_name: lastName }), + ...(fullName && { full_name: fullName }), + ...(linkedinUrl && { linkedin_url: linkedinUrl }), + ...(companyName && { company_name: companyName }), + ...(currentPosition && { current_position: currentPosition }), + ...(domain && { domain }), + ...(location && { location }), + ...(email && { email }), + ...(contactId && { contact_id: contactId }), + }; + + responseData = await lonescaleApiRequest.call( + this, + 'POST', + `/lists/${listId}/item`, + body, + ); + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData), + { itemData: { item: i } }, + ); + returnData.push(...executionData); + } + } + } catch (error) { + if (this.continueOnFail()) { + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ error: error.message }), + { itemData: { item: i } }, + ); + returnData.push(...executionData); + continue; + } + throw error; + } + } + return this.prepareOutputData(returnData); + } +} diff --git a/packages/nodes-base/nodes/LoneScale/LoneScaleTrigger.node.json b/packages/nodes-base/nodes/LoneScale/LoneScaleTrigger.node.json new file mode 100644 index 0000000000..fa23dc1452 --- /dev/null +++ b/packages/nodes-base/nodes/LoneScale/LoneScaleTrigger.node.json @@ -0,0 +1,18 @@ +{ + "node": "n8n-nodes-base.lonescaleTrigger", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": ["Sales"], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/lonescale" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.lonescaletrigger/" + } + ] + } +} diff --git a/packages/nodes-base/nodes/LoneScale/LoneScaleTrigger.node.ts b/packages/nodes-base/nodes/LoneScale/LoneScaleTrigger.node.ts new file mode 100644 index 0000000000..a6d89a6f44 --- /dev/null +++ b/packages/nodes-base/nodes/LoneScale/LoneScaleTrigger.node.ts @@ -0,0 +1,131 @@ +import type { IWebhookFunctions } from 'n8n-core'; + +import type { + IDataObject, + IHookFunctions, + ILoadOptionsFunctions, + INodePropertyOptions, + INodeType, + INodeTypeDescription, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { lonescaleApiRequest } from './GenericFunctions'; + +export class LoneScaleTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'LoneScale Trigger', + name: 'loneScaleTrigger', + icon: 'file:lonescale-logo.svg', + group: ['trigger'], + version: 1, + description: 'Trigger LoneScale Workflow', + defaults: { + name: 'LoneScale Trigger', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'loneScaleApi', + required: true, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + + properties: [ + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Workflow Name', + name: 'workflow', + type: 'options', + noDataExpression: true, + typeOptions: { + loadOptionsMethod: 'getWorkflows', + }, + default: '', + // eslint-disable-next-line n8n-nodes-base/node-param-description-missing-final-period, n8n-nodes-base/node-param-description-wrong-for-dynamic-options + description: 'Select one workflow. Choose from the list', + required: true, + }, + ], + }; + + methods = { + loadOptions: { + async getWorkflows(this: ILoadOptionsFunctions): Promise { + const data = await lonescaleApiRequest.call(this, 'GET', '/workflows'); + return (data as Array<{ title: string; id: string }>)?.map((d) => ({ + name: d.title, + value: d.id, + })); + }, + }, + }; + + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const webhookUrl = this.getNodeWebhookUrl('default'); + const workflowId = this.getNodeParameter('workflow') as string; + const webhook = await lonescaleApiRequest.call( + this, + 'GET', + `/workflows/${workflowId}/hook?type=n8n`, + ); + if (webhook.target_url === webhookUrl) { + webhookData.webhookId = webhook.webhook_id; + return true; + } + return false; + }, + async create(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + const workflowId = this.getNodeParameter('workflow') as string; + const body: IDataObject = { + type: 'n8n', + target_url: webhookUrl, + }; + const webhook = await lonescaleApiRequest.call( + this, + 'POST', + `/workflows/${workflowId}/hook`, + body, + ); + webhookData.webhookId = webhook.webhook_id; + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + try { + await lonescaleApiRequest.call( + this, + 'DELETE', + `/workflows/${webhookData.webhookId}/hook?type=n8n`, + ); + } 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/LoneScale/constants.ts b/packages/nodes-base/nodes/LoneScale/constants.ts new file mode 100644 index 0000000000..0f25710c86 --- /dev/null +++ b/packages/nodes-base/nodes/LoneScale/constants.ts @@ -0,0 +1 @@ +export const BASE_URL = 'https://public-api.lonescale.com'; diff --git a/packages/nodes-base/nodes/LoneScale/lonescale-logo.svg b/packages/nodes-base/nodes/LoneScale/lonescale-logo.svg new file mode 100644 index 0000000000..ecbedb6813 --- /dev/null +++ b/packages/nodes-base/nodes/LoneScale/lonescale-logo.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index f2cf9b4227..150ef66100 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -176,6 +176,7 @@ "dist/credentials/LineNotifyOAuth2Api.credentials.js", "dist/credentials/LingvaNexApi.credentials.js", "dist/credentials/LinkedInOAuth2Api.credentials.js", + "dist/credentials/LoneScaleApi.credentials.js", "dist/credentials/Magento2Api.credentials.js", "dist/credentials/MailcheckApi.credentials.js", "dist/credentials/MailchimpApi.credentials.js", @@ -542,6 +543,8 @@ "dist/nodes/LingvaNex/LingvaNex.node.js", "dist/nodes/LinkedIn/LinkedIn.node.js", "dist/nodes/LocalFileTrigger/LocalFileTrigger.node.js", + "dist/nodes/LoneScale/LoneScaleTrigger.node.js", + "dist/nodes/LoneScale/LoneScaleList.node.js", "dist/nodes/Magento/Magento2.node.js", "dist/nodes/Mailcheck/Mailcheck.node.js", "dist/nodes/Mailchimp/Mailchimp.node.js",