diff --git a/packages/nodes-base/credentials/HubspotApi.credentials.ts b/packages/nodes-base/credentials/HubspotApi.credentials.ts new file mode 100644 index 0000000000..0b149d9288 --- /dev/null +++ b/packages/nodes-base/credentials/HubspotApi.credentials.ts @@ -0,0 +1,17 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class HubspotApi implements ICredentialType { + name = 'hubspotApi'; + displayName = 'Hubspot API'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Hubspot/DealDescription.ts b/packages/nodes-base/nodes/Hubspot/DealDescription.ts new file mode 100644 index 0000000000..d37b398962 --- /dev/null +++ b/packages/nodes-base/nodes/Hubspot/DealDescription.ts @@ -0,0 +1,133 @@ +import { INodeProperties } from "n8n-workflow"; + +export const dealOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'deal', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a deal', + }, + { + name: 'Get', + value: 'get', + description: 'Get a deal', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const dealFields = [ + +/* -------------------------------------------------------------------------- */ +/* deal:create */ +/* -------------------------------------------------------------------------- */ + + { + displayName: 'Stages', + name: 'stages', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getDealStages' + }, + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + options: [], + description: 'The dealstage is required when creating a deal. See the CRM Pipelines API for details on managing pipelines and stages.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Deal Name', + name: 'dealName', + type: 'string', + default: '', + }, + { + displayName: 'Deal Stage', + name: 'dealStage', + type: 'string', + default: '', + }, + { + displayName: 'Pipeline', + name: 'pipeline', + type: 'string', + default: '', + }, + { + displayName: 'Close Date', + name: 'closeDate', + type: 'dateTime', + default: '', + }, + { + displayName: 'Amount', + name: 'amount', + type: 'string', + default: '', + }, + { + displayName: 'Deal Type', + name: 'dealType', + type: 'string', + default: '', + }, + { + displayName: 'Associated Company', + name: 'associatedCompany', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod:'getCompanies' , + }, + default: [], + }, + { + displayName: 'Associated Vids', + name: 'associatedVids', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod:'getContacts' , + }, + default: [], + }, + ] + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts b/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts new file mode 100644 index 0000000000..8cc245ffd6 --- /dev/null +++ b/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts @@ -0,0 +1,78 @@ +import { OptionsWithUri } from 'request'; + +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IExecuteSingleFunctions +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; +import { response } from 'express'; + +export async function hubspotApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, query: IDataObject = {}, uri?: string): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('hubspotApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + query!.hapikey = credentials.apiKey as string; + const options: OptionsWithUri = { + method, + qs: query, + uri: uri || `https://api.hubapi.com${endpoint}`, + body, + json: true + }; + + try { + return await this.helpers.request!(options); + } catch (error) { + const errorMessage = error.response.body.message || error.response.body.Message; + + if (errorMessage !== undefined) { + throw errorMessage; + } + throw error.response.body; + } +} + + + +/** + * Make an API request to paginated hubspot endpoint + * and return all results + */ +export async function hubspotApiRequestAllItems(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + query.limit = 250; + query.count = 100; + + do { + responseData = await hubspotApiRequest.call(this, method, endpoint, body, query); + query.offset = responseData.offset; + query['vid-offset'] = responseData['vid-offset']; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData['has-more'] !== undefined && + responseData['has-more'] !== null && + responseData['has-more'] !== false + ); + return returnData; +} + + +export function validateJSON(json: string | undefined): any { // tslint:disable-line:no-any + let result; + try { + result = JSON.parse(json!); + } catch (exception) { + result = ''; + } + return result; +} diff --git a/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts new file mode 100644 index 0000000000..d978d9038d --- /dev/null +++ b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts @@ -0,0 +1,237 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; +import { + IDataObject, + INodeTypeDescription, + INodeExecutionData, + INodeType, + ILoadOptionsFunctions, + INodePropertyOptions, +} from 'n8n-workflow'; +import { + hubspotApiRequest, + hubspotApiRequestAllItems, + validateJSON, + } from './GenericFunctions'; +import { + dealOperations, + dealFields, +} from '../Hubspot/DealDescription'; + +export class Hubspot implements INodeType { + description: INodeTypeDescription = { + displayName: 'Hubspot', + name: 'hubspot', + icon: 'file:hubspot.png', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Hubspot API', + defaults: { + name: 'Hubspot', + color: '#356ae6', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'hubspotApi', + required: true, + } + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Deal', + value: 'deal', + }, + ], + default: 'deal', + description: 'Resource to consume.', + }, + + // Deal + ...dealOperations, + ...dealFields, + ], + }; + + methods = { + loadOptions: { + // Get all the groups to display them to user so that he can + // select them easily + async getDealStages(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + let stages; + try { + const endpoint = '/crm-pipelines/v1/pipelines/deals'; + stages = await hubspotApiRequest.call(this, 'GET', endpoint, {}); + stages = stages.results[0].stages; + console.log(stages) + } catch (err) { + throw new Error(`Hubspot Error: ${err}`); + } + for (const stage of stages) { + const stageName = stage.label; + const stageId = stage.stageId; + returnData.push({ + name: stageName, + value: stageId, + }); + } + return returnData; + }, + + // Get all the companies to display them to user so that he can + // select them easily + async getCompanies(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + let companies; + try { + const endpoint = '/companies/v2/companies/paged'; + companies = await hubspotApiRequestAllItems.call(this, 'results', 'GET', endpoint); + } catch (err) { + throw new Error(`Hubspot Error: ${err}`); + } + for (const company of companies) { + const companyName = company.properties.name.value; + const companyId = company.companyId; + returnData.push({ + name: companyName, + value: companyId, + }); + } + return returnData; + }, + + // Get all the companies to display them to user so that he can + // select them easily + async getContacts(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + let contacts; + try { + const endpoint = '/contacts/v1/lists/all/contacts/all'; + contacts = await hubspotApiRequestAllItems.call(this, 'contacts', 'GET', endpoint); + } catch (err) { + throw new Error(`Hubspot Error: ${err}`); + } + for (const contact of contacts) { + const contactName = `${contact.properties.firstname.value} ${contact.properties.lastname.value}` ; + const contactId = contact.vid; + returnData.push({ + name: contactName, + value: contactId, + }); + } + return returnData; + } + } + }; + + async execute(this: IExecuteFunctions): Promise { + // const items = this.getInputData(); + // const returnData: IDataObject[] = []; + // const length = items.length as unknown as number; + // let responseData; + // const qs: IDataObject = {}; + + // 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 === 'payout') { + // if (operation === 'create') { + // const body: IPaymentBatch = {}; + // const header: ISenderBatchHeader = {}; + // const jsonActive = this.getNodeParameter('jsonParameters', i) as boolean; + // const senderBatchId = this.getNodeParameter('senderBatchId', i) as string; + // const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + // header.sender_batch_id = senderBatchId; + // if (additionalFields.emailSubject) { + // header.email_subject = additionalFields.emailSubject as string; + // } + // if (additionalFields.emailMessage) { + // header.email_message = additionalFields.emailMessage as string; + // } + // if (additionalFields.note) { + // header.note = additionalFields.note as string; + // } + // body.sender_batch_header = header; + // if (!jsonActive) { + // const payoutItems: IItem[] = []; + // const itemsValues = (this.getNodeParameter('itemsUi', i) as IDataObject).itemsValues as IDataObject[]; + // if (itemsValues && itemsValues.length > 0) { + // itemsValues.forEach(o => { + // const payoutItem: IItem = {}; + // const amount: IAmount = {}; + // amount.currency = o.currency as string; + // amount.value = parseFloat(o.amount as string); + // payoutItem.amount = amount; + // payoutItem.note = o.note as string || ''; + // payoutItem.receiver = o.receiverValue as string; + // payoutItem.recipient_type = o.recipientType as RecipientType; + // payoutItem.recipient_wallet = o.recipientWallet as RecipientWallet; + // payoutItem.sender_item_id = o.senderItemId as string || ''; + // payoutItems.push(payoutItem); + // }); + // body.items = payoutItems; + // } else { + // throw new Error('You must have at least one item.'); + // } + // } else { + // const itemsJson = validateJSON(this.getNodeParameter('itemsJson', i) as string); + // body.items = itemsJson; + // } + // try { + // responseData = await payPalApiRequest.call(this, '/payments/payouts', 'POST', body); + // } catch (err) { + // throw new Error(`PayPal Error: ${JSON.stringify(err)}`); + // } + // } + // if (operation === 'get') { + // const payoutBatchId = this.getNodeParameter('payoutBatchId', i) as string; + // const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + // try { + // if (returnAll === true) { + // responseData = await payPalApiRequestAllItems.call(this, 'items', `/payments/payouts/${payoutBatchId}`, 'GET', {}, qs); + // } else { + // qs.page_size = this.getNodeParameter('limit', i) as number; + // responseData = await payPalApiRequest.call(this, `/payments/payouts/${payoutBatchId}`, 'GET', {}, qs); + // responseData = responseData.items; + // } + // } catch (err) { + // throw new Error(`PayPal Error: ${JSON.stringify(err)}`); + // } + // } + // } else if (resource === 'payoutItem') { + // if (operation === 'get') { + // const payoutItemId = this.getNodeParameter('payoutItemId', i) as string; + // try { + // responseData = await payPalApiRequest.call(this,`/payments/payouts-item/${payoutItemId}`, 'GET', {}, qs); + // } catch (err) { + // throw new Error(`PayPal Error: ${JSON.stringify(err)}`); + // } + // } + // if (operation === 'cancel') { + // const payoutItemId = this.getNodeParameter('payoutItemId', i) as string; + // try { + // responseData = await payPalApiRequest.call(this,`/payments/payouts-item/${payoutItemId}/cancel`, 'POST', {}, qs); + // } catch (err) { + // throw new Error(`PayPal Error: ${JSON.stringify(err)}`); + // } + // } + // } + // if (Array.isArray(responseData)) { + // returnData.push.apply(returnData, responseData as IDataObject[]); + // } else { + // returnData.push(responseData as IDataObject); + // } + // } + return [this.helpers.returnJsonArray({})]; + } +} diff --git a/packages/nodes-base/nodes/Hubspot/hubspot.png b/packages/nodes-base/nodes/Hubspot/hubspot.png new file mode 100644 index 0000000000..4b15a3edfc Binary files /dev/null and b/packages/nodes-base/nodes/Hubspot/hubspot.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 60f74c27c2..552385284a 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -41,9 +41,10 @@ "dist/credentials/HttpBasicAuth.credentials.js", "dist/credentials/HttpDigestAuth.credentials.js", "dist/credentials/HttpHeaderAuth.credentials.js", + "dist/credentials/HubspotApi.credentials.js", "dist/credentials/IntercomApi.credentials.js", - "dist/credentials/Imap.credentials.js", - "dist/credentials/JiraSoftwareCloudApi.credentials.js", + "dist/credentials/Imap.credentials.js", + "dist/credentials/JiraSoftwareCloudApi.credentials.js", "dist/credentials/LinkFishApi.credentials.js", "dist/credentials/MailchimpApi.credentials.js", "dist/credentials/MailgunApi.credentials.js", @@ -100,6 +101,7 @@ "dist/nodes/Google/GoogleSheets.node.js", "dist/nodes/GraphQL/GraphQL.node.js", "dist/nodes/HttpRequest.node.js", + "dist/nodes/Hubspot/Hubspot.node.js", "dist/nodes/If.node.js", "dist/nodes/Interval.node.js", "dist/nodes/Intercom/Intercom.node.js",