From 8026bb1c9af61298c7ec9b1bd79c61bcad53c44a Mon Sep 17 00:00:00 2001 From: ricardo Date: Wed, 8 Apr 2020 00:35:50 -0400 Subject: [PATCH 1/2] :zap: Added ticket resource --- .../nodes/Hubspot/DealDescription.ts | 612 +++++++++--------- .../nodes-base/nodes/Hubspot/DealInterface.ts | 5 +- .../nodes/Hubspot/FormDescription.ts | 4 +- .../nodes-base/nodes/Hubspot/FormInterface.ts | 4 +- .../nodes/Hubspot/GenericFunctions.ts | 11 +- .../nodes-base/nodes/Hubspot/Hubspot.node.ts | 504 +++++++++++++-- .../nodes/Hubspot/HubspotTrigger.node.ts | 4 +- .../nodes/Hubspot/TicketDescription.ts | 555 ++++++++++++++++ 8 files changed, 1340 insertions(+), 359 deletions(-) create mode 100644 packages/nodes-base/nodes/Hubspot/TicketDescription.ts diff --git a/packages/nodes-base/nodes/Hubspot/DealDescription.ts b/packages/nodes-base/nodes/Hubspot/DealDescription.ts index 3f4f7c863f..a25e09e7d3 100644 --- a/packages/nodes-base/nodes/Hubspot/DealDescription.ts +++ b/packages/nodes-base/nodes/Hubspot/DealDescription.ts @@ -1,4 +1,6 @@ -import { INodeProperties } from "n8n-workflow"; +import { + INodeProperties, + } from 'n8n-workflow'; export const dealOperations = [ { @@ -19,9 +21,9 @@ export const dealOperations = [ description: 'Create a deal', }, { - name: 'Update', - value: 'update', - description: 'Update a deal', + name: 'Delete', + value: 'delete', + description: 'Delete a deals', }, { name: 'Get', @@ -33,11 +35,6 @@ export const dealOperations = [ value: 'getAll', description: 'Get all deals', }, - { - name: 'Delete', - value: 'delete', - description: 'Delete a deals', - }, { name: 'Get Recently Created', value: 'getRecentlyCreated', @@ -48,6 +45,11 @@ export const dealOperations = [ value: 'getRecentlyModified', description: 'Get recently modified deals', }, + { + name: 'Update', + value: 'update', + description: 'Update a deal', + }, ], default: 'create', description: 'The operation to perform.', @@ -57,7 +59,7 @@ export const dealOperations = [ export const dealFields = [ /* -------------------------------------------------------------------------- */ -/* deal:create */ +/* deal:create */ /* -------------------------------------------------------------------------- */ { displayName: 'Deal Stage', @@ -160,330 +162,330 @@ export const dealFields = [ /* -------------------------------------------------------------------------- */ /* deal:update */ /* -------------------------------------------------------------------------- */ -{ - displayName: 'Deal ID', - name: 'dealId', - type: 'string', - required: true, - displayOptions: { - show: { - resource: [ - 'deal', - ], - operation: [ - 'update', - ], - }, - }, - default: '', - description: 'Unique identifier for a particular deal', -}, -{ - displayName: 'Update Fields', - name: 'updateFields', - type: 'collection', - placeholder: 'Add Update Field', - default: {}, - displayOptions: { - show: { - resource: [ - 'deal', - ], - operation: [ - 'update', - ], - }, - }, - options: [ - { - displayName: 'Deal Name', - name: 'dealName', - type: 'string', - default: '', - }, - { - displayName: 'Deal Stage', - name: 'stage', - type: 'options', - required: true, - typeOptions: { - loadOptionsMethod: 'getDealStages' + { + displayName: 'Deal ID', + name: 'dealId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'update', + ], }, - default: '', - description: 'The dealstage is required when creating a deal. See the CRM Pipelines API for details on managing pipelines and stages.', }, - { - 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: 'options', - typeOptions: { - loadOptionsMethod: 'getDealTypes', + default: '', + description: 'Unique identifier for a particular deal', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Update Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'update', + ], }, - default: '', }, - ] -}, + options: [ + { + displayName: 'Deal Name', + name: 'dealName', + type: 'string', + default: '', + }, + { + displayName: 'Deal Stage', + name: 'stage', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getDealStages' + }, + default: '', + description: 'The dealstage is required when creating a deal. See the CRM Pipelines API for details on managing pipelines and stages.', + }, + { + 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: 'options', + typeOptions: { + loadOptionsMethod: 'getDealTypes', + }, + default: '', + }, + ] + }, /* -------------------------------------------------------------------------- */ /* deal:get */ /* -------------------------------------------------------------------------- */ -{ - displayName: 'Deal ID', - name: 'dealId', - type: 'string', - required: true, - displayOptions: { - show: { - resource: [ - 'deal', - ], - operation: [ - 'get', - ], + { + displayName: 'Deal ID', + name: 'dealId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'get', + ], + }, }, + default: '', + description: 'Unique identifier for a particular deal', }, - default: '', - description: 'Unique identifier for a particular deal', -}, -{ - displayName: 'Additional Fields', - name: 'additionalFields', - type: 'collection', - placeholder: 'Add Field', - default: {}, - displayOptions: { - show: { - resource: [ - 'deal', - ], - operation: [ - 'get', - ], + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'get', + ], + }, }, + options: [ + { + displayName: 'Include Property Versions ', + name: 'includePropertyVersions', + type: 'boolean', + default: false, + description: `By default, you will only get data for the most recent version of a property in the "versions" data.
+ If you include this parameter, you will get data for all previous versions.`, + }, + ] }, - options: [ - { - displayName: 'Include Property Versions ', - name: 'includePropertyVersions', - type: 'boolean', - default: false, - description: `By default, you will only get data for the most recent version of a property in the "versions" data.
- If you include this parameter, you will get data for all previous versions.`, - }, - ] -}, /* -------------------------------------------------------------------------- */ /* deal:getAll */ /* -------------------------------------------------------------------------- */ -{ - displayName: 'Return All', - name: 'returnAll', - type: 'boolean', - displayOptions: { - show: { - resource: [ - 'deal', - ], - operation: [ - 'getAll', - ], + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'getAll', + ], + }, }, + default: false, + description: 'If all results should be returned or only up to a given limit.', }, - 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: [ - 'deal', - ], - operation: [ - 'getAll', - ], - returnAll: [ - false, - ], + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, }, + typeOptions: { + minValue: 1, + maxValue: 250, + }, + default: 100, + description: 'How many results to return.', }, - typeOptions: { - minValue: 1, - maxValue: 250, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Include Associations', + name: 'includeAssociations', + type: 'boolean', + default: false, + description: `Include the IDs of the associated contacts and companies in the results
. + This will also automatically include the num_associated_contacts property.`, + }, + { + displayName: 'Properties', + name: 'properties', + type: 'string', + default: '', + description: `Used to include specific deal properties in the results.
+ By default, the results will only include Deal ID and will not include the values for any properties for your Deals.
+ Including this parameter will include the data for the specified property in the results.
+ You can include this parameter multiple times to request multiple properties separed by ,.`, + }, + { + displayName: 'Properties With History', + name: 'propertiesWithHistory', + type: 'string', + default: '', + description: `Works similarly to properties=, but this parameter will include the history for the specified property,
+ instead of just including the current value. Use this parameter when you need the full history of changes to a property's value.`, + }, + ] }, - default: 100, - description: 'How many results to return.', -}, -{ - displayName: 'Filters', - name: 'filters', - type: 'collection', - placeholder: 'Add Filter', - default: {}, - displayOptions: { - show: { - resource: [ - 'deal', - ], - operation: [ - 'getAll', - ], - }, - }, - options: [ - { - displayName: 'Include Associations', - name: 'includeAssociations', - type: 'boolean', - default: false, - description: `Include the IDs of the associated contacts and companies in the results
. - This will also automatically include the num_associated_contacts property.`, - }, - { - displayName: 'Properties', - name: 'properties', - type: 'string', - default: '', - description: `Used to include specific deal properties in the results.
- By default, the results will only include Deal ID and will not include the values for any properties for your Deals.
- Including this parameter will include the data for the specified property in the results.
- You can include this parameter multiple times to request multiple properties separed by ,.`, - }, - { - displayName: 'Properties With History', - name: 'propertiesWithHistory', - type: 'string', - default: '', - description: `Works similarly to properties=, but this parameter will include the history for the specified property,
- instead of just including the current value. Use this parameter when you need the full history of changes to a property's value.`, - }, - ] -}, /* -------------------------------------------------------------------------- */ /* deal:delete */ /* -------------------------------------------------------------------------- */ -{ - displayName: 'Deal ID', - name: 'dealId', - type: 'string', - required: true, - displayOptions: { - show: { - resource: [ - 'deal', - ], - operation: [ - 'delete', - ], + { + displayName: 'Deal ID', + name: 'dealId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'delete', + ], + }, }, + default: '', + description: 'Unique identifier for a particular deal', }, - default: '', - description: 'Unique identifier for a particular deal', -}, /* -------------------------------------------------------------------------- */ -/* deal:getRecentlyCreated deal:getRecentlyModified */ +/* deal:getRecentlyCreated deal:getRecentlyModified */ /* -------------------------------------------------------------------------- */ -{ - displayName: 'Return All', - name: 'returnAll', - type: 'boolean', - displayOptions: { - show: { - resource: [ - 'deal', - ], - operation: [ - 'getRecentlyCreated', - 'getRecentlyModified', - ], + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'getRecentlyCreated', + 'getRecentlyModified', + ], + }, }, + default: false, + description: 'If all results should be returned or only up to a given limit.', }, - 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: [ - 'deal', - ], - operation: [ - 'getRecentlyCreated', - 'getRecentlyModified', - ], - returnAll: [ - false, - ], + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'getRecentlyCreated', + 'getRecentlyModified', + ], + returnAll: [ + false, + ], + }, }, + typeOptions: { + minValue: 1, + maxValue: 250, + }, + default: 100, + description: 'How many results to return.', }, - typeOptions: { - minValue: 1, - maxValue: 250, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'getRecentlyCreated', + 'getRecentlyModified', + ], + }, + }, + options: [ + { + displayName: 'Since', + name: 'since', + type: 'dateTime', + default: '', + description: `Only return deals created after timestamp x`, + }, + { + displayName: 'Include Property Versions', + name: 'includePropertyVersions', + type: 'boolean', + default: false, + description: `By default, you will only get data for the most recent version of a property in the "versions" data.
+ If you include this parameter, you will get data for all previous versions.`, + }, + ] }, - default: 100, - description: 'How many results to return.', -}, -{ - displayName: 'Filters', - name: 'filters', - type: 'collection', - placeholder: 'Add Filter', - default: {}, - displayOptions: { - show: { - resource: [ - 'deal', - ], - operation: [ - 'getRecentlyCreated', - 'getRecentlyModified', - ], - }, - }, - options: [ - { - displayName: 'Since', - name: 'since', - type: 'dateTime', - default: '', - description: `Only return deals created after timestamp x`, - }, - { - displayName: 'Include Property Versions', - name: 'includePropertyVersions', - type: 'boolean', - default: false, - description: `By default, you will only get data for the most recent version of a property in the "versions" data.
- If you include this parameter, you will get data for all previous versions.`, - }, - ] -}, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Hubspot/DealInterface.ts b/packages/nodes-base/nodes/Hubspot/DealInterface.ts index 92a41d6c95..fe422ff5be 100644 --- a/packages/nodes-base/nodes/Hubspot/DealInterface.ts +++ b/packages/nodes-base/nodes/Hubspot/DealInterface.ts @@ -1,5 +1,6 @@ -import { IDataObject } from "n8n-workflow"; - +import { + IDataObject, + } from 'n8n-workflow'; export interface IAssociation { associatedCompanyIds?: number[]; diff --git a/packages/nodes-base/nodes/Hubspot/FormDescription.ts b/packages/nodes-base/nodes/Hubspot/FormDescription.ts index ab77db9585..15dd2592af 100644 --- a/packages/nodes-base/nodes/Hubspot/FormDescription.ts +++ b/packages/nodes-base/nodes/Hubspot/FormDescription.ts @@ -1,4 +1,6 @@ -import { INodeProperties } from "n8n-workflow"; +import { + INodeProperties, + } from 'n8n-workflow'; export const formOperations = [ { diff --git a/packages/nodes-base/nodes/Hubspot/FormInterface.ts b/packages/nodes-base/nodes/Hubspot/FormInterface.ts index 8b3b424f7a..98edbf67ff 100644 --- a/packages/nodes-base/nodes/Hubspot/FormInterface.ts +++ b/packages/nodes-base/nodes/Hubspot/FormInterface.ts @@ -1,4 +1,6 @@ -import { IDataObject } from "n8n-workflow"; +import { + IDataObject, + } from 'n8n-workflow'; export interface IContext { goToWebinarWebinarKey?: string; diff --git a/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts b/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts index 454b604438..df1ae184a2 100644 --- a/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts @@ -1,10 +1,12 @@ -import { OptionsWithUri } from 'request'; +import { + OptionsWithUri, + } from 'request'; import { IExecuteFunctions, IHookFunctions, ILoadOptionsFunctions, - IExecuteSingleFunctions + IExecuteSingleFunctions, } from 'n8n-core'; import { @@ -50,7 +52,7 @@ export async function hubspotApiRequestAllItems(this: IHookFunctions | IExecuteF let responseData; - query.limit = 250; + query.limit = query.limit || 250; query.count = 100; do { @@ -58,6 +60,9 @@ export async function hubspotApiRequestAllItems(this: IHookFunctions | IExecuteF query.offset = responseData.offset; query['vid-offset'] = responseData['vid-offset']; returnData.push.apply(returnData, responseData[propertyName]); + if (query.limit && query.limit <= returnData.length) { + return returnData; + } } while ( responseData['has-more'] !== undefined && responseData['has-more'] !== null && diff --git a/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts index 9b88c91162..bde5f7931e 100644 --- a/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts +++ b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts @@ -21,20 +21,25 @@ import { dealFields, } from './DealDescription'; -import { - IDeal, - IAssociation -} from './DealInterface'; - import { formOperations, formFields, } from './FormDescription'; + import { + ticketOperations, + ticketFields, +} from './TicketDescription'; + import { - IForm + IForm, } from './FormInterface'; +import { + IDeal, + IAssociation, +} from './DealInterface'; + export class Hubspot implements INodeType { description: INodeTypeDescription = { displayName: 'Hubspot', @@ -70,22 +75,31 @@ export class Hubspot implements INodeType { name: 'Form', value: 'form', }, + { + name: 'Ticket', + value: 'ticket', + }, ], default: 'deal', description: 'Resource to consume.', }, - - // Deal + // DEAL ...dealOperations, ...dealFields, - // Form + // FORM ...formOperations, ...formFields, + // TICKET + ...ticketOperations, + ...ticketFields, ], }; methods = { loadOptions: { + /* -------------------------------------------------------------------------- */ + /* DEAL */ + /* -------------------------------------------------------------------------- */ // Get all the groups to display them to user so that he can // select them easily @@ -104,41 +118,6 @@ export class Hubspot implements INodeType { } 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[] = []; - const endpoint = '/companies/v2/companies/paged'; - const companies = await hubspotApiRequestAllItems.call(this, 'results', 'GET', endpoint); - 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[] = []; - const endpoint = '/contacts/v1/lists/all/contacts/all'; - const contacts = await hubspotApiRequestAllItems.call(this, 'contacts', 'GET', endpoint); - 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; - }, - // Get all the deal types to display them to user so that he can // select them easily async getDealTypes(this: ILoadOptionsFunctions): Promise { @@ -156,6 +135,10 @@ export class Hubspot implements INodeType { return returnData; }, + /* -------------------------------------------------------------------------- */ + /* FORM */ + /* -------------------------------------------------------------------------- */ + // Get all the forms to display them to user so that he can // select them easily async getForms(this: ILoadOptionsFunctions): Promise { @@ -172,7 +155,6 @@ export class Hubspot implements INodeType { } return returnData; }, - // Get all the subscription types to display them to user so that he can // select them easily async getSubscriptionTypes(this: ILoadOptionsFunctions): Promise { @@ -189,7 +171,201 @@ export class Hubspot implements INodeType { } return returnData; }, - } + + /* -------------------------------------------------------------------------- */ + /* TICKET */ + /* -------------------------------------------------------------------------- */ + + // Get all the ticket categories to display them to user so that he can + // select them easily + async getTicketCategories(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const endpoint = '/properties/v2/tickets/properties'; + const properties = await hubspotApiRequest.call(this, 'GET', endpoint, {}); + for (const property of properties) { + if (property.name === 'hs_ticket_category') { + for (const option of property.options) { + const categoryName = option.label; + const categoryId = option.value; + returnData.push({ + name: categoryName, + value: categoryId, + }); + } + } + } + return returnData.sort((a, b) => a.name < b.name ? 0 : 1); + }, + // Get all the ticket pipelines to display them to user so that he can + // select them easily + async getTicketPipelines(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const endpoint = '/crm-pipelines/v1/pipelines/tickets'; + const { results } = await hubspotApiRequest.call(this, 'GET', endpoint, {}); + for (const pipeline of results) { + const pipelineName = pipeline.label; + const pipelineId = pipeline.pipelineId; + returnData.push({ + name: pipelineName, + value: pipelineId, + }); + } + return returnData; + }, + // Get all the ticket resolutions to display them to user so that he can + // select them easily + async getTicketPriorities(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const endpoint = '/properties/v2/tickets/properties'; + const properties = await hubspotApiRequest.call(this, 'GET', endpoint, {}); + for (const property of properties) { + if (property.name === 'hs_ticket_priority') { + for (const option of property.options) { + const priorityName = option.label; + const priorityId = option.value; + returnData.push({ + name: priorityName, + value: priorityId, + }); + } + } + } + return returnData; + }, + // Get all the ticket properties to display them to user so that he can + // select them easily + async getTicketProperties(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const endpoint = '/properties/v2/tickets/properties'; + const properties = await hubspotApiRequest.call(this, 'GET', endpoint, {}); + for (const property of properties) { + const propertyName = property.label; + const propertyId = property.name; + returnData.push({ + name: propertyName, + value: propertyId, + }); + } + return returnData; + }, + // Get all the ticket resolutions to display them to user so that he can + // select them easily + async getTicketResolutions(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const endpoint = '/properties/v2/tickets/properties'; + const properties = await hubspotApiRequest.call(this, 'GET', endpoint, {}); + for (const property of properties) { + if (property.name === 'hs_resolution') { + for (const option of property.options) { + const resolutionName = option.label; + const resolutionId = option.value; + returnData.push({ + name: resolutionName, + value: resolutionId, + }); + } + } + } + return returnData.sort((a, b) => a.name < b.name ? 0 : 1); + }, + // Get all the ticket sources to display them to user so that he can + // select them easily + async getTicketSources(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const endpoint = '/properties/v2/tickets/properties'; + const properties = await hubspotApiRequest.call(this, 'GET', endpoint, {}); + for (const property of properties) { + if (property.name === 'source_type') { + for (const option of property.options) { + const sourceName = option.label; + const sourceId = option.value; + returnData.push({ + name: sourceName, + value: sourceId, + }); + } + } + } + return returnData.sort((a, b) => a.name < b.name ? 0 : 1); + }, + // Get all the ticket stages to display them to user so that he can + // select them easily + async getTicketStages(this: ILoadOptionsFunctions): Promise { + const currentPipelineId = this.getCurrentNodeParameter('pipelineId') as string; + const returnData: INodePropertyOptions[] = []; + const endpoint = '/crm-pipelines/v1/pipelines/tickets'; + const { results } = await hubspotApiRequest.call(this, 'GET', endpoint, {}); + for (const pipeline of results) { + if (currentPipelineId === pipeline.pipelineId) { + for (const stage of pipeline.stages) { + const stageName = stage.label; + const stageId = stage.stageId; + returnData.push({ + name: stageName, + value: stageId, + }); + } + } + } + return returnData; + }, + + /* -------------------------------------------------------------------------- */ + /* COMMON */ + /* -------------------------------------------------------------------------- */ + + // Get all the owners to display them to user so that he can + // select them easily + async getOwners(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const endpoint = '/owners/v2/owners'; + const owners = await hubspotApiRequest.call(this, 'GET', endpoint); + for (const owner of owners) { + const ownerName = owner.email; + const ownerId = owner.ownerId; + returnData.push({ + name: ownerName, + value: ownerId, + }); + } + 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[] = []; + const qs: IDataObject = { + properties: ['name'], + }; + const endpoint = '/companies/v2/companies/paged'; + const companies = await hubspotApiRequestAllItems.call(this, 'companies', 'GET', endpoint, {}, qs); + for (const company of companies) { + const companyName = company.properties.name.value; + const companyId = company.companyId; + returnData.push({ + name: companyName, + value: companyId, + }); + } + return returnData.sort((a, b) => a.name < b.name ? 0 : 1); + }, + // Get all the companies to display them to user so that he can + // select them easily + async getContacts(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const endpoint = '/contacts/v1/lists/all/contacts/all'; + const contacts = await hubspotApiRequestAllItems.call(this, 'contacts', 'GET', endpoint); + 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.sort((a, b) => a.name < b.name ? 0 : 1); + }, + }, }; async execute(this: IExecuteFunctions): Promise { @@ -425,6 +601,242 @@ export class Hubspot implements INodeType { responseData = await hubspotApiRequest.call(this, 'POST', '', body, {}, uri); } } + //https://developers.hubspot.com/docs/methods/tickets/tickets-overview + if (resource === 'ticket') { + //https://developers.hubspot.com/docs/methods/tickets/create-ticket + if (operation === 'create') { + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const pipelineId = this.getNodeParameter('pipelineId', i) as string; + const stageId = this.getNodeParameter('stageId', i) as string; + const ticketName = this.getNodeParameter('ticketName', i) as string; + const body: IDataObject[] = [ + { + name: 'hs_pipeline', + value: pipelineId, + }, + { + name: 'hs_pipeline_stage', + value: stageId, + }, + { + name: 'subject', + value: ticketName, + }, + ]; + if (additionalFields.category) { + body.push({ + name: 'hs_ticket_category', + value: additionalFields.category as string + }); + } + if (additionalFields.closeDate) { + body.push({ + name: 'closed_date', + value: new Date(additionalFields.closeDate as string).getTime(), + }); + } + if (additionalFields.createDate) { + body.push({ + name: 'createdate', + value: new Date(additionalFields.createDate as string).getTime(), + }); + } + if (additionalFields.description) { + body.push({ + name: 'content', + value: additionalFields.description as string + }); + } + if (additionalFields.priority) { + body.push({ + name: 'hs_ticket_priority', + value: additionalFields.priority as string + }); + } + if (additionalFields.resolution) { + body.push({ + name: 'hs_resolution', + value: additionalFields.resolution as string + }); + } + if (additionalFields.source) { + body.push({ + name: 'source_type', + value: additionalFields.source as string + }); + } + if (additionalFields.ticketOwnerId) { + body.push({ + name: 'hubspot_owner_id', + value: additionalFields.ticketOwnerId as string + }); + } + const endpoint = '/crm-objects/v1/objects/tickets'; + responseData = await hubspotApiRequest.call(this, 'POST', endpoint, body); + + if (additionalFields.associatedCompanyIds) { + const companyAssociations: IDataObject[] = []; + for (const companyId of additionalFields.associatedCompanyIds as IDataObject[]) { + companyAssociations.push({ + fromObjectId: responseData.objectId, + toObjectId: companyId, + category: 'HUBSPOT_DEFINED', + definitionId: 26, + }); + } + await hubspotApiRequest.call(this, 'PUT', '/crm-associations/v1/associations/create-batch', companyAssociations); + } + + if (additionalFields.associatedContactIds) { + const contactAssociations: IDataObject[] = []; + for (const contactId of additionalFields.associatedContactIds as IDataObject[]) { + contactAssociations.push({ + fromObjectId: responseData.objectId, + toObjectId: contactId, + category: 'HUBSPOT_DEFINED', + definitionId: 16, + }); + } + await hubspotApiRequest.call(this, 'PUT', '/crm-associations/v1/associations/create-batch', contactAssociations); + } + } + //https://developers.hubspot.com/docs/methods/tickets/get_ticket_by_id + if (operation === 'get') { + const ticketId = this.getNodeParameter('ticketId', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + if (additionalFields.properties) { + qs.properties = additionalFields.properties as string[] + } + if (additionalFields.propertiesWithHistory) { + qs.propertiesWithHistory = (additionalFields.propertiesWithHistory as string).split(','); + } + if (additionalFields.includeDeleted) { + qs.includeDeleted = additionalFields.includeDeleted as boolean; + } + const endpoint = `/crm-objects/v1/objects/tickets/${ticketId}`; + responseData = await hubspotApiRequest.call(this, 'GET', endpoint, {}, qs); + } + //https://developers.hubspot.com/docs/methods/tickets/get-all-tickets + if (operation === 'getAll') { + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + if (additionalFields.properties) { + qs.properties = additionalFields.properties as string[]; + } + if (additionalFields.propertiesWithHistory) { + qs.propertiesWithHistory = (additionalFields.propertiesWithHistory as string).split(','); + } + const endpoint = `/crm-objects/v1/objects/tickets/paged`; + if (returnAll) { + responseData = await hubspotApiRequestAllItems.call(this, 'objects', 'GET', endpoint, {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', 0) as number; + responseData = await hubspotApiRequestAllItems.call(this, 'objects', 'GET', endpoint, {}, qs); + responseData = responseData.splice(0, qs.limit); + } + } + //https://developers.hubspot.com/docs/methods/tickets/delete-ticket + if (operation === 'delete') { + const ticketId = this.getNodeParameter('ticketId', i) as string; + const endpoint = `/crm-objects/v1/objects/tickets/${ticketId}`; + responseData = await hubspotApiRequest.call(this, 'DELETE', endpoint); + responseData = { success: true }; + } + //https://developers.hubspot.com/docs/methods/tickets/update-ticket + if (operation === 'update') { + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + const ticketId = this.getNodeParameter('ticketId', i) as string; + const body: IDataObject[] = []; + if (updateFields.pipelineId) { + body.push({ + name: 'hs_pipeline', + value: updateFields.pipelineId as string, + }); + } + if (updateFields.ticketName) { + body.push({ + name: 'subject', + value: updateFields.ticketName as string, + }); + } + if (updateFields.category) { + body.push({ + name: 'hs_ticket_category', + value: updateFields.category as string + }); + } + if (updateFields.closeDate) { + body.push({ + name: 'closed_date', + value: new Date(updateFields.createDate as string).getTime(), + }); + } + if (updateFields.createDate) { + body.push({ + name: 'createdate', + value: new Date(updateFields.createDate as string).getTime(), + }); + } + if (updateFields.description) { + body.push({ + name: 'content', + value: updateFields.description as string + }); + } + if (updateFields.priority) { + body.push({ + name: 'hs_ticket_priority', + value: updateFields.priority as string + }); + } + if (updateFields.resolution) { + body.push({ + name: 'hs_resolution', + value: updateFields.resolution as string + }); + } + if (updateFields.source) { + body.push({ + name: 'source_type', + value: updateFields.source as string + }); + } + if (updateFields.ticketOwnerId) { + body.push({ + name: 'hubspot_owner_id', + value: updateFields.ticketOwnerId as string + }); + } + const endpoint = `/crm-objects/v1/objects/tickets/${ticketId}`; + responseData = await hubspotApiRequest.call(this, 'PUT', endpoint, body); + + if (updateFields.associatedCompanyIds) { + const companyAssociations: IDataObject[] = []; + for (const companyId of updateFields.associatedCompanyIds as IDataObject[]) { + companyAssociations.push({ + fromObjectId: responseData.objectId, + toObjectId: companyId, + category: 'HUBSPOT_DEFINED', + definitionId: 26, + }); + } + await hubspotApiRequest.call(this, 'PUT', '/crm-associations/v1/associations/create-batch', companyAssociations); + } + + if (updateFields.associatedContactIds) { + const contactAssociations: IDataObject[] = []; + for (const contactId of updateFields.associatedContactIds as IDataObject[]) { + contactAssociations.push({ + fromObjectId: responseData.objectId, + toObjectId: contactId, + category: 'HUBSPOT_DEFINED', + definitionId: 16, + }); + } + await hubspotApiRequest.call(this, 'PUT', '/crm-associations/v1/associations/create-batch', contactAssociations); + } + } + } if (Array.isArray(responseData)) { returnData.push.apply(returnData, responseData as IDataObject[]); } else { diff --git a/packages/nodes-base/nodes/Hubspot/HubspotTrigger.node.ts b/packages/nodes-base/nodes/Hubspot/HubspotTrigger.node.ts index c1409765ac..25028f88e8 100644 --- a/packages/nodes-base/nodes/Hubspot/HubspotTrigger.node.ts +++ b/packages/nodes-base/nodes/Hubspot/HubspotTrigger.node.ts @@ -14,7 +14,9 @@ import { hubspotApiRequest, } from './GenericFunctions'; -import { createHash } from 'crypto'; +import { + createHash, + } from 'crypto'; export class HubspotTrigger implements INodeType { description: INodeTypeDescription = { diff --git a/packages/nodes-base/nodes/Hubspot/TicketDescription.ts b/packages/nodes-base/nodes/Hubspot/TicketDescription.ts new file mode 100644 index 0000000000..8f60251315 --- /dev/null +++ b/packages/nodes-base/nodes/Hubspot/TicketDescription.ts @@ -0,0 +1,555 @@ +import { + INodeProperties, + } from 'n8n-workflow'; + +export const ticketOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'ticket', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a ticket', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a tickets', + }, + { + name: 'Get', + value: 'get', + description: 'Get a ticket', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all tickets', + }, + { + name: 'Update', + value: 'update', + description: 'Update a ticket', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const ticketFields = [ + +/* -------------------------------------------------------------------------- */ +/* ticket:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Pipeline ID', + name: 'pipelineId', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getTicketPipelines' + }, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: 'The ID of the pipeline the ticket is in. ', + }, + { + displayName: 'Stage ID', + name: 'stageId', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getTicketStages', + loadOptionsDependsOn: [ + 'pipelineId', + ], + }, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: 'The ID of the pipeline the ticket is in. ', + }, + { + displayName: 'Ticket Name', + name: 'ticketName', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: 'The ID of the pipeline the ticket is in. ', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Company Ids', + name: 'associatedCompanyIds', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod:'getCompanies' , + }, + default: [], + description: 'Companies associated with the ticket' + }, + { + displayName: 'Contact Ids', + name: 'associatedContactIds', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod:'getContacts' , + }, + default: [], + description: 'Contacts associated with the ticket' + }, + { + displayName: 'Category', + name: 'category', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTicketCategories', + }, + default: '', + description: 'Main reason customer reached out for help', + }, + { + displayName: 'Close Date', + name: 'closeDate', + type: 'dateTime', + default: '', + description: 'The date the ticket was closed', + }, + { + displayName: 'Create Date', + name: 'createDate', + type: 'dateTime', + default: '', + description: 'the date the ticket was created', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Description of the ticket', + }, + { + displayName: 'Priority', + name: 'priority', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTicketPriorities', + }, + default: '', + description: 'The level of attention needed on the ticket', + }, + { + displayName: 'Resolution', + name: 'resolution', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTicketResolutions', + }, + default: '', + description: 'The action taken to resolve the ticket', + }, + { + displayName: 'Source', + name: 'source', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTicketSources', + }, + default: '', + description: 'Channel where ticket was originally submitted', + }, + { + displayName: 'Ticket Owner ID', + name: 'ticketOwnerId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getOwners', + }, + default: '', + description: `The user from your team that the ticket is assigned to.
+ You can assign additional users to a ticket record by creating a custom HubSpot user property`, + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* ticket:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Ticket ID', + name: 'ticketId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + description: 'Unique identifier for a particular ticket', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Company Ids', + name: 'associatedCompanyIds', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod:'getCompanies' , + }, + default: [], + description: 'Companies associated with the ticket' + }, + { + displayName: 'Contact Ids', + name: 'associatedContactIds', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod:'getContacts' , + }, + default: [], + description: 'Contact associated with the ticket' + }, + { + displayName: 'Category', + name: 'category', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTicketCategories', + }, + default: '', + description: 'Main reason customer reached out for help', + }, + { + displayName: 'Close Date', + name: 'closeDate', + type: 'dateTime', + default: '', + description: 'The date the ticket was closed', + }, + { + displayName: 'Create Date', + name: 'createDate', + type: 'dateTime', + default: '', + description: 'the date the ticket was created', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Description of the ticket', + }, + { + displayName: 'Pipeline ID', + name: 'pipelineId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTicketPipelines' + }, + default: '', + description: 'The ID of the pipeline the ticket is in. ', + }, + { + displayName: 'Priority', + name: 'priority', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTicketPriorities', + }, + default: '', + description: 'The level of attention needed on the ticket', + }, + { + displayName: 'Resolution', + name: 'resolution', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTicketResolutions', + }, + default: '', + description: 'The action taken to resolve the ticket', + }, + { + displayName: 'Source', + name: 'source', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTicketSources', + }, + default: '', + description: 'Channel where ticket was originally submitted', + }, + { + displayName: 'Ticket Name', + name: 'ticketName', + type: 'string', + default: '', + description: 'The ID of the pipeline the ticket is in. ', + }, + { + displayName: 'Ticket Owner ID', + name: 'ticketOwnerId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getOwners', + }, + default: '', + description: `The user from your team that the ticket is assigned to.
+ You can assign additional users to a ticket record by creating a custom HubSpot user property`, + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* ticket:get */ +/* -------------------------------------------------------------------------- */ +{ + displayName: 'Ticket ID', + name: 'ticketId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'get', + ], + }, + }, + default: '', + description: 'Unique identifier for a particular ticket', +}, +{ + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + displayName: 'Include Deleted', + name: 'includeDeleted', + type: 'boolean', + default: false, + }, + { + displayName: 'Properties', + name: 'properties', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTicketProperties', + }, + default: [], + description: `Used to include specific ticket properties in the results.
+ By default, the results will only include ticket ID and will not include the values for any properties for your tickets.
+ Including this parameter will include the data for the specified property in the results.
+ You can include this parameter multiple times to request multiple properties separed by ,.`, + }, + { + displayName: 'Properties With History', + name: 'propertiesWithHistory', + type: 'string', + default: '', + description: `Works similarly to properties=, but this parameter will include the history for the specified property,
+ instead of just including the current value. Use this parameter when you need the full history of changes to a property's value.`, + }, + ], +}, +/* -------------------------------------------------------------------------- */ +/* ticket:getAll */ +/* -------------------------------------------------------------------------- */ +{ + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'ticket', + ], + 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: [ + 'ticket', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 250, + }, + default: 100, + description: 'How many results to return.', +}, +{ + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Properties', + name: 'properties', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTicketProperties', + }, + default: [], + description: `Used to include specific ticket properties in the results.
+ By default, the results will only include ticket ID and will not include the values for any properties for your tickets.
+ Including this parameter will include the data for the specified property in the results.
+ You can include this parameter multiple times to request multiple properties separed by ,.`, + }, + { + displayName: 'Properties With History', + name: 'propertiesWithHistory', + type: 'string', + default: '', + description: `Works similarly to properties=, but this parameter will include the history for the specified property,
+ instead of just including the current value. Use this parameter when you need the full history of changes to a property's value.`, + }, + ], +}, +/* -------------------------------------------------------------------------- */ +/* ticket:delete */ +/* -------------------------------------------------------------------------- */ +{ + displayName: 'Ticket ID', + name: 'ticketId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'delete', + ], + }, + }, + default: '', + description: 'Unique identifier for a particular ticket', +}, +] as INodeProperties[]; From 212210b032e8cd99d05c789e3f8383f17a528201 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 9 Apr 2020 00:12:06 +0200 Subject: [PATCH 2/2] :zap: Small improvements to Hubspot-Node --- packages/nodes-base/nodes/Hubspot/Hubspot.node.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts index bde5f7931e..788488f5c3 100644 --- a/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts +++ b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts @@ -705,7 +705,7 @@ export class Hubspot implements INodeType { const ticketId = this.getNodeParameter('ticketId', i) as string; const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; if (additionalFields.properties) { - qs.properties = additionalFields.properties as string[] + qs.properties = additionalFields.properties as string[]; } if (additionalFields.propertiesWithHistory) { qs.propertiesWithHistory = (additionalFields.propertiesWithHistory as string).split(','); @@ -739,7 +739,7 @@ export class Hubspot implements INodeType { if (operation === 'delete') { const ticketId = this.getNodeParameter('ticketId', i) as string; const endpoint = `/crm-objects/v1/objects/tickets/${ticketId}`; - responseData = await hubspotApiRequest.call(this, 'DELETE', endpoint); + await hubspotApiRequest.call(this, 'DELETE', endpoint); responseData = { success: true }; } //https://developers.hubspot.com/docs/methods/tickets/update-ticket