From 755af2e41f8f70fbdbc810144fc54c0d78203b68 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Wed, 29 Dec 2021 17:36:25 -0500 Subject: [PATCH] :sparkles: Add engagement resource - Hubspot Node (#2615) * :zap: Add engagement resource * :zap: Improvements * :bug: Fix forObjectType:contact Co-authored-by: Jan Oberhauser --- .../nodes/Hubspot/EngagementDescription.ts | 548 ++++++++++++++++++ .../nodes/Hubspot/GenericFunctions.ts | 73 ++- .../nodes-base/nodes/Hubspot/Hubspot.node.ts | 98 +++- 3 files changed, 714 insertions(+), 5 deletions(-) create mode 100644 packages/nodes-base/nodes/Hubspot/EngagementDescription.ts diff --git a/packages/nodes-base/nodes/Hubspot/EngagementDescription.ts b/packages/nodes-base/nodes/Hubspot/EngagementDescription.ts new file mode 100644 index 0000000000000..dbe09ff0172f6 --- /dev/null +++ b/packages/nodes-base/nodes/Hubspot/EngagementDescription.ts @@ -0,0 +1,548 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const engagementOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'engagement', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create an engagement', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an engagement', + }, + { + name: 'Get', + value: 'get', + description: 'Get an engagement', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all engagements', + }, + ], + default: 'create', + description: 'The operation to perform', + }, +]; + +export const engagementFields: INodeProperties[] = [ + + /* -------------------------------------------------------------------------- */ + /* engagement:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Type', + name: 'type', + type: 'options', + required: true, + options: [ + { + name: 'Call', + value: 'call', + }, + { + name: 'Email', + value: 'email', + }, + { + name: 'Meeting', + value: 'meeting', + }, + { + name: 'Task', + value: 'task', + }, + ], + displayOptions: { + show: { + resource: [ + 'engagement', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + }, + { + displayName: 'Metadata', + name: 'metadata', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'engagement', + ], + operation: [ + 'create', + ], + type: [ + 'task', + ], + }, + }, + options: [ + { + displayName: 'Body', + name: 'body', + type: 'string', + default: '', + description: '', + }, + { + displayName: 'For Object Type', + name: 'forObjectType', + type: 'options', + options: [ + { + name: 'Company', + value: 'COMPANY', + }, + { + name: 'Contact', + value: 'CONTACT', + }, + ], + default: '', + description: '', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Not Started', + value: 'NOT_STARTED', + }, + { + name: 'In Progress', + value: 'IN_PROGRESS', + }, + { + name: 'Waiting', + value: 'WAITING', + }, + { + name: 'Completed', + value: 'COMPLETED', + }, + { + name: 'Deferred', + value: 'DEFERRED', + }, + ], + default: '', + description: '', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'string', + default: '', + description: '', + }, + ], + }, + { + displayName: 'Metadata', + name: 'metadata', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'engagement', + ], + operation: [ + 'create', + ], + type: [ + 'email', + ], + }, + }, + options: [ + { + displayName: 'BCC', + name: 'bcc', + type: 'string', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add BCC', + }, + default: '', + description: '', + }, + { + displayName: 'CC', + name: 'cc', + type: 'string', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add CC', + }, + default: '', + description: '', + }, + { + displayName: 'From Email', + name: 'fromEmail', + type: 'string', + default: '', + }, + { + displayName: 'From First Name', + name: 'firstName', + type: 'string', + default: '', + description: '', + }, + { + displayName: 'From Last Name', + name: 'lastName', + type: 'string', + default: '', + description: '', + }, + { + displayName: 'HTML', + name: 'html', + type: 'string', + default: '', + description: '', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'string', + default: '', + description: '', + }, + { + displayName: 'To Emails', + name: 'toEmail', + type: 'string', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add Email', + }, + default: '', + description: '', + }, + ], + }, + { + displayName: 'Metadata', + name: 'metadata', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'engagement', + ], + operation: [ + 'create', + ], + type: [ + 'meeting', + ], + }, + }, + options: [ + { + displayName: 'Body', + name: 'body', + type: 'string', + default: '', + description: '', + }, + { + displayName: 'End Time', + name: 'endTime', + type: 'dateTime', + default: '', + description: '', + }, + { + displayName: 'Internal Meeting Notes', + name: 'internalMeetingNotes', + type: 'string', + default: '', + description: '', + }, + { + displayName: 'Start Time', + name: 'startTime', + type: 'dateTime', + default: '', + description: '', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: '', + }, + ], + }, + { + displayName: 'Metadata', + name: 'metadata', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'engagement', + ], + operation: [ + 'create', + ], + type: [ + 'call', + ], + }, + }, + options: [ + { + displayName: 'Body', + name: 'body', + type: 'string', + default: '', + description: '', + }, + { + displayName: 'Duration Milliseconds', + name: 'durationMilliseconds', + type: 'number', + default: 0, + description: '', + }, + { + displayName: 'From Number', + name: 'fromNumber', + type: 'string', + default: '', + description: '', + }, + { + displayName: 'Recording URL', + name: 'recordingUrl', + type: 'string', + default: '', + description: '', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Queued', + value: 'QUEUED', + }, + { + name: 'Ringing', + value: 'RINGING', + }, + { + name: 'In Progress', + value: 'IN_PROGRESS', + }, + { + name: 'Canceled', + value: 'CANCELED', + }, + { + name: 'Completed', + value: 'COMPLETED', + }, + { + name: 'Busy', + value: 'BUSY', + }, + { + name: 'Failed', + value: 'FAILED', + }, + { + name: 'No Answer', + value: 'NO_ANSWER', + }, + { + name: 'Connecting', + value: 'CONNECTING', + }, + { + name: 'Calling CRM User', + value: 'CALLING_CRM_USER', + }, + ], + default: 'QUEUED', + description: '', + }, + { + displayName: 'To Number', + name: 'toNumber', + type: 'string', + default: '', + description: '', + }, + ], + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'engagement', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Associations', + name: 'associations', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Company IDs', + name: 'companyIds', + type: 'string', + default: '', + description: '', + }, + { + displayName: 'Contact IDs', + name: 'contactIds', + type: 'string', + default: '', + description: '', + }, + { + displayName: 'Deals IDs', + name: 'dealIds', + type: 'string', + default: '', + description: '', + }, + { + displayName: 'Owner IDs', + name: 'ownerIds', + type: 'string', + default: '', + description: '', + }, + { + displayName: 'Ticket IDs', + name: 'ticketIds', + type: 'string', + default: '', + description: '', + }, + ], + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* engagement:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Engagement ID', + name: 'engagementId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'engagement', + ], + operation: [ + 'get', + 'delete', + ], + }, + }, + default: '', + description: 'Unique identifier for a particular engagement', + }, + /* -------------------------------------------------------------------------- */ + /* engagement:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'engagement', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'engagement', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 250, + }, + default: 100, + description: 'How many results to return', + }, +]; diff --git a/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts b/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts index bebad7777f1b5..b390486b10777 100644 --- a/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts @@ -14,6 +14,8 @@ import { NodeApiError, } from 'n8n-workflow'; +import * as moment from 'moment'; + 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 let authenticationMethod = this.getNodeParameter('authentication', 0); @@ -36,7 +38,6 @@ export async function hubspotApiRequest(this: IHookFunctions | IExecuteFunctions const credentials = await this.getCredentials('hubspotApi'); options.qs.hapikey = credentials!.apiKey as string; - return await this.helpers.request!(options); } else if (authenticationMethod === 'developerApi') { if (endpoint.includes('webhooks')) { @@ -1892,3 +1893,73 @@ export const dealFields = [ 'label': 'Closed Won Reason', }, ]; + +const reduceMetadatFields = (data: string[]) => { + return data.reduce((a, v) => { + //@ts-ignore + a.push(...v.split(',')); + return a; + }, []).map(email => ({ email })); +}; + +export const getEmailMetadata = (meta: IDataObject) => { + return { + from: { + ...(meta.fromEmail && { email: meta.fromEmail }), + ...(meta.firstName && { firstName: meta.firstName }), + ...(meta.lastName && { lastName: meta.lastName }), + }, + cc: reduceMetadatFields(meta.cc as string[] || []), + bcc: reduceMetadatFields(meta.bcc as string[] || []), + ...(meta.subject && { subject: meta.subject }), + ...(meta.html && { html: meta.html }), + ...(meta.text && { text: meta.text }), + }; +}; + +export const getTaskMetadata = (meta: IDataObject) => { + return { + ...(meta.body && { body: meta.body }), + ...(meta.subject && { subject: meta.subject }), + ...(meta.status && { status: meta.status }), + ...(meta.forObjectType && { forObjectType: meta.forObjectType }), + }; +}; + +export const getMeetingMetadata = (meta: IDataObject) => { + return { + ...(meta.body && { body: meta.body }), + ...(meta.startTime && { startTime: moment(meta.startTime as string).unix() }), + ...(meta.endTime && { endTime: moment(meta.endTime as string).unix() }), + ...(meta.title && { title: meta.title }), + ...(meta.internalMeetingNotes && { internalMeetingNotes: meta.internalMeetingNotes }), + }; +}; + +export const getCallMetadata = (meta: IDataObject) => { + return { + ...(meta.toNumber && { toNumber: meta.toNumber }), + ...(meta.fromNumber && { fromNumber: meta.fromNumber }), + ...(meta.status && { status: meta.status }), + ...(meta.durationMilliseconds && { durationMilliseconds: meta.durationMilliseconds }), + ...(meta.recordingUrl && { recordingUrl: meta.recordingUrl }), + ...(meta.body && { body: meta.body }), + }; +}; + + +export const getAssociations = (associations: { + companyIds: string, + dealIds: string, + ownerIds: string, + contactIds: string, + ticketIds: string; +}) => { + return { + ...(associations.companyIds && { companyIds: associations.companyIds.toString().split(',') }), + ...(associations.contactIds && { contactIds: associations.contactIds.toString().split(',') }), + ...(associations.dealIds && { dealIds: associations.dealIds.toString().split(',') }), + ...(associations.ownerIds && { ownerIds: associations.ownerIds.toString().split(',') }), + ...(associations.ticketIds && { ticketIds: associations.ticketIds.toString().split(',') }), + }; +}; diff --git a/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts index d1ffd91177137..298f6e7aafc74 100644 --- a/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts +++ b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts @@ -9,10 +9,16 @@ import { INodePropertyOptions, INodeType, INodeTypeDescription, + NodeOperationError, } from 'n8n-workflow'; import { clean, + getAssociations, + getCallMetadata, + getEmailMetadata, + getMeetingMetadata, + getTaskMetadata, hubspotApiRequest, hubspotApiRequestAllItems, } from './GenericFunctions'; @@ -37,6 +43,11 @@ import { dealOperations, } from './DealDescription'; +import { + engagementFields, + engagementOperations, +} from './EngagementDescription'; + import { formFields, formOperations, @@ -137,6 +148,10 @@ export class Hubspot implements INodeType { name: 'Deal', value: 'deal', }, + { + name: 'Engagement', + value: 'engagement', + }, { name: 'Form', value: 'form', @@ -161,6 +176,9 @@ export class Hubspot implements INodeType { // DEAL ...dealOperations, ...dealFields, + // ENGAGEMENT + ...engagementOperations, + ...engagementFields, // FORM ...formOperations, ...formFields, @@ -913,7 +931,7 @@ export class Hubspot implements INodeType { } else { for (let i = 0; i < length; i++) { try { - //https://developers.hubspot.com/docs/methods/contacts/create_or_update + //https://developers.hubspot.com/docs/methods/contacts/create_or_update if (resource === 'contact') { //https://developers.hubspot.com/docs/methods/companies/create_company if (operation === 'upsert') { @@ -1345,11 +1363,9 @@ export class Hubspot implements INodeType { const endpoint = '/crm/v3/objects/contacts/search'; if (returnAll) { - responseData = await hubspotApiRequestAllItems.call(this, 'results', 'POST', endpoint, body, qs); - } else { - qs.count = this.getNodeParameter('limit', 0) as number; + body.limit = this.getNodeParameter('limit', 0) as number; responseData = await hubspotApiRequest.call(this, 'POST', endpoint, body, qs); responseData = responseData.results; } @@ -2123,6 +2139,80 @@ export class Hubspot implements INodeType { } } } + if (resource === 'engagement') { + //https://legacydocs.hubspot.com/docs/methods/engagements/create_engagement + if (operation === 'create') { + const type = this.getNodeParameter('type', i) as string; + const metadata = this.getNodeParameter('metadata', i) as IDataObject; + const associations = this.getNodeParameter('additionalFields.associations', i, {}) as IDataObject; + + if (!Object.keys(metadata).length) { + throw new NodeOperationError( + this.getNode(), + `At least one metadata field needs to set`, + ); + } + + const body: { + engagement: { type: string }, + metadata: IDataObject, + associations: IDataObject + } = { + engagement: { + type: type.toUpperCase(), + }, + metadata: {}, + associations: {}, + }; + + if (type === 'email') { + body.metadata = getEmailMetadata(metadata); + } + + if (type === 'task') { + body.metadata = getTaskMetadata(metadata); + } + + if (type === 'meeting') { + body.metadata = getMeetingMetadata(metadata); + } + + if (type === 'call') { + body.metadata = getCallMetadata(metadata); + } + + //@ts-ignore + body.associations = getAssociations(associations); + + const endpoint = '/engagements/v1/engagements'; + responseData = await hubspotApiRequest.call(this, 'POST', endpoint, body); + } + //https://legacydocs.hubspot.com/docs/methods/engagements/get_engagement + if (operation === 'delete') { + const engagementId = this.getNodeParameter('engagementId', i) as string; + const endpoint = `/engagements/v1/engagements/${engagementId}`; + responseData = await hubspotApiRequest.call(this, 'DELETE', endpoint, {}, qs); + responseData = { success: true }; + } + //https://legacydocs.hubspot.com/docs/methods/engagements/get_engagement + if (operation === 'get') { + const engagementId = this.getNodeParameter('engagementId', i) as string; + const endpoint = `/engagements/v1/engagements/${engagementId}`; + responseData = await hubspotApiRequest.call(this, 'GET', endpoint, {}, qs); + } + //https://legacydocs.hubspot.com/docs/methods/engagements/get-all-engagements + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + const endpoint = `/engagements/v1/engagements/paged`; + if (returnAll) { + responseData = await hubspotApiRequestAllItems.call(this, 'results', 'GET', endpoint, {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', 0) as number; + responseData = await hubspotApiRequest.call(this, 'GET', endpoint, {}, qs); + responseData = responseData.results; + } + } + } //https://developers.hubspot.com/docs/methods/forms/forms_overview if (resource === 'form') { //https://developers.hubspot.com/docs/methods/forms/v2/get_fields