From 09547e91530da22764db81a1a58bb6433ba2c26a Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Wed, 18 Nov 2020 17:58:41 -0500 Subject: [PATCH] :sparkles: Add Humantic AI node (#1177) * :sparkles: Add Humantic AI node * :zap: Improvements Improvements to #1165 Co-authored-by: Tanay Pant --- .../credentials/HumanticAiApi.credentials.ts | 18 ++ .../nodes/HumanticAI/GenericFunctions.ts | 59 +++++ .../nodes/HumanticAI/HumanticAi.node.ts | 190 ++++++++++++++ .../nodes/HumanticAI/ProfileDescription.ts | 238 ++++++++++++++++++ .../nodes/HumanticAI/humanticai.png | Bin 0 -> 1639 bytes packages/nodes-base/package.json | 2 + 6 files changed, 507 insertions(+) create mode 100644 packages/nodes-base/credentials/HumanticAiApi.credentials.ts create mode 100644 packages/nodes-base/nodes/HumanticAI/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/HumanticAI/HumanticAi.node.ts create mode 100644 packages/nodes-base/nodes/HumanticAI/ProfileDescription.ts create mode 100644 packages/nodes-base/nodes/HumanticAI/humanticai.png diff --git a/packages/nodes-base/credentials/HumanticAiApi.credentials.ts b/packages/nodes-base/credentials/HumanticAiApi.credentials.ts new file mode 100644 index 0000000000..b9303e028a --- /dev/null +++ b/packages/nodes-base/credentials/HumanticAiApi.credentials.ts @@ -0,0 +1,18 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class HumanticAiApi implements ICredentialType { + name = 'humanticAiApi'; + displayName = 'Humantic AI API'; + documentationUrl = 'humanticAi'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/HumanticAI/GenericFunctions.ts b/packages/nodes-base/nodes/HumanticAI/GenericFunctions.ts new file mode 100644 index 0000000000..b2f785e13e --- /dev/null +++ b/packages/nodes-base/nodes/HumanticAI/GenericFunctions.ts @@ -0,0 +1,59 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function humanticAiApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + try { + const credentials = this.getCredentials('humanticAiApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + let options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + qs, + body, + uri: `https://api.humantic.ai/v1${resource}`, + json: true, + }; + + options = Object.assign({}, options, option); + options.qs.apikey = credentials.apiKey; + + if (Object.keys(options.body).length === 0) { + delete options.body; + } + + const response = await this.helpers.request!(options); + + if (response.data && response.data.status === 'error') { + throw new Error(`Humantic AI error response [400]: ${response.data.message}`); + } + + return response; + + } catch (error) { + + if (error.response && error.response.body && error.response.body.message) { + // Try to return the error prettier + const errorBody = error.response.body; + throw new Error(`Humantic AI error response [${error.statusCode}]: ${errorBody.message}`); + } + + // Expected error data did not get returned so throw the actual error + throw error; + } +} diff --git a/packages/nodes-base/nodes/HumanticAI/HumanticAi.node.ts b/packages/nodes-base/nodes/HumanticAI/HumanticAi.node.ts new file mode 100644 index 0000000000..b7996bc008 --- /dev/null +++ b/packages/nodes-base/nodes/HumanticAI/HumanticAi.node.ts @@ -0,0 +1,190 @@ +import { + BINARY_ENCODING, + IExecuteFunctions, +} from 'n8n-core'; + +import { + IBinaryData, + IBinaryKeyData, + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + humanticAiApiRequest, +} from './GenericFunctions'; + +import { + profileFields, + profileOperations, +} from './ProfileDescription'; + +export class HumanticAi implements INodeType { + description: INodeTypeDescription = { + displayName: 'Humantic AI', + name: 'humanticAi', + icon: 'file:humanticai.png', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Humantic AI API', + defaults: { + name: 'Humantic AI', + color: '#f8ce59', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'humanticAiApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Profile', + value: 'profile', + }, + ], + default: 'profile', + description: 'Resource to consume.', + }, + // PROFILE + ...profileOperations, + ...profileFields, + ], + }; + + 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; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < length; i++) { + if (resource === 'profile') { + if (operation === 'create') { + const userId = this.getNodeParameter('userId', i) as string; + const sendResume = this.getNodeParameter('sendResume', i) as boolean; + qs.userid = userId; + + if (sendResume) { + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string; + + if (items[i].binary === undefined) { + throw new Error('No binary data exists on item!'); + } + + const item = items[i].binary as IBinaryKeyData; + + const binaryData = item[binaryPropertyName] as IBinaryData; + + if (binaryData === undefined) { + throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`); + } + + responseData = await humanticAiApiRequest.call( + this, + 'POST', + `/user-profile/create`, + {}, + qs, + { + formData: { + resume: { + value: Buffer.from(binaryData.data, BINARY_ENCODING), + options: { + filename: binaryData.fileName, + }, + }, + }, + }, + ); + responseData = responseData.data; + } else { + responseData = await humanticAiApiRequest.call(this, 'GET', `/user-profile/create`, {}, qs); + responseData = responseData.data; + } + } + if (operation === 'get') { + const userId = this.getNodeParameter('userId', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + + qs.userid = userId; + + if (options.persona) { + qs.persona = (options.persona as string[]).join(','); + } + + responseData = await humanticAiApiRequest.call(this, 'GET', `/user-profile`, {}, qs); + responseData = responseData.results; + } + if (operation === 'update') { + const userId = this.getNodeParameter('userId', i) as string; + const sendResume = this.getNodeParameter('sendResume', i) as string; + qs.userid = userId; + + if (sendResume) { + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string; + + if (items[i].binary === undefined) { + throw new Error('No binary data exists on item!'); + } + + const item = items[i].binary as IBinaryKeyData; + + const binaryData = item[binaryPropertyName] as IBinaryData; + + if (binaryData === undefined) { + throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`); + } + + responseData = await humanticAiApiRequest.call( + this, + 'POST', + `/user-profile/create`, + {}, + qs, + { + formData: { + resume: { + value: Buffer.from(binaryData.data, BINARY_ENCODING), + options: { + filename: binaryData.fileName, + }, + }, + }, + }, + ); + responseData = responseData.data; + } else { + const text = this.getNodeParameter('text', i) as string; + const body: IDataObject = { + text, + }; + + qs.userid = userId; + + responseData = await humanticAiApiRequest.call(this, 'POST', `/user-profile/create`, body, 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/HumanticAI/ProfileDescription.ts b/packages/nodes-base/nodes/HumanticAI/ProfileDescription.ts new file mode 100644 index 0000000000..3e54b8fadb --- /dev/null +++ b/packages/nodes-base/nodes/HumanticAI/ProfileDescription.ts @@ -0,0 +1,238 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const profileOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'profile', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a profile', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve a profile', + }, + { + name: 'Update', + value: 'update', + description: 'Update a profile', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const profileFields = [ + /* -------------------------------------------------------------------------- */ + /* profile:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'User ID', + name: 'userId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'profile', + ], + }, + }, + description: `The LinkedIn profile URL or email ID for creating a Humantic profile. If you are sending the resume, this should be a unique string.`, + }, + { + displayName: 'Send Resume', + name: 'sendResume', + type: 'boolean', + default: false, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'profile', + ], + }, + }, + description: `Send a resume for a resume based analysis.`, + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'profile', + ], + sendResume: [ + true, + ], + }, + }, + description: `The resume in PDF or DOCX format.`, + }, + + /* -------------------------------------------------------------------------- */ + /* profile:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'User ID', + name: 'userId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'profile', + ], + }, + }, + description: `This value is the same as the User ID that was provided when the analysis was created. This could be a LinkedIn URL, email ID, or a unique string in case of resume based analysis.`, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'profile', + ], + }, + }, + options: [ + { + displayName: 'Persona', + name: 'persona', + type: 'multiOptions', + options: [ + { + name: 'Sales', + value: 'sales', + }, + { + name: 'Hiring', + value: 'hiring', + }, + ], + default: [], + description: `Fetch the Humantic profile of the user for a particular persona type.
+ Multiple persona values can be supported using comma as a delimiter.`, + }, + ], + }, + + + + /* -------------------------------------------------------------------------- */ + /* profile:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'User ID', + name: 'userId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'profile', + ], + }, + }, + description: `This value is the same as the User ID that was provided when the analysis was created. Currently only supported for profiles created using LinkedIn URL.`, + }, + { + displayName: 'Send Resume', + name: 'sendResume', + type: 'boolean', + default: false, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'profile', + ], + }, + }, + description: `Send a resume for a resume of the user.`, + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'profile', + ], + sendResume: [ + false, + ], + }, + }, + description: `Additional text written by the user.`, + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'profile', + ], + sendResume: [ + true, + ], + }, + }, + description: `The resume in PDF or DOCX format.`, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/HumanticAI/humanticai.png b/packages/nodes-base/nodes/HumanticAI/humanticai.png new file mode 100644 index 0000000000000000000000000000000000000000..efdfbdbe7f6bcc93a4920e64a25e6bacd12f9120 GIT binary patch literal 1639 zcmV-t2AKJYP)8uqtj#qoo9Gb5`)ul5hUM1Ewb4Zd6=zwdt+KgQ&aqXNBs!d+ zdBzkk3=EM22&5AzamvgJ5#dsUVQ=a1{KwRk7StzC#A!S+?RC4Y>? z7k@#8lnc_T=3}^7os9TkGYHvbD+;7sNP0k7YRSMKV~1gAu(|xLPhG0MXo{*M2OV+C zFeuodDEJO50}1M{`V?4$2HSib4Vs-Uc#+R-mt# zbB~eac$TPinz%|LNXi=^1pfKkcroCx<3?bxsx$cmiAo~0fifgHo>PV1^9f!H7;I=L zp6EPUKU!p9iDk1 zz_8t-ZR^j42JpBkuvNRUi=V>b+8r;SPgJ#psB$q;`8@4on^VjqUJ)PmO`Q% z8ANp-ssGOP1|K+ou*}bgIe4fExcwyBWx#nM3RBHIWrr_(M%3z3R>OAXHn;^W!f;x% zS5;>x=o_~;j;X_X)m)%!JhP^$rPDA)i1yG{EYei0D(3T@yqO^m>s5{z})F*4%?|~yNv_$Dt74%4sf3!0$()&_%B-!fr}?=OzBUJ17F#taSWT8 z(hgqHn$BNB8U?Q3&@)BrN*XT7r=@|~r={CY1He`6HfvT01CtGs5O8G@ev<#S`tX6P z-(fpfzd=86QyRO?swHe#=R_Is-7GW<0q@>`xta=B*(`pPukX_jttFicpOX1onuK&A zV1P4@&yMFq*RJG4U)`e*xFNx}opbMM8o#s0V1O`iTQ=^?8d2IFa*Nj^)vYJGQ^tq; zyMpM#m!5AMw*C8U_ckhCVZ#xxNrcK zOz6Uq>d>W8o``4#Js0EG&J5qIaV_-)_C3TCTWlzpMdCeDalRw~?OmL_rF?{2yGrq7 zvP+eVWNA$$Q@eb6n1gnsz{uQ&cy?-oNc4V*6@9v`mRIz&We#8q-e)7OEsd^=h`eKaL&dy#MIe!MShp$U@|tWPbDtNBZ`| zxw({14>z@V0RYAW{rJF-Wb1DZsAG(BKY0UuAJ6z*p#rd&0u8{@eiC<8!M)>KX*uen zNRo;tlaxP5ydgWM0J!(zlqRQaeMve!jHIGbp2ulLVV-SWw+btw z_G~<8O2<`^Z%qXT0PAK<&>Rf|%KQeKynTme-FILWSC|7e7t&eE0P+muuHkP)M$m>mVg8C(TD*M5)7w$ lt=D?(KWIVz&$u49e*jBpKRv0y_F@14002ovPDHLkV1mCmAhQ4f literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 136dac82a8..7403c6a4ff 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -99,6 +99,7 @@ "dist/credentials/HubspotApi.credentials.js", "dist/credentials/HubspotDeveloperApi.credentials.js", "dist/credentials/HubspotOAuth2Api.credentials.js", + "dist/credentials/HumanticAiApi.credentials.js", "dist/credentials/HunterApi.credentials.js", "dist/credentials/Imap.credentials.js", "dist/credentials/IntercomApi.credentials.js", @@ -307,6 +308,7 @@ "dist/nodes/HttpRequest.node.js", "dist/nodes/Hubspot/Hubspot.node.js", "dist/nodes/Hubspot/HubspotTrigger.node.js", + "dist/nodes/HumanticAI/HumanticAi.node.js", "dist/nodes/Hunter/Hunter.node.js", "dist/nodes/If.node.js", "dist/nodes/Intercom/Intercom.node.js",