Add Dropcontact node (#2394)

* Add a new dropcontact node

* Improvements to #2389

*  Add credentials verification

*  Small improvement

*  set default time to 45 seconds

*  Improvements

*  Improvements

*  Improvements

*  Improvements

*  Improvements

* 🐛 Set siren and language correctly

Co-authored-by: PaulineDropcontact <pauline@dropcontact.io>
Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
Ricardo Espinoza 2021-11-05 13:37:50 -04:00 committed by GitHub
parent 8c4040dc5b
commit 18597808f3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 475 additions and 0 deletions

View file

@ -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: '',
},
];
}

View file

@ -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 <a href="https://en.wikipedia.org/wiki/SIREN_code" target="_blank">SIREN number</a>, NAF code, TVA number, company address and informations about the company leader.</br>
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<NodeCredentialTestResult> {
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<INodeExecutionData[][]> {
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)];
}
}

View file

@ -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<any> { // 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);
}

View file

@ -0,0 +1,3 @@
<svg viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.5692 16.219C28.1469 15.5371 26.5535 15.1552 24.8709 15.1552C18.8615 15.1552 13.9899 20.0268 13.9899 26.0362C13.9899 32.0456 18.8615 36.9172 24.8709 36.9172C28.9621 36.9172 32.526 34.6592 34.3843 31.3215L31.7614 29.8643C30.4154 32.2818 27.8341 33.9172 24.8709 33.9172C20.5183 33.9172 16.9899 30.3888 16.9899 26.0362C16.9899 21.6836 20.5183 18.1552 24.8709 18.1552C26.0896 18.1552 27.2437 18.4318 28.2738 18.9257L29.5692 16.219ZM34.7779 1.98444V27.7461H31.669V0.899363C29.5459 0.313171 27.3095 0 25 0C11.1929 0 0 11.1929 0 25C0 38.8071 11.1929 50 25 50C38.8071 50 50 38.8071 50 25C50 14.6626 43.7259 5.7907 34.7779 1.98444Z" fill="#0ABA9F"/>
</svg>

After

Width:  |  Height:  |  Size: 773 B

View file

@ -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",