diff --git a/packages/nodes-base/nodes/Hubspot/FormDescription.ts b/packages/nodes-base/nodes/Hubspot/FormDescription.ts new file mode 100644 index 0000000000..ab77db9585 --- /dev/null +++ b/packages/nodes-base/nodes/Hubspot/FormDescription.ts @@ -0,0 +1,326 @@ +import { INodeProperties } from "n8n-workflow"; + +export const formOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'form', + ], + }, + }, + options: [ + { + name: 'Get Fields', + value: 'getFields', + description: 'Get all fields from a form', + }, + { + name: 'Submit', + value: 'submit', + description: 'Submit data to a form', + }, + ], + default: 'getFields', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const formFields = [ + +/* -------------------------------------------------------------------------- */ +/* form:submit */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Form', + name: 'formId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getForms', + }, + required: true, + displayOptions: { + show: { + resource: [ + 'form', + ], + operation: [ + 'submit', + ], + }, + }, + default: '', + description: `The ID of the form you're sending data to.`, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'form', + ], + operation: [ + 'submit', + ], + }, + }, + options: [ + { + displayName: 'Skip Validation', + name: 'skipValidation', + type: 'boolean', + default: false, + description: `Whether or not to skip validation based on the form settings.`, + }, + { + displayName: 'Submitted At', + name: 'submittedAt', + type: 'dateTime', + default: '', + description: 'Time of the form submission.', + }, + ], + }, + { + displayName: 'Context', + name: 'contextUi', + placeholder: 'Add Context', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + displayOptions: { + show: { + resource: [ + 'form', + ], + operation: [ + 'submit', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Context', + name: 'contextValue', + values: [ + { + displayName: 'HubSpot usertoken', + name: 'hutk', + type: 'string', + default: '', + description: 'Include this parameter and set it to the hubspotutk cookie value to enable cookie tracking on your submission', + }, + { + displayName: 'IP Address', + name: 'ipAddress', + type: 'string', + default: '', + description: 'The IP address of the visitor filling out the form.', + }, + { + displayName: 'Page URI', + name: 'pageUri', + type: 'string', + default: '', + description: 'The URI of the page the submission happened on.', + }, + { + displayName: 'Page Name', + name: 'pageName', + type: 'string', + default: '', + description: 'The name or title of the page the submission happened on.', + }, + { + displayName: 'Page ID', + name: 'pageId', + type: 'string', + default: '', + description: 'The ID of a page created on the HubSpot CMS.', + }, + { + displayName: 'SFDC campaign ID', + name: 'sfdcCampaignId', + type: 'string', + default: '', + description: `If the form is for an account using the HubSpot Salesforce Integration,
+ you can include the ID of a Salesforce campaign to add the contact to the specified campaign.`, + }, + { + displayName: 'Go to Webinar Webinar ID', + name: 'goToWebinarWebinarKey', + type: 'string', + default: '', + description: `If the form is for an account using the HubSpot GoToWebinar Integration,
+ you can include the ID of a webinar to enroll the contact in that webinar when they submit the form.`, + }, + ], + }, + ], + }, + { + displayName: 'Legal Consent', + name: 'lengalConsentUi', + placeholder: 'Add Legal Consent', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + displayOptions: { + show: { + resource: [ + 'form', + ], + operation: [ + 'submit', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Consent', + name: 'lengalConsentValues', + values: [ + { + displayName: 'Consent To Process', + name: 'consentToProcess', + type: 'boolean', + default: false, + description: 'Whether or not the visitor checked the Consent to process checkbox', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: 'The text displayed to the visitor for the Consent to process checkbox', + }, + { + displayName: 'Communications', + name: 'communicationsUi', + placeholder: 'Add Communication', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + displayName: 'Communication', + name: 'communicationValues', + values: [ + { + displayName: 'Subcription Type', + name: 'subscriptionTypeId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getSubscriptionTypes', + }, + default: '', + description: 'The ID of the specific subscription type', + }, + { + displayName: 'Value', + name: 'value', + type: 'boolean', + default: false, + description: ' Whether or not the visitor checked the checkbox for this subscription type.', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: 'The text displayed to the visitor for this specific subscription checkbox', + }, + ], + }, + ], + }, + ], + }, + { + displayName: 'Legitimate Interest', + name: 'legitimateInterestValues', + values: [ + { + displayName: 'Subcription Type', + name: 'subscriptionTypeId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getSubscriptionTypes', + }, + default: '', + description: 'The ID of the specific subscription type that this forms indicates interest to.', + }, + { + displayName: 'Value', + name: 'value', + type: 'boolean', + default: false, + description: `This must be true when using the 'legitimateInterest' option, as it reflects
+ the consent indicated by the visitor when submitting the form`, + }, + { + displayName: 'Legal Basis', + name: 'legalBasis', + type: 'options', + options: [ + { + name: 'Customer', + value: 'CUSTOMER', + }, + { + name: 'Lead', + value: 'LEAD', + }, + ], + default: '', + description: 'The privacy text displayed to the visitor.', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: 'The privacy text displayed to the visitor.', + }, + ], + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* form:getFields */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Form', + name: 'formId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getForms', + }, + required: true, + displayOptions: { + show: { + resource: [ + 'form', + ], + operation: [ + 'getFields', + ], + }, + }, + default: '', + description: 'The ID of the form', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Hubspot/FormInterface.ts b/packages/nodes-base/nodes/Hubspot/FormInterface.ts new file mode 100644 index 0000000000..8b3b424f7a --- /dev/null +++ b/packages/nodes-base/nodes/Hubspot/FormInterface.ts @@ -0,0 +1,21 @@ +import { IDataObject } from "n8n-workflow"; + +export interface IContext { + goToWebinarWebinarKey?: string; + hutk?: string; + ipAddress?: string; + pageId?: string; + pageName?: string; + pageUri?: string; + sfdcCampaignId?: string; +} + +export interface IForm { + portalId?: number; + formId?: string; + fields?: IDataObject[]; + legalConsentOptions?: IDataObject; + context?: IContext[]; + submittedAt?: number; + skipValidation?: boolean; +} diff --git a/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts b/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts index 427446ac6d..454b604438 100644 --- a/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts @@ -26,17 +26,20 @@ export async function hubspotApiRequest(this: IHookFunctions | IExecuteFunctions json: true, useQuerystring: true, }; - try { return await this.helpers.request!(options); } catch (error) { - const errorMessage = error.response.body.message || error.response.body.Message || error.message; - throw new Error(`Hubspot error response [${error.statusCode}]: ${errorMessage}`); + + if (error.response && error.response.body && error.response.body.errors) { + // Try to return the error prettier + const errorMessages = error.response.body.errors.map((e: IDataObject) => e.message); + throw new Error(`Hubspot error response [${error.statusCode}]: ${errorMessages.join(' | ')}`); + } + + throw error; } } - - /** * Make an API request to paginated hubspot endpoint * and return all results diff --git a/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts index 645751e499..1dbc563ca0 100644 --- a/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts +++ b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts @@ -1,6 +1,7 @@ import { IExecuteFunctions, } from 'n8n-core'; + import { IDataObject, INodeTypeDescription, @@ -9,15 +10,30 @@ import { ILoadOptionsFunctions, INodePropertyOptions, } from 'n8n-workflow'; + import { hubspotApiRequest, hubspotApiRequestAllItems, } from './GenericFunctions'; + import { dealOperations, dealFields, -} from '../Hubspot/DealDescription'; -import { IDeal, IAssociation } from './DealInterface'; +} from './DealDescription'; + +import { + IDeal, + IAssociation +} from './DealInterface'; + +import { + formOperations, + formFields, + } from './FormDescription'; + +import { + IForm +} from './FormInterface'; export class Hubspot implements INodeType { description: INodeTypeDescription = { @@ -50,6 +66,10 @@ export class Hubspot implements INodeType { name: 'Deal', value: 'deal', }, + { + name: 'Form', + value: 'form', + }, ], default: 'deal', description: 'Resource to consume.', @@ -58,23 +78,22 @@ export class Hubspot implements INodeType { // Deal ...dealOperations, ...dealFields, + // Form + ...formOperations, + ...formFields, ], }; methods = { loadOptions: { + // Get all the groups to display them to user so that he can // select them easily async getDealStages(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - let stages; - try { - const endpoint = '/crm-pipelines/v1/pipelines/deals'; - stages = await hubspotApiRequest.call(this, 'GET', endpoint, {}); - stages = stages.results[0].stages; - } catch (err) { - throw new Error(`Hubspot Error: ${err}`); - } + const endpoint = '/crm-pipelines/v1/pipelines/deals'; + let stages = await hubspotApiRequest.call(this, 'GET', endpoint, {}); + stages = stages.results[0].stages; for (const stage of stages) { const stageName = stage.label; const stageId = stage.stageId; @@ -90,13 +109,8 @@ export class Hubspot implements INodeType { // select them easily async getCompanies(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - let companies; - try { - const endpoint = '/companies/v2/companies/paged'; - companies = await hubspotApiRequestAllItems.call(this, 'results', 'GET', endpoint); - } catch (err) { - throw new Error(`Hubspot Error: ${err}`); - } + 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; @@ -112,13 +126,8 @@ export class Hubspot implements INodeType { // select them easily async getContacts(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - let contacts; - try { - const endpoint = '/contacts/v1/lists/all/contacts/all'; - contacts = await hubspotApiRequestAllItems.call(this, 'contacts', 'GET', endpoint); - } catch (err) { - throw new Error(`Hubspot Error: ${err}`); - } + 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; @@ -134,13 +143,8 @@ export class Hubspot implements INodeType { // select them easily async getDealTypes(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - let dealTypes; - try { - const endpoint = '/properties/v1/deals/properties/named/dealtype'; - dealTypes = await hubspotApiRequest.call(this, 'GET', endpoint); - } catch (err) { - throw new Error(`Hubspot Error: ${err}`); - } + const endpoint = '/properties/v1/deals/properties/named/dealtype'; + const dealTypes = await hubspotApiRequest.call(this, 'GET', endpoint); for (const dealType of dealTypes.options) { const dealTypeName = dealType.label ; const dealTypeId = dealType.value; @@ -151,6 +155,40 @@ export class Hubspot implements INodeType { } return returnData; }, + + // Get all the forms to display them to user so that he can + // select them easily + async getForms(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const endpoint = '/forms/v2/forms'; + const forms = await hubspotApiRequest.call(this, 'GET', endpoint, {}, { formTypes: 'ALL' }); + for (const form of forms) { + const formName = form.name; + const formId = form.guid; + returnData.push({ + name: formName, + value: formId, + }); + } + return returnData; + }, + + // Get all the subscription types to display them to user so that he can + // select them easily + async getSubscriptionTypes(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const endpoint = '/email/public/v1/subscriptions'; + const subscriptions = await hubspotApiRequestAllItems.call(this, 'subscriptionDefinitions', 'GET', endpoint, {}); + for (const subscription of subscriptions) { + const subscriptionName = subscription.name; + const subscriptionId = subscription.id; + returnData.push({ + name: subscriptionName, + value: subscriptionId, + }); + } + return returnData; + }, } }; @@ -357,6 +395,60 @@ export class Hubspot implements INodeType { } } } + //https://developers.hubspot.com/docs/methods/forms/forms_overview + if (resource === 'form') { + //https://developers.hubspot.com/docs/methods/forms/v2/get_fields + if (operation === 'getFields') { + const formId = this.getNodeParameter('formId', i) as string; + responseData = await hubspotApiRequest.call(this, 'GET', `/forms/v2/fields/${formId}`); + } + //https://developers.hubspot.com/docs/methods/forms/submit_form_v3 + if (operation === 'submit') { + const formId = this.getNodeParameter('formId', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const context = (this.getNodeParameter('contextUi', i) as IDataObject).contextValue as IDataObject; + const legalConsent = (this.getNodeParameter('lengalConsentUi', i) as IDataObject).lengalConsentValues as IDataObject; + const legitimateInteres = (this.getNodeParameter('lengalConsentUi', i) as IDataObject).legitimateInterestValues as IDataObject; + const { portalId } = await hubspotApiRequest.call(this, 'GET', `/forms/v2/forms/${formId}`); + const body: IForm = { + formId, + portalId, + legalConsentOptions: {}, + fields: [], + }; + if (additionalFields.submittedAt) { + body.submittedAt = new Date(additionalFields.submittedAt as string).getTime(); + } + if (additionalFields.skipValidation) { + body.skipValidation = additionalFields.skipValidation as boolean; + } + const consent: IDataObject = {}; + if (legalConsent) { + if (legalConsent.consentToProcess) { + consent!.consentToProcess = legalConsent.consentToProcess as boolean; + } + if (legalConsent.text) { + consent!.text = legalConsent.text as string; + } + if (legalConsent.communicationsUi) { + consent.communications = (legalConsent.communicationsUi as IDataObject).communicationValues as IDataObject; + } + } + body.legalConsentOptions!.consent = consent; + const fields: IDataObject = items[i].json; + for (const key of Object.keys(fields)) { + body.fields?.push({ name: key, value: fields[key] }); + } + if (body.legalConsentOptions!.legitimateInterest) { + Object.assign(body, { legalConsentOptions: { legitimateInterest: legitimateInteres } }); + } + if (context) { + Object.assign(body, { context }); + } + const uri = `https://api.hsforms.com/submissions/v3/integration/submit/${portalId}/${formId}`; + responseData = await hubspotApiRequest.call(this, 'POST', '', body, {}, uri); + } + } if (Array.isArray(responseData)) { returnData.push.apply(returnData, responseData as IDataObject[]); } else {