2022-08-17 08:50:24 -07:00
import { IHookFunctions , IWebhookFunctions } from 'n8n-core' ;
2020-03-08 19:39:20 -07:00
import {
2020-10-01 05:01:39 -07:00
IDataObject ,
2021-01-31 23:31:40 -08:00
ILoadOptionsFunctions ,
INodePropertyOptions ,
2020-03-08 19:39:20 -07:00
INodeType ,
2020-10-01 05:01:39 -07:00
INodeTypeDescription ,
2020-03-08 19:39:20 -07:00
IWebhookResponseData ,
2021-04-16 09:33:36 -07:00
NodeOperationError ,
2020-03-08 19:39:20 -07:00
} from 'n8n-workflow' ;
2022-11-08 06:28:21 -08:00
import { hubspotApiRequest , propertyEvents } from './GenericFunctions' ;
2020-03-08 19:39:20 -07:00
2022-08-17 08:50:24 -07:00
import { createHash } from 'crypto' ;
2021-01-31 23:31:40 -08:00
2020-03-08 19:39:20 -07:00
export class HubspotTrigger implements INodeType {
description : INodeTypeDescription = {
2020-07-24 03:56:41 -07:00
displayName : 'HubSpot Trigger' ,
2020-03-08 19:39:20 -07:00
name : 'hubspotTrigger' ,
2021-01-31 23:31:40 -08:00
icon : 'file:hubspot.svg' ,
2020-03-08 19:39:20 -07:00
group : [ 'trigger' ] ,
version : 1 ,
2021-07-03 05:40:16 -07:00
description : 'Starts the workflow when HubSpot events occur' ,
2020-03-08 19:39:20 -07:00
defaults : {
feat(editor): Node creator actions (#4696)
* WIP: Node Actions List UI
* WIP: Recommended Actions and preseting of fields
* WIP: Resource category
* :art: Moved actions categorisation to the server
* :label: Add missing INodeAction type
* :sparkles: Improve SSR categorisation, fix adding of mixed actions
* :recycle: Refactor CategorizedItems to composition api, style fixes
* WIP: Adding multiple nodes
* :recycle: Refactor rest of the NodeCreator component to composition API, conver globalLinkActions to composable
* :sparkles: Allow actions dragging, fix search and refactor passing of actions to categorized items
* :lipstick: Fix node actions title
* Migrate to the pinia store, add posthog feature and various fixes
* :bug: Fix filtering of trigger actions when not merged
* fix: N8N-5439 — Do not use simple node item when at NodeHelperPanel root
* :bug: Design review fixes
* :bug: Fix disabling of merged actions
* Fix trigger root filtering
* :sparkles: Allow for custom node actions parser, introduce hubspot parser
* :bug: Fix initial node params validation, fix position of second added node
* :bug: Introduce operations category, removed canvas node names overrride, fix API actions display and prevent dragging of action nodes
* :sparkles: Prevent NDV auto-open feature flag
* :bug: Inject recommened action for trigger nodes without actions
* Refactored NodeCreatorNode to Storybook, change filtering of merged nodes for the trigger helper panel, minor fixes
* Improve rendering of app nodes and animation
* Cleanup, any only enable accordion transition on triggerhelperpanel
* Hide node creator scrollbars in Firefox
* Minor styles fixes
* Do not copy the array in rendering method
* Removed unused props
* Fix memory leak
* Fix categorisation of regular nodes with a single resource
* Implement telemetry calls for node actions
* Move categorization to FE
* Fix client side actions categorisation
* Skip custom action show
* Only load tooltip for NodeIcon if necessary
* Fix lodash startCase import
* Remove lodash.startcase
* Cleanup
* Fix node creator autofocus on "tab"
* Prevent posthog getFeatureFlag from crashing
* Debugging preview env search issues
* Remove logs
* Make sure the pre-filled params are update not overwritten
* Get rid of transition in itemiterator
* WIP: Rough version of NodeActions keyboard navigation, replace nodeCreator composable with Pinia store module
* Rewrite to add support for ActionItem to ItemIterator and make CategorizedItems accept items props
* Fix category item counter & cleanup
* Add APIHint to actions search no-result, clean up NodeCreatorNode
* Improve node actions no results message
* Remove logging, fix filtering of recommended placeholder category
* Remove unused NodeActions component and node merging feature falg
* Do not show regular nodes without actions
* Make sure to add manual trigger when adding http node via actions hint
* Fixed api hint footer line height
* Prevent pointer-events od NodeIcon img and remove "this" from template
* Address PR points
* Fix e2e specs
* Make sure canvas ia loaded
* Make sure canvas ia loaded before opening nodeCreator in e2e spec
* Fix flaky workflows tags e2e getter
* Imrpove node creator click outside UX, add manual node to regular nodes added from trigger panel
* Add manual trigger node if dragging regular from trigger panel
2022-12-09 01:56:36 -08:00
name : 'HubSpot Trigger' ,
2020-03-08 19:39:20 -07:00
} ,
inputs : [ ] ,
outputs : [ 'main' ] ,
credentials : [
{
name : 'hubspotDeveloperApi' ,
required : true ,
} ,
] ,
webhooks : [
{
name : 'default' ,
httpMethod : 'POST' ,
responseMode : 'onReceived' ,
path : 'webhook' ,
} ,
{
name : 'setup' ,
httpMethod : 'GET' ,
responseMode : 'onReceived' ,
path : 'webhook' ,
} ,
] ,
properties : [
{
2021-01-31 23:31:40 -08:00
displayName : 'Events' ,
name : 'eventsUi' ,
type : 'fixedCollection' ,
typeOptions : {
multipleValues : true ,
} ,
placeholder : 'Add Event' ,
default : { } ,
2020-03-08 19:39:20 -07:00
options : [
{
2021-01-31 23:31:40 -08:00
displayName : 'Event' ,
name : 'eventValues' ,
values : [
{
displayName : 'Name' ,
name : 'name' ,
type : 'options' ,
options : [
2022-06-03 10:23:49 -07:00
{
name : 'Company Created' ,
value : 'company.creation' ,
2022-08-17 08:50:24 -07:00
description :
"To get notified if any company is created in a customer's account" ,
2022-06-03 10:23:49 -07:00
} ,
{
name : 'Company Deleted' ,
value : 'company.deletion' ,
2022-08-17 08:50:24 -07:00
description :
"To get notified if any company is deleted in a customer's account" ,
2022-06-03 10:23:49 -07:00
} ,
{
name : 'Company Property Changed' ,
value : 'company.propertyChange' ,
2022-08-17 08:50:24 -07:00
description :
"To get notified if a specified property is changed for any company in a customer's account" ,
2022-06-03 10:23:49 -07:00
} ,
2021-01-31 23:31:40 -08:00
{
name : 'Contact Created' ,
value : 'contact.creation' ,
2022-08-17 08:50:24 -07:00
description :
"To get notified if any contact is created in a customer's account" ,
2021-01-31 23:31:40 -08:00
} ,
{
name : 'Contact Deleted' ,
value : 'contact.deletion' ,
2022-08-17 08:50:24 -07:00
description :
"To get notified if any contact is deleted in a customer's account" ,
2021-01-31 23:31:40 -08:00
} ,
{
name : 'Contact Privacy Deleted' ,
value : 'contact.privacyDeletion' ,
2022-08-17 08:50:24 -07:00
description :
'To get notified if a contact is deleted for privacy compliance reasons' ,
2021-01-31 23:31:40 -08:00
} ,
{
name : 'Contact Property Changed' ,
value : 'contact.propertyChange' ,
2022-08-17 08:50:24 -07:00
description :
"To get notified if a specified property is changed for any contact in a customer's account" ,
2021-01-31 23:31:40 -08:00
} ,
{
name : 'Deal Created' ,
value : 'deal.creation' ,
2022-08-17 08:50:24 -07:00
description : "To get notified if any deal is created in a customer's account" ,
2021-01-31 23:31:40 -08:00
} ,
{
name : 'Deal Deleted' ,
value : 'deal.deletion' ,
2022-08-17 08:50:24 -07:00
description : "To get notified if any deal is deleted in a customer's account" ,
2021-01-31 23:31:40 -08:00
} ,
{
name : 'Deal Property Changed' ,
value : 'deal.propertyChange' ,
2022-08-17 08:50:24 -07:00
description :
"To get notified if a specified property is changed for any deal in a customer's account" ,
2021-01-31 23:31:40 -08:00
} ,
] ,
default : 'contact.creation' ,
required : true ,
} ,
{
2022-06-03 10:23:49 -07:00
displayName : 'Property Name or ID' ,
2021-01-31 23:31:40 -08:00
name : 'property' ,
type : 'options' ,
2022-08-17 08:50:24 -07:00
description :
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>' ,
2021-01-31 23:31:40 -08:00
typeOptions : {
2022-08-17 08:50:24 -07:00
loadOptionsDependsOn : [ 'contact.propertyChange' ] ,
2021-01-31 23:31:40 -08:00
loadOptionsMethod : 'getContactProperties' ,
} ,
displayOptions : {
show : {
2022-08-17 08:50:24 -07:00
name : [ 'contact.propertyChange' ] ,
2021-01-31 23:31:40 -08:00
} ,
} ,
default : '' ,
required : true ,
} ,
{
2022-06-03 10:23:49 -07:00
displayName : 'Property Name or ID' ,
2021-01-31 23:31:40 -08:00
name : 'property' ,
type : 'options' ,
2022-08-17 08:50:24 -07:00
description :
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>' ,
2021-01-31 23:31:40 -08:00
typeOptions : {
2022-08-17 08:50:24 -07:00
loadOptionsDependsOn : [ 'company.propertyChange' ] ,
2021-01-31 23:31:40 -08:00
loadOptionsMethod : 'getCompanyProperties' ,
} ,
displayOptions : {
show : {
2022-08-17 08:50:24 -07:00
name : [ 'company.propertyChange' ] ,
2021-01-31 23:31:40 -08:00
} ,
} ,
default : '' ,
required : true ,
} ,
{
2022-06-03 10:23:49 -07:00
displayName : 'Property Name or ID' ,
2021-01-31 23:31:40 -08:00
name : 'property' ,
type : 'options' ,
2022-08-17 08:50:24 -07:00
description :
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>' ,
2021-01-31 23:31:40 -08:00
typeOptions : {
2022-08-17 08:50:24 -07:00
loadOptionsDependsOn : [ 'deal.propertyChange' ] ,
2021-01-31 23:31:40 -08:00
loadOptionsMethod : 'getDealProperties' ,
} ,
displayOptions : {
show : {
2022-08-17 08:50:24 -07:00
name : [ 'deal.propertyChange' ] ,
2021-01-31 23:31:40 -08:00
} ,
} ,
default : '' ,
required : true ,
} ,
2020-03-08 19:39:20 -07:00
] ,
} ,
2021-01-31 23:31:40 -08:00
] ,
2020-03-08 19:39:20 -07:00
} ,
{
displayName : 'Additional Fields' ,
name : 'additionalFields' ,
type : 'collection' ,
placeholder : 'Add Field' ,
default : { } ,
options : [
{
displayName : 'Max Concurrent Requests' ,
name : 'maxConcurrentRequests' ,
type : 'number' ,
typeOptions : {
minValue : 5 ,
} ,
default : 5 ,
} ,
] ,
} ,
] ,
2021-01-31 23:31:40 -08:00
} ;
2020-03-08 19:39:20 -07:00
2021-01-31 23:31:40 -08:00
methods = {
loadOptions : {
// Get all the available contacts to display them to user so that he can
// select them easily
async getContactProperties ( this : ILoadOptionsFunctions ) : Promise < INodePropertyOptions [ ] > {
const returnData : INodePropertyOptions [ ] = [ ] ;
2021-12-10 11:28:59 -08:00
const endpoint = '/properties/v2/contacts/properties' ;
const properties = await hubspotApiRequest . call ( this , 'GET' , endpoint , { } ) ;
for ( const property of properties ) {
const propertyName = property . label ;
const propertyId = property . name ;
2021-01-31 23:31:40 -08:00
returnData . push ( {
2021-12-10 11:28:59 -08:00
name : propertyName ,
value : propertyId ,
2021-01-31 23:31:40 -08:00
} ) ;
}
return returnData ;
} ,
// Get all the available companies to display them to user so that he can
// select them easily
async getCompanyProperties ( this : ILoadOptionsFunctions ) : Promise < INodePropertyOptions [ ] > {
const returnData : INodePropertyOptions [ ] = [ ] ;
2021-12-10 11:28:59 -08:00
const endpoint = '/properties/v2/companies/properties' ;
const properties = await hubspotApiRequest . call ( this , 'GET' , endpoint , { } ) ;
for ( const property of properties ) {
const propertyName = property . label ;
const propertyId = property . name ;
2021-01-31 23:31:40 -08:00
returnData . push ( {
2021-12-10 11:28:59 -08:00
name : propertyName ,
value : propertyId ,
2021-01-31 23:31:40 -08:00
} ) ;
}
return returnData ;
} ,
// Get all the available deals to display them to user so that he can
// select them easily
async getDealProperties ( this : ILoadOptionsFunctions ) : Promise < INodePropertyOptions [ ] > {
const returnData : INodePropertyOptions [ ] = [ ] ;
2021-12-10 11:28:59 -08:00
const endpoint = '/properties/v2/deals/properties' ;
const properties = await hubspotApiRequest . call ( this , 'GET' , endpoint , { } ) ;
for ( const property of properties ) {
const propertyName = property . label ;
const propertyId = property . name ;
2021-01-31 23:31:40 -08:00
returnData . push ( {
2021-12-10 11:28:59 -08:00
name : propertyName ,
value : propertyId ,
2021-01-31 23:31:40 -08:00
} ) ;
}
return returnData ;
} ,
} ,
2020-03-08 19:39:20 -07:00
} ;
// @ts-ignore (because of request)
webhookMethods = {
default : {
async checkExists ( this : IHookFunctions ) : Promise < boolean > {
// Check all the webhooks which exist already if it is identical to the
// one that is supposed to get created.
2021-01-31 23:31:40 -08:00
const currentWebhookUrl = this . getNodeWebhookUrl ( 'default' ) as string ;
2022-04-14 23:00:47 -07:00
const { appId } = await this . getCredentials ( 'hubspotDeveloperApi' ) ;
2021-01-31 23:31:40 -08:00
try {
2022-08-17 08:50:24 -07:00
const { targetUrl } = await hubspotApiRequest . call (
this ,
'GET' ,
` /webhooks/v3/ ${ appId } /settings ` ,
{ } ,
) ;
2021-01-31 23:31:40 -08:00
if ( targetUrl !== currentWebhookUrl ) {
2022-08-17 08:50:24 -07:00
throw new NodeOperationError (
this . getNode ( ) ,
` The APP ID ${ appId } already has a target url ${ targetUrl } . Delete it or use another APP ID before executing the trigger. Due to Hubspot API limitations, you can have just one trigger per APP. ` ,
) ;
2021-01-31 23:31:40 -08:00
}
} catch ( error ) {
if ( error . statusCode === 404 ) {
return false ;
2020-03-08 19:39:20 -07:00
}
}
2021-01-31 23:31:40 -08:00
// if the app is using the current webhook url. Delete everything and create it again with the current events
2022-08-17 08:50:24 -07:00
const { results : subscriptions } = await hubspotApiRequest . call (
this ,
'GET' ,
` /webhooks/v3/ ${ appId } /subscriptions ` ,
{ } ,
) ;
2021-01-31 23:31:40 -08:00
// delete all subscriptions
for ( const subscription of subscriptions ) {
2022-08-17 08:50:24 -07:00
await hubspotApiRequest . call (
this ,
'DELETE' ,
` /webhooks/v3/ ${ appId } /subscriptions/ ${ subscription . id } ` ,
{ } ,
) ;
2021-01-31 23:31:40 -08:00
}
await hubspotApiRequest . call ( this , 'DELETE' , ` /webhooks/v3/ ${ appId } /settings ` , { } ) ;
2020-03-08 19:39:20 -07:00
return false ;
} ,
async create ( this : IHookFunctions ) : Promise < boolean > {
const webhookUrl = this . getNodeWebhookUrl ( 'default' ) ;
2022-04-14 23:00:47 -07:00
const { appId } = await this . getCredentials ( 'hubspotDeveloperApi' ) ;
2022-08-17 08:50:24 -07:00
const events =
2022-12-02 12:54:28 -08:00
( ( this . getNodeParameter ( 'eventsUi' ) as IDataObject ) ? . eventValues as IDataObject [ ] ) || [ ] ;
2020-03-08 19:39:20 -07:00
const additionalFields = this . getNodeParameter ( 'additionalFields' ) as IDataObject ;
2021-01-31 23:31:40 -08:00
let endpoint = ` /webhooks/v3/ ${ appId } /settings ` ;
2020-03-13 04:09:09 -07:00
let body : IDataObject = {
2021-01-31 23:31:40 -08:00
targetUrl : webhookUrl ,
2020-03-08 19:39:20 -07:00
maxConcurrentRequests : additionalFields.maxConcurrentRequests || 5 ,
} ;
2021-01-31 23:31:40 -08:00
2020-03-08 19:39:20 -07:00
await hubspotApiRequest . call ( this , 'PUT' , endpoint , body ) ;
2021-01-31 23:31:40 -08:00
endpoint = ` /webhooks/v3/ ${ appId } /subscriptions ` ;
2020-03-08 19:39:20 -07:00
2021-01-31 23:31:40 -08:00
if ( Array . isArray ( events ) && events . length === 0 ) {
2021-04-16 09:33:36 -07:00
throw new NodeOperationError ( this . getNode ( ) , ` You must define at least one event ` ) ;
2021-01-31 23:31:40 -08:00
}
2020-03-08 19:39:20 -07:00
2021-01-31 23:31:40 -08:00
for ( const event of events ) {
body = {
eventType : event.name ,
active : true ,
} ;
if ( propertyEvents . includes ( event . name as string ) ) {
const property = event . property ;
body . propertyName = property ;
}
await hubspotApiRequest . call ( this , 'POST' , endpoint , body ) ;
2020-03-08 19:39:20 -07:00
}
return true ;
} ,
async delete ( this : IHookFunctions ) : Promise < boolean > {
2022-04-14 23:00:47 -07:00
const { appId } = await this . getCredentials ( 'hubspotDeveloperApi' ) ;
2020-03-08 19:39:20 -07:00
2022-08-17 08:50:24 -07:00
const { results : subscriptions } = await hubspotApiRequest . call (
this ,
'GET' ,
` /webhooks/v3/ ${ appId } /subscriptions ` ,
{ } ,
) ;
2020-03-08 19:39:20 -07:00
2021-01-31 23:31:40 -08:00
for ( const subscription of subscriptions ) {
2022-08-17 08:50:24 -07:00
await hubspotApiRequest . call (
this ,
'DELETE' ,
` /webhooks/v3/ ${ appId } /subscriptions/ ${ subscription . id } ` ,
{ } ,
) ;
2021-01-31 23:31:40 -08:00
}
try {
await hubspotApiRequest . call ( this , 'DELETE' , ` /webhooks/v3/ ${ appId } /settings ` , { } ) ;
2021-04-16 09:33:36 -07:00
} catch ( error ) {
2021-01-31 23:31:40 -08:00
return false ;
2020-03-08 19:39:20 -07:00
}
return true ;
} ,
} ,
} ;
async webhook ( this : IWebhookFunctions ) : Promise < IWebhookResponseData > {
2022-04-14 23:00:47 -07:00
const credentials = await this . getCredentials ( 'hubspotDeveloperApi' ) ;
2020-06-10 03:57:13 -07:00
if ( credentials === undefined ) {
2021-04-16 09:33:36 -07:00
throw new NodeOperationError ( this . getNode ( ) , 'No credentials found!' ) ;
2020-06-10 03:57:13 -07:00
}
2020-03-13 04:09:09 -07:00
const req = this . getRequestObject ( ) ;
2020-03-08 19:39:20 -07:00
const bodyData = req . body ;
const headerData = this . getHeaderData ( ) ;
//@ts-ignore
2020-03-13 04:09:09 -07:00
if ( headerData [ 'x-hubspot-signature' ] === undefined ) {
2020-03-08 19:39:20 -07:00
return { } ;
}
2020-06-13 16:48:24 -07:00
2022-12-02 12:54:28 -08:00
const hash = ` ${ credentials . clientSecret } ${ JSON . stringify ( bodyData ) } ` ;
2021-09-18 13:18:35 -07:00
const signature = createHash ( 'sha256' ) . update ( hash ) . digest ( 'hex' ) ;
//@ts-ignore
if ( signature !== headerData [ 'x-hubspot-signature' ] ) {
return { } ;
2020-03-08 19:39:20 -07:00
}
2020-06-13 16:48:24 -07:00
2020-03-08 19:39:20 -07:00
for ( let i = 0 ; i < bodyData . length ; i ++ ) {
const subscriptionType = bodyData [ i ] . subscriptionType as string ;
if ( subscriptionType . includes ( 'contact' ) ) {
bodyData [ i ] . contactId = bodyData [ i ] . objectId ;
}
if ( subscriptionType . includes ( 'company' ) ) {
bodyData [ i ] . companyId = bodyData [ i ] . objectId ;
}
if ( subscriptionType . includes ( 'deal' ) ) {
bodyData [ i ] . dealId = bodyData [ i ] . objectId ;
}
delete bodyData [ i ] . objectId ;
}
return {
2022-08-17 08:50:24 -07:00
workflowData : [ this . helpers . returnJsonArray ( bodyData ) ] ,
2020-03-08 19:39:20 -07:00
} ;
}
}