🔀 Merge branch 'feature/extend-hubspot' of https://github.com/RicardoE105/n8n into RicardoE105-feature/extend-hubspot

This commit is contained in:
Jan Oberhauser 2020-03-27 07:29:30 +01:00
commit 7be0a0a7b3
4 changed files with 478 additions and 36 deletions

View file

@ -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,</br>
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,</br>
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</br>
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[];

View file

@ -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;
}

View file

@ -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

View file

@ -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<INodePropertyOptions[]> {
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<INodePropertyOptions[]> {
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<INodePropertyOptions[]> {
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<INodePropertyOptions[]> {
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<INodePropertyOptions[]> {
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<INodePropertyOptions[]> {
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 {