From eab4df1afdd4e3c23aa7d3a0288910adbcb42199 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Fri, 24 Jan 2020 16:16:54 -0500 Subject: [PATCH] :sparkles: hunter node --- .../credentials/HunterApi.credentials.ts | 17 + .../nodes/Hunter/GenericFunctions.ts | 56 +++ .../nodes-base/nodes/Hunter/Hunter.node.ts | 335 ++++++++++++++++++ packages/nodes-base/nodes/Hunter/hunter.png | Bin 0 -> 43032 bytes packages/nodes-base/package.json | 2 + 5 files changed, 410 insertions(+) create mode 100644 packages/nodes-base/credentials/HunterApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Hunter/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Hunter/Hunter.node.ts create mode 100644 packages/nodes-base/nodes/Hunter/hunter.png diff --git a/packages/nodes-base/credentials/HunterApi.credentials.ts b/packages/nodes-base/credentials/HunterApi.credentials.ts new file mode 100644 index 0000000000..802ff5159b --- /dev/null +++ b/packages/nodes-base/credentials/HunterApi.credentials.ts @@ -0,0 +1,17 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class HunterApi implements ICredentialType { + name = 'hunterApi'; + displayName = 'Hunter API'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Hunter/GenericFunctions.ts b/packages/nodes-base/nodes/Hunter/GenericFunctions.ts new file mode 100644 index 0000000000..02fafdd482 --- /dev/null +++ b/packages/nodes-base/nodes/Hunter/GenericFunctions.ts @@ -0,0 +1,56 @@ +import { OptionsWithUri } from 'request'; +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; +import { IDataObject } from 'n8n-workflow'; + +export async function hunterApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('hunterApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + qs = Object.assign({ api_key: credentials.apiKey }, qs); + let options: OptionsWithUri = { + method, + qs, + body, + uri: uri ||`https://api.hunter.io/v2${resource}`, + json: true + }; + options = Object.assign({}, options, option); + if (Object.keys(options.body).length === 0) { + delete options.body; + } + try { + return await this.helpers.request!(options); + } catch (err) { + throw new Error(err); + } +} + +/** + * Make an API request to paginated flow endpoint + * and return all results + */ +export async function hunterApiRequestAllItems(this: IHookFunctions | IExecuteFunctions| ILoadOptionsFunctions, propertyName: string, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + query.offset = 0; + query.limit = 100; + + do { + responseData = await hunterApiRequest.call(this, method, resource, body, query); + returnData.push.apply(returnData, responseData[propertyName]); + query.offset += query.limit; + } while ( + responseData.meta !== undefined && + responseData.meta.results !== undefined && + responseData.meta.offset <= responseData.meta.results + ); + return returnData; +} diff --git a/packages/nodes-base/nodes/Hunter/Hunter.node.ts b/packages/nodes-base/nodes/Hunter/Hunter.node.ts new file mode 100644 index 0000000000..b4df2e1595 --- /dev/null +++ b/packages/nodes-base/nodes/Hunter/Hunter.node.ts @@ -0,0 +1,335 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; +import { + IDataObject, + INodeTypeDescription, + INodeExecutionData, + INodeType, +} from 'n8n-workflow'; +import { + hunterApiRequest, + hunterApiRequestAllItems, +} from './GenericFunctions'; + +export class Hunter implements INodeType { + description: INodeTypeDescription = { + displayName: 'Hunter', + name: 'hunter', + icon: 'file:hunter.png', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"]}}', + description: 'Consume Hunter API', + defaults: { + name: 'Hunter', + color: '#ff3807', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'hunterApi', + required: true, + } + ], + properties: [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: ' Domain Search', + value: 'domainSearch', + description: 'Get every email address found on the internet using a given domain name, with sources.', + }, + { + name: ' Email Finder', + value: 'emailFinder', + description: 'Generates or retrieves the most likely email address from a domain name, a first name and a last name.', + }, + { + name: 'Email Verifier', + value: 'emailVerifier', + description: 'Allows you to verify the deliverability of an email address.', + }, + ], + default: 'domainSearch', + description: 'operation to consume.', + }, + { + displayName: 'Domain', + name: 'domain', + type: 'string', + displayOptions: { + show: { + operation: [ + 'domainSearch', + ], + returnAll: [ + false, + ], + }, + }, + default: '', + required: true, + description: 'Domain name from which you want to find the email addresses. For example, "stripe.com".', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'domainSearch', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'domainSearch', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + operation: [ + 'domainSearch', + ], + }, + }, + options: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + default: '', + options: [ + { + name: 'Personal', + value: 'personal', + }, + { + name: 'Generic', + value: 'generic', + }, + ] + }, + { + displayName: 'Seniority', + name: 'seniority', + type: 'multiOptions', + default: [], + options: [ + { + name: 'Junior', + value: 'junior', + }, + { + name: 'Senior', + value: 'senior', + }, + { + name: 'Executive', + value: 'executive', + }, + ] + }, + { + displayName: 'Department', + name: 'department', + type: 'multiOptions', + default: [], + options: [ + { + name: 'Executive', + value: 'executive', + }, + { + name: 'IT', + value: 'it', + }, + { + name: 'Finance', + value: 'finance', + }, + { + name: 'Management', + value: 'management', + }, + { + name: 'Sales', + value: 'sales', + }, + { + name: 'Legal', + value: 'legal', + }, + { + name: 'Support', + value: 'support', + }, + { + name: 'HR', + value: 'hr', + }, + { + name: 'Marketing', + value: 'marketing', + }, + { + name: 'Communication', + value: 'communication', + }, + ] + }, + ], + }, + { + displayName: 'Domain', + name: 'domain', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'emailFinder' + ], + }, + }, + required: true, + description: 'Domain name from which you want to find the email addresses. For example, "stripe.com".', + }, + { + displayName: 'First Name', + name: 'firstname', + type: 'string', + displayOptions: { + show: { + operation: [ + 'emailFinder' + ], + }, + }, + default: '', + required: true, + description: `The person's first name. It doesn't need to be in lowercase..`, + }, + { + displayName: 'Last Name', + name: 'lastname', + type: 'string', + displayOptions: { + show: { + operation: [ + 'emailFinder' + ], + }, + }, + default: '', + required: true, + description: `The person's last name. It doesn't need to be in lowercase..`, + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + displayOptions: { + show: { + operation: [ + 'emailVerifier' + ], + }, + }, + default: '', + required: true, + description: 'The email address you want to verify', + }, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + const qs: IDataObject = {}; + let responseData; + for (let i = 0; i < length; i++) { + const operation = this.getNodeParameter('operation', 0) as string; + //https://hunter.io/api-documentation/v2#domain-search + if (operation === 'domainSearch') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + const domain = this.getNodeParameter('domain', i) as string; + qs.domain = domain; + if (filters.type){ + qs.type = filters.type; + } + if (filters.seniority){ + qs.seniority = (filters.seniority as string[]).join(','); + } + if (filters.department){ + qs.department = (filters.department as string[]).join(','); + } + if (returnAll) { + responseData = await hunterApiRequestAllItems.call(this, 'data', 'GET', '/domain-search', {}, qs); + } else { + const limit = this.getNodeParameter('limit', i) as number; + qs.limit = limit; + responseData = await hunterApiRequest.call(this, 'GET', '/domain-search', {}, qs); + responseData = responseData.data; + } + } + //https://hunter.io/api-documentation/v2#email-finder + if (operation === 'emailFinder') { + const domain = this.getNodeParameter('domain', i) as string; + const firstname = this.getNodeParameter('firstname', i) as string; + const lastname = this.getNodeParameter('lastname', i) as string; + qs.first_name = firstname; + qs.last_name = lastname; + qs.domain = domain; + responseData = await hunterApiRequest.call(this, 'GET', '/email-finder', {}, qs); + responseData = responseData.data; + } + //https://hunter.io/api-documentation/v2#email-verifier + if (operation === 'emailVerifier') { + const email = this.getNodeParameter('email', i) as string; + qs.email = email; + responseData = await hunterApiRequest.call(this, 'GET', '/email-verifier', {}, qs); + responseData = responseData.data; + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else { + returnData.push(responseData as IDataObject); + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Hunter/hunter.png b/packages/nodes-base/nodes/Hunter/hunter.png new file mode 100644 index 0000000000000000000000000000000000000000..fb214f80ee2efed6e42179b68c30ef3516328fc2 GIT binary patch literal 43032 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 jLN^qz$&LEd)y@CuM&=jVM@w%A@MCIdjy