From 18597808f31d6c47af607d37964198bd66c3f5f7 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Fri, 5 Nov 2021 13:37:50 -0400 Subject: [PATCH] :sparkles: Add Dropcontact node (#2394) * Add a new dropcontact node * Improvements to #2389 * :zap: Add credentials verification * :zap: Small improvement * :zap: set default time to 45 seconds * :zap: Improvements * :zap: Improvements * :zap: Improvements * :zap: Improvements * :zap: Improvements * :bug: Set siren and language correctly Co-authored-by: PaulineDropcontact Co-authored-by: Jan Oberhauser --- .../credentials/DropcontactApi.credentials.ts | 18 + .../nodes/Dropcontact/Dropcontact.node.ts | 370 ++++++++++++++++++ .../nodes/Dropcontact/GenericFunction.ts | 82 ++++ .../nodes/Dropcontact/dropcontact.svg | 3 + packages/nodes-base/package.json | 2 + 5 files changed, 475 insertions(+) create mode 100644 packages/nodes-base/credentials/DropcontactApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Dropcontact/Dropcontact.node.ts create mode 100644 packages/nodes-base/nodes/Dropcontact/GenericFunction.ts create mode 100644 packages/nodes-base/nodes/Dropcontact/dropcontact.svg diff --git a/packages/nodes-base/credentials/DropcontactApi.credentials.ts b/packages/nodes-base/credentials/DropcontactApi.credentials.ts new file mode 100644 index 0000000000..de91630b01 --- /dev/null +++ b/packages/nodes-base/credentials/DropcontactApi.credentials.ts @@ -0,0 +1,18 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class DropcontactApi implements ICredentialType { + name = 'dropcontactApi'; + displayName = 'Dropcontact API'; + documentationUrl = 'dropcontact'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Dropcontact/Dropcontact.node.ts b/packages/nodes-base/nodes/Dropcontact/Dropcontact.node.ts new file mode 100644 index 0000000000..6269903dd3 --- /dev/null +++ b/packages/nodes-base/nodes/Dropcontact/Dropcontact.node.ts @@ -0,0 +1,370 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + ICredentialDataDecryptedObject, + ICredentialsDecrypted, + ICredentialTestFunctions, + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, + NodeApiError, + NodeCredentialTestResult, +} from 'n8n-workflow'; + +import { + dropcontactApiRequest, + validateCrendetials, +} from './GenericFunction'; + +export class Dropcontact implements INodeType { + description: INodeTypeDescription = { + displayName: 'Dropcontact', + name: 'dropcontact', + icon: 'file:dropcontact.svg', + group: ['transform'], + version: 1, + description: 'Find B2B emails and enrich contacts', + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + defaults: { + name: 'Dropcontact', + color: '#0ABA9F', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'dropcontactApi', + required: true, + testedBy: 'dropcontactApiCredentialTest', + }, + ], + properties: [ + { + displayName: 'Resource', + noDataExpression: true, + name: 'resource', + type: 'options', + options: [ + { + name: 'Contact', + value: 'contact', + }, + ], + default: 'contact', + required: true, + }, + { + displayName: 'Operation', + noDataExpression: true, + name: 'operation', + type: 'options', + options: [ + { + name: 'Enrich', + value: 'enrich', + description: 'Find B2B emails and enrich your contact from his name and his website', + }, + { + name: 'Fetch Request', + value: 'fetchRequest', + }, + ], + default: 'enrich', + required: true, + }, + { + displayName: 'Request ID', + name: 'requestId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'fetchRequest', + ], + }, + }, + default: '', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'enrich', + ], + }, + }, + default: '', + }, + { + displayName: 'Simplify Output (Faster)', + name: 'simplify', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'enrich', + ], + }, + }, + default: false, + description: 'When off, waits for the contact data before completing. Waiting time can be adjusted with Extend Wait Time option. When on, returns a request_id that can be used later in the Fetch Request operation.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'enrich', + ], + }, + }, + options: [ + { + displayName: 'Company SIREN Number', + name: 'num_siren', + type: 'string', + default: '', + }, + { + displayName: 'Company SIRET Code', + name: 'siret', + type: 'string', + default: '', + }, + { + displayName: 'Company Name', + name: 'company', + type: 'string', + default: '', + }, + { + displayName: 'Country', + name: 'country', + type: 'string', + default: '', + }, + { + displayName: 'First Name', + name: 'first_name', + type: 'string', + default: '', + }, + { + displayName: 'Full Name', + name: 'full_name', + type: 'string', + default: '', + }, + { + displayName: 'Last Name', + name: 'last_name', + type: 'string', + default: '', + }, + { + displayName: 'LinkedIn Profile', + name: 'linkedin', + type: 'string', + default: '', + }, + { + displayName: 'Phone Number', + name: 'phone', + type: 'string', + default: '', + }, + { + displayName: 'Website', + name: 'website', + type: 'string', + default: '', + }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'enrich', + ], + }, + }, + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Data Fetch Wait Time', + name: 'waitTime', + type: 'number', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + '/simplify': [ + false, + ], + }, + }, + default: 45, + description: 'When not simplifying the response, data will be fetched in two steps. This parameter controls how long to wait (in seconds) before trying the second step', + }, + { + displayName: 'French Company Enrich', + name: 'siren', + type: 'boolean', + default: false, + description: `Whether you want the SIREN number, NAF code, TVA number, company address and informations about the company leader.
+ Only applies to french companies`, + }, + { + displayName: 'Language', + name: 'language', + type: 'options', + options: [ + { + name: 'English', + value: 'en', + }, + { + name: 'French', + value: 'fr', + }, + ], + default: 'en', + description: 'Whether the response is in English or French', + }, + ], + }, + ], + }; + + methods = { + credentialTest: { + async dropcontactApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise { + try { + await validateCrendetials.call(this, credential.data as ICredentialDataDecryptedObject); + } catch (error) { + return { + status: 'Error', + message: 'The API Key included in the request is invalid', + }; + } + + return { + status: 'OK', + message: 'Connection successful!', + }; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const entryData = this.getInputData(); + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + // tslint:disable-next-line: no-any + let responseData: any; + const returnData: IDataObject[] = []; + + if (resource === 'contact') { + if (operation === 'enrich') { + const options = this.getNodeParameter('options', 0) as IDataObject; + const data = []; + const simplify = this.getNodeParameter('simplify', 0) as boolean; + + const siren = options.siren === true ? true : false; + const language = options.language ? options.language : 'en'; + + for (let i = 0; i < entryData.length; i++) { + const email = this.getNodeParameter('email', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i); + const body: IDataObject = {}; + if (email !== '') { + body.email = email; + } + Object.assign(body, additionalFields); + data.push(body); + } + + responseData = await dropcontactApiRequest.call(this, 'POST', '/batch', { data, siren, language }, {}) as { request_id: string, error: string, success: boolean }; + + if (!responseData.success) { + if (this.continueOnFail()) { + returnData.push({ error: responseData.reason || 'invalid request' }); + } else { + throw new NodeApiError(this.getNode(), { error: responseData.reason || 'invalid request' }); + } + } + + if (simplify === false) { + const waitTime = this.getNodeParameter('options.waitTime', 0, 45) as number; + // tslint:disable-next-line: no-any + const delay = (ms: any) => new Promise(res => setTimeout(res, ms * 1000)); + await delay(waitTime); + responseData = await dropcontactApiRequest.call(this, 'GET', `/batch/${responseData.request_id}`, {}, {}); + if (!responseData.success) { + if (this.continueOnFail()) { + responseData.push({ error: responseData.reason }); + } else { + throw new NodeApiError(this.getNode(), { + error: responseData.reason, + description: 'Hint: Increase the Wait Time to avoid this error', + }); + } + } else { + returnData.push(...responseData.data); + } + } else { + returnData.push(responseData); + } + } + + if (operation === 'fetchRequest') { + for (let i = 0; i < entryData.length; i++) { + const requestId = this.getNodeParameter('requestId', i) as string; + responseData = await dropcontactApiRequest.call(this, 'GET', `/batch/${requestId}`, {}, {}) as { request_id: string, error: string, success: boolean }; + if (!responseData.success) { + if (this.continueOnFail()) { + responseData.push({ error: responseData.reason || 'invalid request' }); + } else { + throw new NodeApiError(this.getNode(), { error: responseData.reason || 'invalid request' }); + } + } + returnData.push(...responseData.data); + } + } + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Dropcontact/GenericFunction.ts b/packages/nodes-base/nodes/Dropcontact/GenericFunction.ts new file mode 100644 index 0000000000..e5af1e6202 --- /dev/null +++ b/packages/nodes-base/nodes/Dropcontact/GenericFunction.ts @@ -0,0 +1,82 @@ +import { + IExecuteFunctions, + IHookFunctions, +} from 'n8n-core'; + +import { + ICredentialDataDecryptedObject, + ICredentialTestFunctions, + IDataObject, + ILoadOptionsFunctions, + NodeApiError, +} from 'n8n-workflow'; + +import { + OptionsWithUri, +} from 'request'; + +/** + * Make an authenticated API request to Bubble. + */ +export async function dropcontactApiRequest( + this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, + method: string, + endpoint: string, + body: IDataObject, + qs: IDataObject, +) { + + const { apiKey } = await this.getCredentials('dropcontactApi') as { + apiKey: string, + }; + + const options: OptionsWithUri = { + headers: { + 'user-agent': 'n8n', + 'X-Access-Token': apiKey, + }, + method, + uri: `https://api.dropcontact.io${endpoint}`, + qs, + body, + json: true, + }; + + if (!Object.keys(body).length) { + delete options.body; + } + + if (!Object.keys(qs).length) { + delete options.qs; + } + + try { + return await this.helpers.request!(options); + } catch (error) { + throw new NodeApiError(this.getNode(), error); + } +} + +export async function validateCrendetials(this: ICredentialTestFunctions, decryptedCredentials: ICredentialDataDecryptedObject): Promise { // tslint:disable-line:no-any + const credentials = decryptedCredentials; + + const { apiKey } = credentials as { + apiKey: string, + }; + + const options: OptionsWithUri = { + headers: { + 'user-agent': 'n8n', + 'X-Access-Token': apiKey, + }, + method: 'POST', + body: { + data: [{ email: '' }], + }, + uri: `https://api.dropcontact.io/batch`, + json: true, + }; + + return this.helpers.request!(options); +} + diff --git a/packages/nodes-base/nodes/Dropcontact/dropcontact.svg b/packages/nodes-base/nodes/Dropcontact/dropcontact.svg new file mode 100644 index 0000000000..447973cc55 --- /dev/null +++ b/packages/nodes-base/nodes/Dropcontact/dropcontact.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 9d107f7f14..5edffc0b58 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -78,6 +78,7 @@ "dist/credentials/DriftOAuth2Api.credentials.js", "dist/credentials/DropboxApi.credentials.js", "dist/credentials/DropboxOAuth2Api.credentials.js", + "dist/credentials/DropcontactApi.credentials.js", "dist/credentials/EgoiApi.credentials.js", "dist/credentials/ElasticsearchApi.credentials.js", "dist/credentials/ElasticSecurityApi.credentials.js", @@ -379,6 +380,7 @@ "dist/nodes/Disqus/Disqus.node.js", "dist/nodes/Drift/Drift.node.js", "dist/nodes/Dropbox/Dropbox.node.js", + "dist/nodes/Dropcontact/Dropcontact.node.js", "dist/nodes/EditImage.node.js", "dist/nodes/Egoi/Egoi.node.js", "dist/nodes/Elastic/ElasticSecurity/ElasticSecurity.node.js",