diff --git a/packages/cli/src/Push.ts b/packages/cli/src/Push.ts index fb91f92f76..94172ac35b 100644 --- a/packages/cli/src/Push.ts +++ b/packages/cli/src/Push.ts @@ -24,7 +24,7 @@ export class Push { this.channel.on('disconnect', (channel: string, res: express.Response) => { if (res.req !== undefined) { - delete this.connections[res.req.query.sessionId]; + delete this.connections[res.req.query.sessionId as string]; } }); } diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 7ce29cfdc9..fa8ac82536 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -67,6 +67,7 @@ import { IDataObject, INodeCredentials, INodeTypeDescription, + INodeParameters, INodePropertyOptions, IRunData, Workflow, @@ -231,7 +232,7 @@ class App { return; } - this.push.add(req.query.sessionId, req, res); + this.push.add(req.query.sessionId as string, req, res); return; } next(); @@ -368,10 +369,10 @@ class App { if (req.query.url === undefined) { throw new ResponseHelper.ResponseError(`The parameter "url" is missing!`, undefined, 400); } - if (!req.query.url.match(/^http[s]?:\/\/.*\.json$/i)) { + if (!(req.query.url as string).match(/^http[s]?:\/\/.*\.json$/i)) { throw new ResponseHelper.ResponseError(`The parameter "url" is not valid! It does not seem to be a URL pointing to a n8n workflow JSON file.`, undefined, 400); } - const data = await requestPromise.get(req.query.url); + const data = await requestPromise.get(req.query.url as string); let workflowData: IWorkflowResponse | undefined; try { @@ -395,7 +396,7 @@ class App { this.app.get('/rest/workflows', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { const findQuery = {} as FindManyOptions; if (req.query.filter) { - findQuery.where = JSON.parse(req.query.filter); + findQuery.where = JSON.parse(req.query.filter as string); } // Return only the fields we need @@ -560,13 +561,13 @@ class App { // Returns parameter values which normally get loaded from an external API or // get generated dynamically this.app.get('/rest/node-parameter-options', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - const nodeType = req.query.nodeType; + const nodeType = req.query.nodeType as string; let credentials: INodeCredentials | undefined = undefined; - const currentNodeParameters = req.query.currentNodeParameters; + const currentNodeParameters = req.query.currentNodeParameters as INodeParameters[]; if (req.query.credentials !== undefined) { - credentials = JSON.parse(req.query.credentials); + credentials = JSON.parse(req.query.credentials as string); } - const methodName = req.query.methodName; + const methodName = req.query.methodName as string; const nodeTypes = NodeTypes(); @@ -790,9 +791,9 @@ class App { const findQuery = {} as FindManyOptions; // Make sure the variable has an expected value - req.query.includeData = (req.query.includeData === 'true' || req.query.includeData === true); + const includeData = ['true', true].includes(req.query.includeData as string); - if (req.query.includeData !== true) { + if (includeData !== true) { // Return only the fields we need findQuery.select = ['id', 'name', 'type', 'nodesAccess', 'createdAt', 'updatedAt']; } @@ -804,7 +805,7 @@ class App { } let encryptionKey = undefined; - if (req.query.includeData === true) { + if (includeData === true) { encryptionKey = await UserSettings.getEncryptionKey(); if (encryptionKey === undefined) { throw new Error('No encryption key got found to decrypt the credentials!'); @@ -824,7 +825,7 @@ class App { this.app.get('/rest/credentials', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { const findQuery = {} as FindManyOptions; if (req.query.filter) { - findQuery.where = JSON.parse(req.query.filter); + findQuery.where = JSON.parse(req.query.filter as string); if ((findQuery.where! as IDataObject).id !== undefined) { // No idea if multiple where parameters make db search // slower but to be sure that that is not the case we @@ -837,6 +838,16 @@ class App { const results = await Db.collections.Credentials!.find(findQuery) as unknown as ICredentialsResponse[]; + let encryptionKey = undefined; + + const includeData = ['true', true].includes(req.query.includeData as string); + if (includeData === true) { + encryptionKey = await UserSettings.getEncryptionKey(); + if (encryptionKey === undefined) { + throw new Error('No encryption key got found to decrypt the credentials!'); + } + } + let result; for (result of results) { (result as ICredentialsDecryptedResponse).id = result.id.toString(); @@ -878,7 +889,7 @@ class App { throw new Error('Required credential id is missing!'); } - const result = await Db.collections.Credentials!.findOne(req.query.id); + const result = await Db.collections.Credentials!.findOne(req.query.id as string); if (result === undefined) { res.status(404).send('The credential is not known.'); return ''; @@ -924,7 +935,7 @@ class App { newCredentialsData.updatedAt = this.getCurrentDate(); // Update the credentials in DB - await Db.collections.Credentials!.update(req.query.id, newCredentialsData); + await Db.collections.Credentials!.update(req.query.id as string, newCredentialsData); const authQueryParameters = _.get(oauthCredentials, 'authQueryParameters', '') as string; let returnUri = oAuthObj.code.getUri(); @@ -950,7 +961,7 @@ class App { let state; try { - state = JSON.parse(Buffer.from(stateEncoded, 'base64').toString()); + state = JSON.parse(Buffer.from(stateEncoded as string, 'base64').toString()); } catch (error) { const errorResponse = new ResponseHelper.ResponseError('Invalid state format returned', undefined, 503); return ResponseHelper.sendErrorResponse(res, errorResponse); @@ -1033,12 +1044,12 @@ class App { let filter: any = {}; // tslint:disable-line:no-any if (req.query.filter) { - filter = JSON.parse(req.query.filter); + filter = JSON.parse(req.query.filter as string); } let limit = 20; if (req.query.limit) { - limit = parseInt(req.query.limit, 10); + limit = parseInt(req.query.limit as string, 10); } const countFilter = JSON.parse(JSON.stringify(filter)); @@ -1216,7 +1227,7 @@ class App { let filter: any = {}; // tslint:disable-line:no-any if (req.query.filter) { - filter = JSON.parse(req.query.filter); + filter = JSON.parse(req.query.filter as string); } for (const data of executingWorkflows) { diff --git a/packages/nodes-base/nodes/HttpRequest.node.ts b/packages/nodes-base/nodes/HttpRequest.node.ts index 606d33ec84..5d7273cec2 100644 --- a/packages/nodes-base/nodes/HttpRequest.node.ts +++ b/packages/nodes-base/nodes/HttpRequest.node.ts @@ -183,7 +183,6 @@ export class HttpRequest implements INodeType { default: 'json', description: 'The format in which the data gets returned from the URL.', }, - { displayName: 'Property Name', name: 'dataPropertyName', @@ -650,9 +649,9 @@ export class HttpRequest implements INodeType { // Paramter is empty so skip it continue; } + const sendBinaryData = this.getNodeParameter('sendBinaryData', itemIndex, false) as boolean; if (optionData.name === 'body' && parametersAreJson === true) { - const sendBinaryData = this.getNodeParameter('sendBinaryData', itemIndex, false) as boolean; if (sendBinaryData === true) { const contentTypesAllowed = [ @@ -714,8 +713,12 @@ export class HttpRequest implements INodeType { } } - // @ts-ignore - requestOptions[optionData.name] = tempValue; + try { + // @ts-ignore + requestOptions[optionData.name] = JSON.parse(tempValue as string); + } catch (error) { + throw new Error(`${optionData.name} must be a valid JSON`); + } // @ts-ignore if (typeof requestOptions[optionData.name] !== 'object' && options.bodyContentType !== 'raw') { @@ -783,6 +786,7 @@ export class HttpRequest implements INodeType { } if (responseFormat === 'json') { + requestOptions.headers!['accept'] = 'application/json,text/*;q=0.99'; } else if (responseFormat === 'string') { requestOptions.headers!['accept'] = 'application/json,text/html,application/xhtml+xml,application/xml,text/*;q=0.9, */*;q=0.1'; @@ -797,9 +801,6 @@ export class HttpRequest implements INodeType { } else { requestOptions.json = true; } - - // Now that the options are all set make the actual http request - try { // Now that the options are all set make the actual http request @@ -873,7 +874,7 @@ export class HttpRequest implements INodeType { returnItems.push({ json: { [dataPropertyName]: response, - } + }, }); } } else { @@ -884,14 +885,22 @@ export class HttpRequest implements INodeType { returnItem[property] = response[property]; } - if (typeof returnItem.body === 'string') { - throw new Error('Response body is not valid JSON. Change "Response Format" to "String"'); + if (responseFormat === 'json' && typeof returnItem.body === 'string') { + try { + returnItem.body = JSON.parse(returnItem.body); + } catch (e) { + throw new Error('Response body is not valid JSON. Change "Response Format" to "String"'); + } } returnItems.push({ json: returnItem }); } else { - if (typeof response === 'string') { - throw new Error('Response body is not valid JSON. Change "Response Format" to "String"'); + if (responseFormat === 'json' && typeof response === 'string') { + try { + response = JSON.parse(response); + } catch (e) { + throw new Error('Response body is not valid JSON. Change "Response Format" to "String"'); + } } returnItems.push({ json: response }); 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..788488f5c3 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}`; + 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[]; diff --git a/packages/nodes-base/nodes/Mailjet/Mailjet.node.ts b/packages/nodes-base/nodes/Mailjet/Mailjet.node.ts index 789b1c41c8..841494b672 100644 --- a/packages/nodes-base/nodes/Mailjet/Mailjet.node.ts +++ b/packages/nodes-base/nodes/Mailjet/Mailjet.node.ts @@ -98,6 +98,7 @@ export class Mailjet implements INodeType { const fromEmail = this.getNodeParameter('fromEmail', i) as string; const htmlBody = this.getNodeParameter('html', i) as string; const textBody = this.getNodeParameter('text', i) as string; + const subject = this.getNodeParameter('subject', i) as string; const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; const toEmail = (this.getNodeParameter('toEmail', i) as string).split(',') as string[]; const variables = (this.getNodeParameter('variablesUi', i) as IDataObject).variablesValues as IDataObject[]; @@ -108,6 +109,7 @@ export class Mailjet implements INodeType { From: { email: fromEmail, }, + subject, to: [], Cc: [], Bcc: [], @@ -154,10 +156,6 @@ export class Mailjet implements INodeType { }); } } - if (additionalFields.subject) { - //@ts-ignore - body.Messages[0].subject = additionalFields.subject as string; - } if (additionalFields.trackOpens) { //@ts-ignore body.Messages[0].TrackOpens = additionalFields.trackOpens as string; diff --git a/packages/nodes-base/nodes/SplitInBatches.node.ts b/packages/nodes-base/nodes/SplitInBatches.node.ts index ec2116dbdf..4fc7987a24 100644 --- a/packages/nodes-base/nodes/SplitInBatches.node.ts +++ b/packages/nodes-base/nodes/SplitInBatches.node.ts @@ -1,5 +1,6 @@ import { IExecuteFunctions } from 'n8n-core'; import { + IDataObject, INodeExecutionData, INodeType, INodeTypeDescription, @@ -31,6 +32,23 @@ export class SplitInBatches implements INodeType { default: 10, description: 'The number of items to return with each call.', }, + + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Reset', + name: 'reset', + type: 'boolean', + default: false, + description: 'If set to true, the node will be reset and so with the current input-data newly initialized.', + }, + ], + }, ], }; @@ -45,7 +63,9 @@ export class SplitInBatches implements INodeType { const returnItems: INodeExecutionData[] = []; - if (nodeContext.items === undefined) { + const options = this.getNodeParameter('options', 0, {}) as IDataObject; + + if (nodeContext.items === undefined || options.reset === true) { // Is the first time the node runs nodeContext.currentRunIndex = 0; @@ -56,7 +76,7 @@ export class SplitInBatches implements INodeType { // Set the other items to be saved in the context to return at later runs nodeContext.items = items; - } else { + } else { // The node has been called before. So return the next batch of items. nodeContext.currentRunIndex += 1; returnItems.push.apply(returnItems, nodeContext.items.splice(0, batchSize)); diff --git a/packages/nodes-base/nodes/Zendesk/ConditionDescription.ts b/packages/nodes-base/nodes/Zendesk/ConditionDescription.ts index 99a1429b0c..1fb12cff8e 100644 --- a/packages/nodes-base/nodes/Zendesk/ConditionDescription.ts +++ b/packages/nodes-base/nodes/Zendesk/ConditionDescription.ts @@ -1,4 +1,6 @@ -import { INodeProperties } from 'n8n-workflow'; +import { + INodeProperties, + } from 'n8n-workflow'; export const conditionFields = [ { diff --git a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts index 5126c69495..f098f97358 100644 --- a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts @@ -1,11 +1,17 @@ -import { OptionsWithUri } from 'request'; +import { + OptionsWithUri, + } from 'request'; + import { IExecuteFunctions, IExecuteSingleFunctions, IHookFunctions, ILoadOptionsFunctions, } from 'n8n-core'; -import { IDataObject } from 'n8n-workflow'; + +import { + IDataObject, + } from 'n8n-workflow'; export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any const credentials = this.getCredentials('zendeskApi'); @@ -28,7 +34,15 @@ export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions try { return await this.helpers.request!(options); } catch (err) { - throw new Error(err); + let errorMessage = err.message; + if (err.response && err.response.body && err.response.body.error) { + errorMessage = err.response.body.error; + if (typeof err.response.body.error !== 'string') { + errorMessage = JSON.stringify(errorMessage); + } + } + + throw new Error(`Zendesk error response [${err.statusCode}]: ${errorMessage}`); } } diff --git a/packages/nodes-base/nodes/Zendesk/TicketDescription.ts b/packages/nodes-base/nodes/Zendesk/TicketDescription.ts index 477a8074e4..7642248e7e 100644 --- a/packages/nodes-base/nodes/Zendesk/TicketDescription.ts +++ b/packages/nodes-base/nodes/Zendesk/TicketDescription.ts @@ -1,4 +1,6 @@ -import { INodeProperties } from 'n8n-workflow'; +import { + INodeProperties, + } from 'n8n-workflow'; export const ticketOperations = [ { @@ -70,6 +72,23 @@ export const ticketFields = [ required: true, description: 'The first comment on the ticket', }, + { + displayName: 'JSON Parameters', + name: 'jsonParameters', + type: 'boolean', + default: false, + description: '', + displayOptions: { + show: { + resource: [ + 'ticket' + ], + operation: [ + 'create', + ], + }, + }, + }, { displayName: 'Additional Fields', name: 'additionalFields', @@ -184,6 +203,30 @@ export const ticketFields = [ } ], }, + { + displayName: ' Custom Fields', + name: 'customFieldsJson', + type: 'json', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'create', + ], + jsonParameters: [ + true, + ], + }, + }, + required: true, + description: `Array of customs fields Details`, + }, /* -------------------------------------------------------------------------- */ /* ticket:update */ /* -------------------------------------------------------------------------- */ @@ -205,6 +248,23 @@ export const ticketFields = [ }, description: 'Ticket ID', }, + { + displayName: 'JSON Parameters', + name: 'jsonParameters', + type: 'boolean', + default: false, + description: '', + displayOptions: { + show: { + resource: [ + 'ticket' + ], + operation: [ + 'update', + ], + }, + }, + }, { displayName: 'Update Fields', name: 'updateFields', @@ -319,6 +379,30 @@ export const ticketFields = [ } ], }, + { + displayName: ' Custom Fields', + name: 'customFieldsJson', + type: 'json', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'update', + ], + jsonParameters: [ + true, + ], + }, + }, + required: true, + description: `Array of customs fields Details`, + }, /* -------------------------------------------------------------------------- */ /* ticket:get */ /* -------------------------------------------------------------------------- */ diff --git a/packages/nodes-base/nodes/Zendesk/TicketFieldDescription.ts b/packages/nodes-base/nodes/Zendesk/TicketFieldDescription.ts new file mode 100644 index 0000000000..8e061db402 --- /dev/null +++ b/packages/nodes-base/nodes/Zendesk/TicketFieldDescription.ts @@ -0,0 +1,57 @@ +import { + INodeProperties, + } from 'n8n-workflow'; + +export const ticketFieldOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'ticketField', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get a ticket field', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all system and custom ticket fields', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const ticketFieldFields = [ + +/* -------------------------------------------------------------------------- */ +/* ticketField:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Ticket Field ID', + name: 'ticketFieldId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'ticketField', + ], + operation: [ + 'get', + ], + }, + }, + description: 'ticketField ID', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zendesk/TicketInterface.ts b/packages/nodes-base/nodes/Zendesk/TicketInterface.ts index 5ef968381b..f3338ca260 100644 --- a/packages/nodes-base/nodes/Zendesk/TicketInterface.ts +++ b/packages/nodes-base/nodes/Zendesk/TicketInterface.ts @@ -1,3 +1,11 @@ +import { + IDataObject, + } from 'n8n-workflow'; + + export interface IComment { + body?: string; +} + export interface ITicket { subject?: string; comment?: IComment; @@ -7,8 +15,5 @@ export interface ITicket { tags?: string[]; status?: string; recipient?: string; -} - -export interface IComment { - body?: string; + custom_fields?: IDataObject[]; } diff --git a/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts b/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts index 91d055dae8..b66481bcc1 100644 --- a/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts +++ b/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts @@ -1,6 +1,7 @@ import { IExecuteFunctions, } from 'n8n-core'; + import { IDataObject, INodeTypeDescription, @@ -9,14 +10,22 @@ import { ILoadOptionsFunctions, INodePropertyOptions, } from 'n8n-workflow'; + import { zendeskApiRequest, zendeskApiRequestAllItems, } from './GenericFunctions'; + import { ticketFields, ticketOperations } from './TicketDescription'; + +import { + ticketFieldFields, + ticketFieldOperations +} from './TicketFieldDescription'; + import { ITicket, IComment, @@ -54,12 +63,21 @@ export class Zendesk implements INodeType { value: 'ticket', description: 'Tickets are the means through which your end users (customers) communicate with agents in Zendesk Support.', }, + { + name: 'Ticket Field', + value: 'ticketField', + description: 'Manage system and custom ticket fields', + }, ], default: 'ticket', description: 'Resource to consume.', }, + // TICKET ...ticketOperations, ...ticketFields, + // TICKET FIELDS + ...ticketFieldOperations, + ...ticketFieldFields, ], }; @@ -112,6 +130,7 @@ export class Zendesk implements INodeType { //https://developer.zendesk.com/rest_api/docs/support/tickets if (operation === 'create') { const description = this.getNodeParameter('description', i) as string; + const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; const comment: IComment = { body: description, @@ -140,16 +159,22 @@ export class Zendesk implements INodeType { if (additionalFields.tags) { body.tags = additionalFields.tags as string[]; } - try { - responseData = await zendeskApiRequest.call(this, 'POST', '/tickets', { ticket: body }); - responseData = responseData.ticket; - } catch (err) { - throw new Error(`Zendesk Error: ${err}`); + if (jsonParameters) { + const customFieldsJson = this.getNodeParameter('customFieldsJson', i) as string; + try { + JSON.parse(customFieldsJson); + } catch(err) { + throw new Error('Custom fields must be a valid JSON'); + } + body.custom_fields = JSON.parse(customFieldsJson); } + responseData = await zendeskApiRequest.call(this, 'POST', '/tickets', { ticket: body }); + responseData = responseData.ticket; } //https://developer.zendesk.com/rest_api/docs/support/tickets#update-ticket if (operation === 'update') { const ticketId = this.getNodeParameter('id', i) as string; + const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; const body: ITicket = {}; if (updateFields.type) { @@ -173,22 +198,23 @@ export class Zendesk implements INodeType { if (updateFields.tags) { body.tags = updateFields.tags as string[]; } - try { - responseData = await zendeskApiRequest.call(this, 'PUT', `/tickets/${ticketId}`, { ticket: body }); - responseData = responseData.ticket; - } catch (err) { - throw new Error(`Zendesk Error: ${err}`); + if (jsonParameters) { + const customFieldsJson = this.getNodeParameter('customFieldsJson', i) as string; + try { + JSON.parse(customFieldsJson); + } catch(err) { + throw new Error('Custom fields must be a valid JSON'); + } + body.custom_fields = JSON.parse(customFieldsJson); } + responseData = await zendeskApiRequest.call(this, 'PUT', `/tickets/${ticketId}`, { ticket: body }); + responseData = responseData.ticket; } //https://developer.zendesk.com/rest_api/docs/support/tickets#show-ticket if (operation === 'get') { const ticketId = this.getNodeParameter('id', i) as string; - try { - responseData = await zendeskApiRequest.call(this, 'GET', `/tickets/${ticketId}`, {}); - responseData = responseData.ticket; - } catch (err) { - throw new Error(`Zendesk Error: ${err}`); - } + responseData = await zendeskApiRequest.call(this, 'GET', `/tickets/${ticketId}`, {}); + responseData = responseData.ticket; } //https://developer.zendesk.com/rest_api/docs/support/search#list-search-results if (operation === 'getAll') { @@ -204,17 +230,13 @@ export class Zendesk implements INodeType { if (options.sortOrder) { qs.sort_order = options.sortOrder; } - try { - if (returnAll) { - responseData = await zendeskApiRequestAllItems.call(this, 'results', 'GET', `/search`, {}, qs); - } else { - const limit = this.getNodeParameter('limit', i) as number; - qs.per_page = limit; - responseData = await zendeskApiRequest.call(this, 'GET', `/search`, {}, qs); - responseData = responseData.results; - } - } catch (err) { - throw new Error(`Zendesk Error: ${err}`); + if (returnAll) { + responseData = await zendeskApiRequestAllItems.call(this, 'results', 'GET', `/search`, {}, qs); + } else { + const limit = this.getNodeParameter('limit', i) as number; + qs.per_page = limit; + responseData = await zendeskApiRequest.call(this, 'GET', `/search`, {}, qs); + responseData = responseData.results; } } //https://developer.zendesk.com/rest_api/docs/support/tickets#delete-ticket @@ -227,6 +249,20 @@ export class Zendesk implements INodeType { } } } + //https://developer.zendesk.com/rest_api/docs/support/ticket_fields + if (resource === 'ticketField') { + //https://developer.zendesk.com/rest_api/docs/support/tickets#show-ticket + if (operation === 'get') { + const ticketFieldId = this.getNodeParameter('ticketFieldId', i) as string; + responseData = await zendeskApiRequest.call(this, 'GET', `/ticket_fields/${ticketFieldId}`, {}); + responseData = responseData.ticket_field; + } + //https://developer.zendesk.com/rest_api/docs/support/ticket_fields#list-ticket-fields + if (operation === 'getAll') { + responseData = await zendeskApiRequest.call(this, 'GET', '/ticket_fields', {}, qs); + responseData = responseData.ticket_fields; + } + } if (Array.isArray(responseData)) { returnData.push.apply(returnData, responseData as IDataObject[]); } else { diff --git a/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts b/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts index e242141d92..cc4e26dbd4 100644 --- a/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts +++ b/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts @@ -21,7 +21,7 @@ import { zendeskApiRequestAllItems, } from './GenericFunctions'; import { - conditionFields + conditionFields, } from './ConditionDescription'; export class ZendeskTrigger implements INodeType {