diff --git a/packages/nodes-base/credentials/KeapOAuth2Api.credentials.ts b/packages/nodes-base/credentials/KeapOAuth2Api.credentials.ts new file mode 100644 index 000000000..8bbe09d6f --- /dev/null +++ b/packages/nodes-base/credentials/KeapOAuth2Api.credentials.ts @@ -0,0 +1,48 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +const scopes = [ + 'full', +]; + +export class KeapOAuth2Api implements ICredentialType { + name = 'keapOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Keap OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://signin.infusionsoft.com/app/oauth/authorize', + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://api.infusionsoft.com/token', + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: scopes.join(' '), + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'body', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Keap/CompanyDescription.ts b/packages/nodes-base/nodes/Keap/CompanyDescription.ts new file mode 100644 index 000000000..edb73d1ce --- /dev/null +++ b/packages/nodes-base/nodes/Keap/CompanyDescription.ts @@ -0,0 +1,374 @@ +import { + INodeProperties, + } from 'n8n-workflow'; + +export const companyOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'company', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a company', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all companies', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const companyFields = [ + +/* -------------------------------------------------------------------------- */ +/* company:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Company Name', + name: 'companyName', + required: true, + type: 'string', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'company', + ], + }, + }, + default: '', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'company', + ], + }, + }, + options: [ + { + displayName: 'Email', + name: 'emailAddress', + type: 'string', + default: '', + }, + { + displayName: 'Notes', + name: 'notes', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + }, + { + displayName: 'Opt In Reason', + name: 'optInReason', + type: 'string', + default: '', + }, + { + displayName: 'Website', + name: 'website', + type: 'string', + default: '', + }, + ], + }, + { + displayName: 'Addresses', + name: 'addressesUi', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + default: '', + placeholder: 'Add Address', + displayOptions: { + show: { + resource: [ + 'company', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + name: 'addressesValues', + displayName: 'Address', + values: [ + { + displayName: 'Country Code', + name: 'countryCode', + type: 'string', + default: '', + description: 'ISO Alpha-3 Code' + }, + { + displayName: 'Line 1', + name: 'line1', + type: 'string', + default: '', + }, + { + displayName: 'Line 2', + name: 'line2', + type: 'string', + default: '', + }, + { + displayName: 'Locality', + name: 'locality', + type: 'string', + default: '', + }, + { + displayName: 'Postal Code', + name: 'postalCode', + type: 'string', + default: '', + }, + { + displayName: 'Region', + name: 'region', + type: 'string', + default: '', + }, + { + displayName: 'Zip Code', + name: 'zipCode', + type: 'string', + default: '', + }, + { + displayName: 'Zip Four', + name: 'zipFour', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Faxes', + name: 'faxesUi', + type: 'fixedCollection', + default: {}, + typeOptions: { + multipleValues: false, + }, + placeholder: 'Add Fax', + displayOptions: { + show: { + resource: [ + 'company', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + name: 'faxesValues', + displayName: 'Fax', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'string', + default: '', + }, + { + displayName: 'Number', + name: 'number', + type: 'string', + default: '', + }, + ], + } + ], + }, + { + displayName: 'Phones', + name: 'phonesUi', + type: 'fixedCollection', + default: {}, + typeOptions: { + multipleValues: true, + }, + placeholder: 'Add Phone', + displayOptions: { + show: { + resource: [ + 'company', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + name: 'phonesValues', + displayName: 'Phones', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'string', + default: '', + }, + { + displayName: 'Number', + name: 'number', + type: 'string', + default: '', + }, + ], + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* company:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'company', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'company', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 200, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'company', + ], + }, + }, + options: [ + { + displayName: 'Company Name', + name: 'companyName', + type: 'string', + default: '', + description: 'Company name to query on', + }, + { + displayName: 'Order', + name: 'order', + type: 'options', + options: [ + { + name: 'Date Created', + value: 'datecreated', + }, + { + name: 'ID', + value: 'id', + }, + { + name: 'Name', + value: 'name', + }, + ], + default: '', + description: 'Attribute to order items by', + }, + { + displayName: 'Order Direction', + name: 'orderDirection', + type: 'options', + options: [ + { + name: 'ASC', + value: 'ascending', + }, + { + name: 'DES', + value: 'descending', + }, + ], + default: '', + }, + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: `Comma-delimited list of Company properties to include in the response.
+ (Fields such as notes, fax_number and custom_fields aren't included, by default.)`, + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Keap/CompanyInterface.ts b/packages/nodes-base/nodes/Keap/CompanyInterface.ts new file mode 100644 index 000000000..7bf32c78e --- /dev/null +++ b/packages/nodes-base/nodes/Keap/CompanyInterface.ts @@ -0,0 +1,12 @@ +import { IDataObject } from 'n8n-workflow'; + +export interface ICompany { + address?: IDataObject; + company_name?: string; + email_address?: string; + fax_number?: IDataObject; + notes?: string; + opt_in_reason?: string; + phone_number?: IDataObject; + website?: string; +} diff --git a/packages/nodes-base/nodes/Keap/ConctactInterface.ts b/packages/nodes-base/nodes/Keap/ConctactInterface.ts new file mode 100644 index 000000000..1bbdb6d6f --- /dev/null +++ b/packages/nodes-base/nodes/Keap/ConctactInterface.ts @@ -0,0 +1,72 @@ +import { + IDataObject, + } from 'n8n-workflow'; + +export interface IAddress { + country_code?: string; + field?: string; + line1?: string; + line2?: string; + locality?: string; + postal_code?: string; + region?: string; + zip_code?: string; + zip_four?: string; +} + +export interface ICustomField { + content: IDataObject; + id: number; +} + +export interface IEmailContact { + email?: string; + field?: string; +} + +export interface IFax { + field?: string; + number?: string; + type?: string; +} + +export interface IPhone { + extension?: string; + field?: string; + number?: string; + type?: string; +} + +export interface ISocialAccount { + name?: string; + type?: string; +} + +export interface IContact { + addresses?: IAddress[]; + anniversary?: string; + company?: IDataObject; + contact_type?: string; + custom_fields?: ICustomField[]; + duplicate_option?: string; + email_addresses?: IEmailContact[]; + family_name?: string; + fax_numbers?: IFax[]; + given_name?: string; + job_title?: string; + lead_source_id?: number; + middle_name?: string; + opt_in_reason?: string; + origin?: IDataObject; + owner_id?: number; + phone_numbers?: IPhone[]; + preferred_locale?: string; + preferred_name?: string; + prefix?: string; + social_accounts?: ISocialAccount[]; + source_type?: string; + spouse_name?: string; + suffix?: string; + time_zone?: string; + website?: string; +} diff --git a/packages/nodes-base/nodes/Keap/ContactDescription.ts b/packages/nodes-base/nodes/Keap/ContactDescription.ts new file mode 100644 index 000000000..b1611c518 --- /dev/null +++ b/packages/nodes-base/nodes/Keap/ContactDescription.ts @@ -0,0 +1,760 @@ +import { + INodeProperties, + } from 'n8n-workflow'; + +export const contactOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'contact', + ], + }, + }, + options: [ + { + name: 'Create/Update', + value: 'upsert', + description: 'Create/update a contact', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an contact', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve an contact', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all contacts', + }, + ], + default: 'upsert', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const contactFields = [ + +/* -------------------------------------------------------------------------- */ +/* contact:upsert */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Duplicate Option', + name: 'duplicateOption', + required: true, + type: 'options', + options: [ + { + name: 'Email', + value: 'email', + }, + { + name: 'Email And Name', + value: 'emailAndName', + }, + ], + displayOptions: { + show: { + operation: [ + 'upsert', + ], + resource: [ + 'contact', + ], + }, + }, + default: 'email', + description: `Performs duplicate checking by one of the following options: Email, EmailAndName,
+ if a match is found using the option provided, the existing contact will be updated` + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'upsert', + ], + resource: [ + 'contact', + ], + }, + }, + options: [ + { + displayName: 'Anniversary', + name: 'anniversary', + type: 'dateTime', + default: '', + }, + { + displayName: 'Company ID', + name: 'companyId', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + }, + { + displayName: 'Contact Type', + name: 'contactType', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getContactTypes', + }, + default: '', + }, + { + displayName: 'Family Name', + name: 'familyName', + type: 'string', + default: '', + }, + { + displayName: 'Given Name', + name: 'givenName', + type: 'string', + default: '', + }, + { + displayName: 'IP Address', + name: 'ipAddress', + type: 'string', + default: '', + }, + { + displayName: 'Job Title', + name: 'jobTitle', + type: 'string', + default: '', + }, + { + displayName: 'Lead Source ID', + name: 'leadSourceId', + type: 'number', + default: 0, + }, + { + displayName: 'Middle Name', + name: 'middleName', + type: 'string', + default: '', + }, + { + displayName: 'Opt In Reason', + name: 'optInReason', + type: 'string', + default: '', + }, + { + displayName: 'Owner ID', + name: 'ownerId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + }, + { + displayName: 'Preferred Locale', + name: 'preferredLocale', + type: 'string', + placeholder: 'en', + default: '', + }, + { + displayName: 'Preferred Name', + name: 'preferredName', + type: 'string', + default: '', + }, + { + displayName: 'Source Type', + name: 'sourceType', + type: 'options', + options: [ + { + name: 'API', + value: 'API', + }, + { + name: 'Import', + value: 'IMPORT', + }, + { + name: 'Landing Page', + value: 'LANDINGPAGE', + }, + { + name: 'Manual', + value: 'MANUAL', + }, + { + name: 'Other', + value: 'OTHER', + }, + { + name: 'Unknown', + value: 'UNKNOWN', + }, + ], + default: '', + }, + { + displayName: 'Spouse Name', + name: 'spouseName', + type: 'string', + default: '', + }, + { + displayName: 'Timezone', + name: 'timezone', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTimezones', + }, + default: '', + }, + { + displayName: 'Website', + name: 'website', + type: 'string', + default: '', + }, + ], + }, + { + displayName: 'Addresses', + name: 'addressesUi', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: '', + placeholder: 'Add Address', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'upsert', + ], + }, + }, + options: [ + { + name: 'addressesValues', + displayName: 'Address', + values: [ + { + displayName: 'Field', + name: 'field', + type: 'options', + options: [ + { + name: 'Billing', + value: 'BILLING', + }, + { + name: 'Shipping', + value: 'SHIPPING', + }, + { + name: 'Other', + value: 'OTHER', + }, + ], + default: '', + }, + { + displayName: 'Country Code', + name: 'countryCode', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCountries', + }, + default: '', + }, + { + displayName: 'Line 1', + name: 'line1', + type: 'string', + default: '', + }, + { + displayName: 'Line 2', + name: 'line2', + type: 'string', + default: '', + }, + { + displayName: 'Locality', + name: 'locality', + type: 'string', + default: '', + }, + { + displayName: 'Postal Code', + name: 'postalCode', + type: 'string', + default: '', + }, + { + displayName: 'Region', + name: 'region', + type: 'string', + default: '', + }, + { + displayName: 'Zip Code', + name: 'zipCode', + type: 'string', + default: '', + }, + { + displayName: 'Zip Four', + name: 'zipFour', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Emails', + name: 'emailsUi', + type: 'fixedCollection', + default: {}, + typeOptions: { + multipleValues: true, + }, + placeholder: 'Add Email', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'upsert', + ], + }, + }, + options: [ + { + name: 'emailsValues', + displayName: 'Email', + values: [ + { + displayName: 'Field', + name: 'field', + type: 'options', + options: [ + { + name: 'Email 1', + value: 'EMAIL1', + }, + { + name: 'Email 2', + value: 'EMAIL2', + }, + { + name: 'Email 3', + value: 'EMAIL3', + }, + ], + default: '', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + }, + ], + } + ], + }, + { + displayName: 'Faxes', + name: 'faxesUi', + type: 'fixedCollection', + default: {}, + typeOptions: { + multipleValues: true, + }, + placeholder: 'Add Fax', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'upsert', + ], + }, + }, + options: [ + { + name: 'faxesValues', + displayName: 'Fax', + values: [ + { + displayName: 'Field', + name: 'field', + type: 'options', + options: [ + { + name: 'Fax 1', + value: 'FAX1', + }, + { + name: 'Fax 2', + value: 'FAX2', + }, + ], + default: '', + }, + { + displayName: 'Number', + name: 'number', + type: 'string', + default: '', + }, + ], + } + ], + }, + { + displayName: 'Phones', + name: 'phonesUi', + type: 'fixedCollection', + default: {}, + typeOptions: { + multipleValues: true, + }, + placeholder: 'Add Phone', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'upsert', + ], + }, + }, + options: [ + { + name: 'phonesValues', + displayName: 'Phones', + values: [ + { + displayName: 'Field', + name: 'field', + type: 'options', + options: [ + { + name: 'Phone 1', + value: 'PHONE1', + }, + { + name: 'Phone 2', + value: 'PHONE2', + }, + { + name: 'Phone 3', + value: 'PHONE3', + }, + { + name: 'Phone 4', + value: 'PHONE4', + }, + { + name: 'Phone 5', + value: 'PHONE5', + }, + ], + default: '', + }, + { + displayName: 'Number', + name: 'number', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Social Accounts', + name: 'socialAccountsUi', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: '', + placeholder: 'Add Social Account', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'upsert', + ], + }, + }, + options: [ + { + name: 'socialAccountsValues', + displayName: 'Social Account', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Facebook', + value: 'Facebook', + }, + { + name: 'Twitter', + value: 'Twitter', + }, + { + name: 'LinkedIn', + value: 'LinkedIn', + }, + ], + default: '', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + }, + ], + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* contact:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'contact', + ], + }, + }, + default: '', + }, +/* -------------------------------------------------------------------------- */ +/* contact:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'contact', + ], + }, + }, + default: '', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Options', + default: {}, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'contact', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: `Comma-delimited list of Contact properties to include in the response.
+ (Some fields such as lead_source_id, custom_fields, and job_title aren't included, by default.)`, + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* contact:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'contact', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'contact', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 200, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'contact', + ], + }, + }, + options: [ + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + }, + { + displayName: 'Given Name', + name: 'givenName', + type: 'string', + default: '', + }, + { + displayName: 'Family Name', + name: 'familyName', + type: 'string', + default: '', + }, + { + displayName: 'Order', + name: 'order', + type: 'options', + options: [ + { + name: 'Date', + value: 'date', + }, + { + name: 'Email', + value: 'email', + }, + { + name: 'ID', + value: 'id', + }, + { + name: 'Name', + value: 'name', + }, + ], + default: '', + description: 'Attribute to order items by', + }, + { + displayName: 'Order Direction', + name: 'orderDirection', + type: 'options', + options: [ + { + name: 'ASC', + value: 'ascending', + }, + { + name: 'DES', + value: 'descending', + }, + ], + default: '', + }, + { + displayName: 'Since', + name: 'since', + type: 'dateTime', + default: '', + description: 'Date to start searching from on LastUpdated', + }, + { + displayName: 'Until', + name: 'until', + type: 'dateTime', + default: '', + description: 'Date to search to on LastUpdated', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Keap/ContactNoteDescription.ts b/packages/nodes-base/nodes/Keap/ContactNoteDescription.ts new file mode 100644 index 000000000..aa625c3b6 --- /dev/null +++ b/packages/nodes-base/nodes/Keap/ContactNoteDescription.ts @@ -0,0 +1,382 @@ +import { + INodeProperties, + } from 'n8n-workflow'; + +export const contactNoteOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'contactNote', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a note', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a note', + }, + { + name: 'Get', + value: 'get', + description: 'Get a notes', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all notes', + }, + { + name: 'Update', + value: 'update', + description: 'Update a note', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const contactNoteFields = [ + +/* -------------------------------------------------------------------------- */ +/* contactNote:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'User ID', + name: 'userId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'contactNote', + ], + }, + }, + default: '', + description: 'The infusionsoft user to create the note on behalf of', + }, + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'contactNote', + ], + }, + }, + default: '', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'contactNote', + ], + }, + }, + options: [ + { + displayName: 'Body', + name: 'body', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Appointment', + value: 'appointment', + }, + { + name: 'Call', + value: 'call', + }, + { + name: 'Email', + value: 'email', + }, + { + name: 'Fax', + value: 'fax', + }, + { + name: 'Letter', + value: 'letter', + }, + { + name: 'Other', + value: 'other', + }, + ], + default: '', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* contactNote:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Note ID', + name: 'noteId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'contactNote', + ], + }, + }, + default: '', + }, +/* -------------------------------------------------------------------------- */ +/* contactNote:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Note ID', + name: 'noteId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'contactNote', + ], + }, + }, + default: '', + }, +/* -------------------------------------------------------------------------- */ +/* contactNote:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'contactNote', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'contactNote', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 200, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'contactNote', + ], + }, + }, + options: [ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + }, + { + displayName: 'User ID', + name: 'userId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* contactNote:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Note ID', + name: 'noteId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'contactNote', + ], + }, + }, + default: '', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'contactNote', + ], + }, + }, + options: [ + { + displayName: 'Body', + name: 'body', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + }, + { + displayName: 'Contact ID', + name: 'contactId', + type: 'number', + typeOptions: { + minValue: 0 + }, + default: 0, + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Appointment', + value: 'appointment', + }, + { + name: 'Call', + value: 'call', + }, + { + name: 'Email', + value: 'email', + }, + { + name: 'Fax', + value: 'fax', + }, + { + name: 'Letter', + value: 'letter', + }, + { + name: 'Other', + value: 'other', + }, + ], + default: '', + }, + { + displayName: 'User ID', + name: 'userId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'The infusionsoft user to create the note on behalf of', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Keap/ContactNoteInterface.ts b/packages/nodes-base/nodes/Keap/ContactNoteInterface.ts new file mode 100644 index 000000000..221bd73e3 --- /dev/null +++ b/packages/nodes-base/nodes/Keap/ContactNoteInterface.ts @@ -0,0 +1,8 @@ + +export interface INote { + body?: string; + contact_id?: number; + title?: string; + type?: string; + user_id?: number; +} diff --git a/packages/nodes-base/nodes/Keap/ContactTagDescription.ts b/packages/nodes-base/nodes/Keap/ContactTagDescription.ts new file mode 100644 index 000000000..a212c112a --- /dev/null +++ b/packages/nodes-base/nodes/Keap/ContactTagDescription.ts @@ -0,0 +1,179 @@ +import { + INodeProperties, + } from 'n8n-workflow'; + +export const contactTagOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'contactTag', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Add a list of tags to a contact', + }, + { + name: 'Delete', + value: 'delete', + description: `Delete a contact's tag`, + }, + { + name: 'Get All', + value: 'getAll', + description: `Retrieve all contact's tags`, + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const contactTagFields = [ + +/* -------------------------------------------------------------------------- */ +/* contactTag:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'contactTag', + ], + }, + }, + default: '', + }, + { + displayName: 'Tag IDs', + name: 'tagIds', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'contactTag', + ], + }, + }, + default: [], + }, +/* -------------------------------------------------------------------------- */ +/* contactTag:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'contactTag', + ], + }, + }, + default: '', + }, + { + displayName: 'Tag IDs', + name: 'tagIds', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'contactTag', + ], + }, + }, + default: 'Tag IDs, multiple ids can be set separated by comma.', + }, +/* -------------------------------------------------------------------------- */ +/* contactTag:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'contactTag', + ], + }, + }, + default: '', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'contactTag', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'contactTag', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 200, + }, + default: 100, + description: 'How many results to return.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Keap/EcommerceOrderDescripion.ts b/packages/nodes-base/nodes/Keap/EcommerceOrderDescripion.ts new file mode 100644 index 000000000..2d43239fb --- /dev/null +++ b/packages/nodes-base/nodes/Keap/EcommerceOrderDescripion.ts @@ -0,0 +1,486 @@ +import { + INodeProperties, + } from 'n8n-workflow'; + +export const ecommerceOrderOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'ecommerceOrder', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create an ecommerce order', + }, + { + name: 'Get', + value: 'get', + description: 'Get an ecommerce order', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an ecommerce order', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all ecommerce orders', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const ecommerceOrderFields = [ + +/* -------------------------------------------------------------------------- */ +/* ecommerceOrder:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'ecommerceOrder', + ], + }, + }, + default: '', + }, + { + displayName: 'Order Date', + name: 'orderDate', + type: 'dateTime', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'ecommerceOrder', + ], + }, + }, + default: '', + }, + { + displayName: 'Order Title', + name: 'orderTitle', + type: 'dateTime', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'ecommerceOrder', + ], + }, + }, + default: '', + }, + { + displayName: 'Order Type', + name: 'orderType', + type: 'options', + options: [ + { + name: 'Offline', + value: 'offline', + }, + { + name: 'Online', + value: 'online', + }, + ], + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'ecommerceOrder', + ], + }, + }, + default: '', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'ecommerceOrder', + ], + }, + }, + options: [ + { + displayName: 'Lead Affiliate ID', + name: 'leadAffiliateId', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + }, + { + displayName: 'Promo Codes', + name: 'promoCodes', + type: 'string', + default: '', + description: `Uses multiple strings separated by comma as promo codes.
+ The corresponding discount will be applied to the order.` + }, + { + displayName: 'Sales Affiliate ID', + name: 'salesAffiliateId', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + }, + ], + }, + { + displayName: 'Shipping Address', + name: 'addressUi', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + default: '', + placeholder: 'Add Address', + displayOptions: { + show: { + resource: [ + 'ecommerceOrder', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + name: 'addressValues', + displayName: 'Address', + values: [ + { + displayName: 'Company', + name: 'company', + type: 'string', + default: '', + }, + { + displayName: 'Country Code', + name: 'countryCode', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCountries', + }, + default: '', + }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + }, + { + displayName: 'Middle Name', + name: 'middleName', + type: 'string', + default: '', + }, + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + default: '', + }, + { + displayName: 'Line 1', + name: 'line1', + type: 'string', + default: '', + }, + { + displayName: 'Line 2', + name: 'line2', + type: 'string', + default: '', + }, + { + displayName: 'Locality', + name: 'locality', + type: 'string', + default: '', + }, + { + displayName: 'Region', + name: 'region', + type: 'string', + default: '', + }, + { + displayName: 'Zip Code', + name: 'zipCode', + type: 'string', + default: '', + }, + { + displayName: 'Zip Four', + name: 'zipFour', + type: 'string', + default: '', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Order Items', + name: 'orderItemsUi', + type: 'fixedCollection', + placeholder: 'Add Order Item', + typeOptions: { + multipleValues: true, + }, + default: {}, + displayOptions: { + show: { + resource: [ + 'ecommerceOrder', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + name: 'orderItemsValues', + displayName: 'Order Item', + values: [ + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + }, + { + displayName: 'Price', + name: 'price', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + description: `Overridable price of the product, if not specified,
+ the default will be used.`, + }, + { + displayName: 'Product ID', + name: 'product ID', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + }, + { + displayName: 'Quantity', + name: 'quantity', + type: 'number', + typeOptions: { + minValue: 1, + }, + default: 1, + }, + ], + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* ecommerceOrder:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Order ID', + name: 'orderId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'ecommerceOrder', + ], + }, + }, + default: '', + }, +/* -------------------------------------------------------------------------- */ +/* ecommerceOrder:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Order ID', + name: 'orderId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'ecommerceOrder', + ], + }, + }, + default: '', + }, +/* -------------------------------------------------------------------------- */ +/* ecommerceOrder:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'ecommerceOrder', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'ecommerceOrder', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 200, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'ecommerceOrder', + ], + }, + }, + options: [ + { + displayName: 'Since', + name: 'since', + type: 'dateTime', + default: '', + description: 'Date to start searching from', + }, + { + displayName: 'Until', + name: 'until', + type: 'dateTime', + default: '', + description: 'Date to search to', + }, + { + displayName: 'Paid', + name: 'paid', + type: 'boolean', + default: false, + }, + { + displayName: 'Order', + name: 'order', + type: 'string', + default: '', + description: 'Attribute to order items by', + }, + { + displayName: 'Contact ID', + name: 'contactId', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + }, + { + displayName: 'Product ID', + name: 'productId', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Keap/EcommerceOrderInterface.ts b/packages/nodes-base/nodes/Keap/EcommerceOrderInterface.ts new file mode 100644 index 000000000..ba7eb8264 --- /dev/null +++ b/packages/nodes-base/nodes/Keap/EcommerceOrderInterface.ts @@ -0,0 +1,35 @@ + + +export interface IItem { + description?: string; + price?: number; + product_id?: number; + quantity?: number; +} + +export interface IShippingAddress { + company?: string; + country_code?: string; + first_name?: string; + last_name?: string; + line1?: string; + line2?: string; + locality?: string; + middle_name?: string; + postal_code?: string; + region?: string; + zip_code?: string; + zip_four?: string; +} + +export interface IEcommerceOrder { + contact_id: number; + lead_affiliate_id?: string; + order_date: string; + order_items?: IItem[]; + order_title: string; + order_type?: string; + promo_codes?: string[]; + sales_affiliate_id?: number; + shipping_address?: IShippingAddress; +} diff --git a/packages/nodes-base/nodes/Keap/EcommerceProductDescription.ts b/packages/nodes-base/nodes/Keap/EcommerceProductDescription.ts new file mode 100644 index 000000000..09ec6ea09 --- /dev/null +++ b/packages/nodes-base/nodes/Keap/EcommerceProductDescription.ts @@ -0,0 +1,236 @@ +import { + INodeProperties, + } from 'n8n-workflow'; + +export const ecommerceProductOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'ecommerceProduct', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create an ecommerce product', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an ecommerce product', + }, + { + name: 'Get', + value: 'get', + description: 'Get an ecommerce product', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all ecommerce product', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const ecommerceProductFields = [ + +/* -------------------------------------------------------------------------- */ +/* ecommerceProduct:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Product Name', + name: 'productName', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'ecommerceProduct', + ], + }, + }, + default: '', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'ecommerceProduct', + ], + }, + }, + options: [ + { + displayName: 'Active', + name: 'active', + type: 'boolean', + default: false, + }, + { + displayName: 'Product Description', + name: 'productDesc', + typeOptions: { + alwaysOpenEditWindow: true, + }, + type: 'string', + default: '', + }, + { + displayName: 'Product Price', + name: 'productPrice', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + }, + { + displayName: 'Product Short Desc', + name: 'productShortDesc', + type: 'string', + default: '', + }, + { + displayName: 'SKU', + name: 'sku', + type: 'string', + default: '', + }, + { + displayName: 'Subscription Only', + name: 'subscriptionOnly', + type: 'boolean', + default: false, + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* ecommerceProduct:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Product ID', + name: 'productId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'ecommerceProduct', + ], + }, + }, + default: '', + }, +/* -------------------------------------------------------------------------- */ +/* ecommerceProduct:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Product ID', + name: 'productId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'ecommerceProduct', + ], + }, + }, + default: '', + }, +/* -------------------------------------------------------------------------- */ +/* ecommerceProduct:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'ecommerceProduct', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'ecommerceProduct', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 200, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'ecommerceProduct', + ], + }, + }, + options: [ + { + displayName: 'Active', + name: 'active', + type: 'boolean', + default: false, + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Keap/EcommerceProductInterface.ts b/packages/nodes-base/nodes/Keap/EcommerceProductInterface.ts new file mode 100644 index 000000000..23c2fbb49 --- /dev/null +++ b/packages/nodes-base/nodes/Keap/EcommerceProductInterface.ts @@ -0,0 +1,10 @@ + +export interface IEcommerceProduct { + active?: string; + product_name?: string; + product_desc?: string; + product_price?: number; + product_short_desc?: string; + sku?: string; + subscription_only?: boolean; +} diff --git a/packages/nodes-base/nodes/Keap/EmaiIInterface.ts b/packages/nodes-base/nodes/Keap/EmaiIInterface.ts new file mode 100644 index 000000000..5a8fdf512 --- /dev/null +++ b/packages/nodes-base/nodes/Keap/EmaiIInterface.ts @@ -0,0 +1,15 @@ +export interface IAttachment { + file_data?: string; + file_name?: string; +} + + +export interface IEmail { + address_field?: string; + attachments?: IAttachment[]; + contacts: number[]; + html_content?: string; + plain_content?: string; + subject?: string; + user_id: number; +} diff --git a/packages/nodes-base/nodes/Keap/EmailDescription.ts b/packages/nodes-base/nodes/Keap/EmailDescription.ts new file mode 100644 index 000000000..0f7015250 --- /dev/null +++ b/packages/nodes-base/nodes/Keap/EmailDescription.ts @@ -0,0 +1,464 @@ +import { + INodeProperties, + } from 'n8n-workflow'; + +export const emailOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'email', + ], + }, + }, + options: [ + { + name: 'Create Record', + value: 'createRecord', + description: 'Create a record of an email sent to a contact', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all sent emails', + }, + { + name: 'Send', + value: 'send', + description: 'Send Email', + }, + ], + default: 'createRecord', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const emailFields = [ + +/* -------------------------------------------------------------------------- */ +/* email:createRecord */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Sent To Address', + name: 'sentToAddress', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'createRecord', + ], + resource: [ + 'email', + ], + }, + }, + default: '', + }, + { + displayName: 'Sent From Address', + name: 'sentFromAddress', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'createRecord', + ], + resource: [ + 'email', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'createRecord', + ], + resource: [ + 'email', + ], + }, + }, + options: [ + { + displayName: 'Clicked Date', + name: 'clickedDate', + type: 'dateTime', + default: '', + }, + { + displayName: 'Contact ID', + name: 'contactId', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + }, + { + displayName: 'Headers', + name: 'headers', + type: 'string', + default: '', + }, + { + displayName: 'HTML content', + name: 'htmlContent', + type: 'string', + default: '', + description: 'Base64 encoded HTML', + }, + { + displayName: 'Opened Date', + name: 'openedDate', + type: 'dateTime', + default: '', + }, + { + displayName: 'Original Provider', + name: 'originalProvider', + type: 'options', + options: [ + { + name: 'Unknown', + value: 'UNKNOWN', + }, + { + name: 'Infusionsoft', + value: 'INFUSIONSOFT', + }, + { + name: 'Microsoft', + value: 'MICROSOFT', + }, + { + name: 'Google', + value: 'GOOGLE', + }, + ], + default: 'UNKNOWN', + description: 'Provider that sent the email case insensitive, must be in list', + }, + { + displayName: 'Original Provider ID', + name: 'originalProviderId', + type: 'string', + default: '', + description: `Provider id that sent the email, must be unique when combined with provider.
+ If omitted a UUID without dashes is autogenerated for the record.` + }, + { + displayName: 'Plain Content', + name: 'plainContent', + type: 'string', + default: '', + description: 'Base64 encoded text', + }, + { + displayName: 'Provider Source ID', + name: 'providerSourceId', + type: 'string', + default: 'The email address of the synced email account that generated this message.', + }, + { + displayName: 'Received Date', + name: 'receivedDate', + type: 'dateTime', + default: '', + }, + { + displayName: 'Sent Date', + name: 'sentDate', + type: 'dateTime', + default: '', + }, + { + displayName: 'Sent From Reply Address', + name: 'sentFromReplyAddress', + type: 'string', + default: '', + }, + { + displayName: 'Sent To Bcc Addresses', + name: 'sentToBccAddresses', + type: 'string', + default: '', + }, + { + displayName: 'Sent To CC Addresses', + name: 'sentToCCAddresses', + type: 'string', + default: '', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'string', + default: '', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* email:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'email', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'email', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 200, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'email', + ], + }, + }, + options: [ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + }, + { + displayName: 'Since Sent Date', + name: 'sinceSentDate', + type: 'dateTime', + default: '', + description: 'Emails sent since the provided date, must be present if untilDate is provided', + + }, + { + displayName: 'Until Sent Date', + name: 'untilSentDate', + type: 'dateTime', + default: '', + description: 'Email sent until the provided date', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* email:send */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'User ID', + name: 'userId', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'email', + ], + }, + }, + default: '', + description: 'The infusionsoft user to send the email on behalf of', + }, + { + displayName: 'Contact IDs', + name: 'contactIds', + type: 'string', + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'email', + ], + }, + }, + default: '', + description: 'Contact Ids to receive the email. Multiple can be added seperated by comma', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'string', + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'email', + ], + }, + }, + default: '', + description: 'The subject line of the email', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'email', + ], + }, + }, + options: [ + { + displayName: 'Address field', + name: 'addressField', + type: 'string', + default: '', + description: `Email field of each Contact record to address the email to, such as
+ 'EmailAddress1', 'EmailAddress2', 'EmailAddress3', defaulting to the contact's primary email`, + }, + { + displayName: 'HTML Content', + name: 'htmlContent', + type: 'string', + default: '', + description: 'The HTML-formatted content of the email, encoded in Base64', + }, + { + displayName: 'Plain Content', + name: 'plainContent', + type: 'string', + default: '', + description: 'The plain-text content of the email, encoded in Base64', + }, + ], + }, + { + displayName: 'Attachments', + name: 'attachmentsUi', + placeholder: 'Add Attachments', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'email', + ], + }, + }, + options: [ + { + name: 'attachmentsValues', + displayName: 'Attachments Values', + values: [ + { + displayName: 'File Data', + name: 'fileData', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'The content of the attachment, encoded in Base64', + }, + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + default: '', + description: 'The filename of the attached file, including extension', + }, + ], + }, + { + name: 'attachmentsBinary', + displayName: 'Attachments Binary', + values: [ + { + displayName: 'Property', + name: 'property', + type: 'string', + default: '', + description: 'Name of the binary properties which contain data which should be added to email as attachment', + }, + ], + }, + ], + default: '', + description: 'Attachments to be sent with each copy of the email, maximum of 10 with size of 1MB each', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Keap/FileDescription.ts b/packages/nodes-base/nodes/Keap/FileDescription.ts new file mode 100644 index 000000000..863590a61 --- /dev/null +++ b/packages/nodes-base/nodes/Keap/FileDescription.ts @@ -0,0 +1,404 @@ +import { + INodeProperties, + } from 'n8n-workflow'; + +export const fileOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'file', + ], + }, + }, + options: [ + { + name: 'Delete', + value: 'delete', + description: 'Delete a file', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all files', + }, + { + name: 'Upload', + value: 'upload', + description: 'Upload a file', + }, + ], + default: 'delete', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const fileFields = [ +/* -------------------------------------------------------------------------- */ +/* file:upload */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Binary Data', + name: 'binaryData', + type: 'boolean', + default: false, + displayOptions: { + show: { + operation: [ + 'upload' + ], + resource: [ + 'file', + ], + }, + }, + description: 'If the data to upload should be taken from binary field.', + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + required: true, + displayOptions: { + show: { + operation: [ + 'upload' + ], + resource: [ + 'file', + ], + binaryData: [ + true, + ], + }, + }, + description: 'Name of the binary property which contains
the data for the file to be uploaded.', + }, + { + displayName: 'File Association', + name: 'fileAssociation', + type: 'options', + options: [ + { + name: 'Company', + value: 'company', + }, + { + name: 'Contact', + value: 'contact', + }, + { + name: 'User', + value: 'user', + }, + ], + required: true, + displayOptions: { + show: { + operation: [ + 'upload', + ], + resource: [ + 'file', + ], + }, + }, + default: '', + }, + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'upload', + ], + resource: [ + 'file', + ], + fileAssociation: [ + 'contact', + ], + }, + }, + default: '', + }, + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'upload', + ], + resource: [ + 'file', + ], + binaryData: [ + false, + ], + }, + }, + default: '', + description: 'The filename of the attached file, including extension', + }, + { + displayName: 'File Data', + name: 'fileData', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + required: true, + displayOptions: { + show: { + operation: [ + 'upload', + ], + resource: [ + 'file', + ], + binaryData: [ + false, + ], + }, + }, + default: '', + description: 'The content of the attachment, encoded in Base64', + }, + { + displayName: 'Is Public', + name: 'isPublic', + type: 'boolean', + default: false, + displayOptions: { + show: { + operation: [ + 'upload' + ], + resource: [ + 'file', + ], + }, + }, + }, +/* -------------------------------------------------------------------------- */ +/* file:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'File ID', + name: 'fileId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'file', + ], + }, + }, + default: '', + }, +/* -------------------------------------------------------------------------- */ +/* file:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'file', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'file', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 200, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'file', + ], + }, + }, + options: [ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'number', + typeOptions: { + minValue: 0 + }, + default: 0, + description: 'Filter based on Contact Id, if user has permission to see Contact files.', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: `Filter files based on name, with '*' preceding or following to indicate LIKE queries.`, + }, + { + displayName: 'Permission', + name: 'permission', + type: 'options', + options: [ + { + name: 'User', + value: 'user', + }, + { + name: 'Company', + value: 'company', + }, + { + name: 'Both', + value: 'both', + }, + ], + default: 'both', + description: 'Filter based on the permission of files', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Application', + value: 'application', + }, + { + name: 'Image', + value: 'image', + }, + { + name: 'Fax', + value: 'fax', + }, + { + name: 'Attachment', + value: 'attachment', + }, + { + name: 'Ticket', + value: 'ticket', + }, + { + name: 'Contact', + value: 'contact', + }, + { + name: 'Digital Product', + value: 'digitalProduct', + }, + { + name: 'Import', + value: 'import', + }, + { + name: 'Hidden', + value: 'hidden', + }, + { + name: 'Webform', + value: 'webform', + }, + { + name: 'Style Cart', + value: 'styleCart', + }, + { + name: 'Re Sampled Image', + value: 'reSampledImage', + }, + { + name: 'Template Thumnail', + value: 'templateThumnail', + }, + { + name: 'Funnel', + value: 'funnel', + }, + { + name: 'Logo Thumnail', + value: 'logoThumnail', + }, + ], + default: '', + description: 'Filter based on the type of file.', + }, + { + displayName: 'Viewable', + name: 'viewable', + type: 'options', + options: [ + { + name: 'Public', + value: 'public', + }, + { + name: 'Private', + value: 'private', + }, + { + name: 'Both', + value: 'both', + }, + ], + default: 'both', + description: 'Include public or private files in response', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Keap/FileInterface.ts b/packages/nodes-base/nodes/Keap/FileInterface.ts new file mode 100644 index 000000000..e924826d2 --- /dev/null +++ b/packages/nodes-base/nodes/Keap/FileInterface.ts @@ -0,0 +1,8 @@ + +export interface IFile { + file_name?: string; + file_data?: string; + contact_id?: number; + is_public?: boolean; + file_association?: string; +} diff --git a/packages/nodes-base/nodes/Keap/GenericFunctions.ts b/packages/nodes-base/nodes/Keap/GenericFunctions.ts new file mode 100644 index 000000000..c04fb057d --- /dev/null +++ b/packages/nodes-base/nodes/Keap/GenericFunctions.ts @@ -0,0 +1,82 @@ +import { + OptionsWithUri, + } from 'request'; + +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject +} from 'n8n-workflow'; + +import { + snakeCase, + } from 'change-case'; + +export async function keapApiRequest(this: IWebhookFunctions | IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, headers: IDataObject = {}, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + let options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: uri || `https://api.infusionsoft.com/crm/rest/v1${resource}`, + json: true + }; + try { + options = Object.assign({}, options, option); + if (Object.keys(headers).length !== 0) { + options.headers = Object.assign({}, options.headers, headers); + } + if (Object.keys(body).length === 0) { + delete options.body; + } + //@ts-ignore + return await this.helpers.requestOAuth.call(this, 'keapOAuth2Api', options); + } catch (error) { + if (error.response && error.response.body && error.response.body.message) { + // Try to return the error prettier + throw new Error(`Infusionsoft error response [${error.statusCode}]: ${error.response.body.message}`); + } + throw error; + } +} + +export async function keapApiRequestAllItems(this: IHookFunctions| IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + let uri: string | undefined; + query.limit = 50; + + do { + responseData = await keapApiRequest.call(this, method, endpoint, body, query, uri); + uri = responseData.next; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + returnData.length < responseData.count + ); + + return returnData; +} + +export function keysToSnakeCase(elements: IDataObject[] | IDataObject) : IDataObject[] { + if (!Array.isArray(elements)) { + elements = [elements]; + } + for (const element of elements) { + for (const key of Object.keys(element)) { + if (key !== snakeCase(key)) { + element[snakeCase(key)] = element[key]; + delete element[key]; + } + } + } + return elements; +} diff --git a/packages/nodes-base/nodes/Keap/Keap.node.ts b/packages/nodes-base/nodes/Keap/Keap.node.ts new file mode 100644 index 000000000..45c755ffd --- /dev/null +++ b/packages/nodes-base/nodes/Keap/Keap.node.ts @@ -0,0 +1,811 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeTypeDescription, + INodeType, + ILoadOptionsFunctions, + INodePropertyOptions, + IBinaryKeyData, +} from 'n8n-workflow'; + +import { + keapApiRequest, + keapApiRequestAllItems, + keysToSnakeCase, +} from './GenericFunctions'; + +import { + contactOperations, + contactFields, +} from './ContactDescription'; + +import { + contactNoteOperations, + contactNoteFields, +} from './ContactNoteDescription'; + +import { + contactTagOperations, + contactTagFields, +} from './ContactTagDescription'; + +import { + ecommerceOrderOperations, + ecommerceOrderFields, +} from './EcommerceOrderDescripion'; + +import { + ecommerceProductOperations, + ecommerceProductFields, +} from './EcommerceProductDescription'; + +import { + emailOperations, + emailFields, +} from './EmailDescription'; + +import { + fileOperations, + fileFields, +} from './FileDescription'; + +import { + companyOperations, + companyFields, + } from './CompanyDescription'; + +import { + IContact, + IAddress, + IFax, + IEmailContact, + ISocialAccount, + IPhone, +} from './ConctactInterface'; + +import { + IEmail, + IAttachment, +} from './EmaiIInterface'; + +import { + INote, +} from './ContactNoteInterface'; + +import { + IEcommerceOrder, + IItem, + IShippingAddress, +} from './EcommerceOrderInterface'; + +import { + IEcommerceProduct, +} from './EcommerceProductInterface'; + +import { + IFile, +} from './FileInterface'; + +import { + ICompany, +} from './CompanyInterface'; + +import { + pascalCase, + titleCase, +} from 'change-case'; + +import * as moment from 'moment-timezone'; + +export class Keap implements INodeType { + description: INodeTypeDescription = { + displayName: 'Keap', + name: ' keap', + icon: 'file:keap.png', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Keap API.', + defaults: { + name: 'Keap', + color: '#79af53', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'keapOAuth2Api', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Company', + value: 'company', + }, + { + name: 'Contact', + value: 'contact', + }, + { + name: 'Contact Note', + value: 'contactNote', + }, + { + name: 'Contact Tag', + value: 'contactTag', + }, + { + name: 'Ecommerce Order', + value: 'ecommerceOrder', + }, + { + name: 'Ecommerce Product', + value: 'ecommerceProduct', + }, + { + name: 'Email', + value: 'email', + }, + { + name: 'File', + value: 'file', + }, + ], + default: 'company', + description: 'The resource to operate on.', + }, + // COMPANY + ...companyOperations, + ...companyFields, + // CONTACT + ...contactOperations, + ...contactFields, + // CONTACT NOTE + ...contactNoteOperations, + ...contactNoteFields, + // CONTACT TAG + ...contactTagOperations, + ...contactTagFields, + // ECOMMERCE ORDER + ...ecommerceOrderOperations, + ...ecommerceOrderFields, + // ECOMMERCE PRODUCT + ...ecommerceProductOperations, + ...ecommerceProductFields, + // EMAIL + ...emailOperations, + ...emailFields, + // FILE + ...fileOperations, + ...fileFields, + ], + }; + + methods = { + loadOptions: { + // Get all the tags to display them to user so that he can + // select them easily + async getTags(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const tags = await keapApiRequestAllItems.call(this, 'tags', 'GET', '/tags'); + for (const tag of tags) { + const tagName = tag.name; + const tagId = tag.id; + returnData.push({ + name: tagName as string, + value: tagId as string, + }); + } + return returnData; + }, + // Get all the users to display them to user so that he can + // select them easily + async getUsers(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const users = await keapApiRequestAllItems.call(this, 'users', 'GET', '/users'); + for (const user of users) { + const userName = user.given_name; + const userId = user.id; + returnData.push({ + name: userName as string, + value: userId as string, + }); + } + return returnData; + }, + // Get all the countries to display them to user so that he can + // select them easily + async getCountries(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const { countries } = await keapApiRequest.call(this, 'GET', '/locales/countries'); + for (const key of Object.keys(countries)) { + const countryName = countries[key]; + const countryId = key; + returnData.push({ + name: countryName as string, + value: countryId as string, + }); + } + return returnData; + }, + // Get all the provinces to display them to user so that he can + // select them easily + async getProvinces(this: ILoadOptionsFunctions): Promise { + const countryCode = this.getCurrentNodeParameter('countryCode') as string; + const returnData: INodePropertyOptions[] = []; + const { provinces } = await keapApiRequest.call(this, 'GET', `/locales/countries/${countryCode}/provinces`); + for (const key of Object.keys(provinces)) { + const provinceName = provinces[key]; + const provinceId = key; + returnData.push({ + name: provinceName as string, + value: provinceId as string, + }); + } + return returnData; + }, + // Get all the contact types to display them to user so that he can + // select them easily + async getContactTypes(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const types = await keapApiRequest.call(this, 'GET', '/setting/contact/optionTypes'); + for (const type of types.value.split(',')) { + const typeName = type; + const typeId = type; + returnData.push({ + name: typeName, + value: typeId, + }); + } + return returnData; + }, + // Get all the timezones to display them to user so that he can + // select them easily + async getTimezones(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + for (const timezone of moment.tz.names()) { + const timezoneName = timezone; + const timezoneId = timezone; + returnData.push({ + name: timezoneName, + value: timezoneId, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < length; i++) { + if (resource === 'company') { + //https://developer.keap.com/docs/rest/#!/Company/createCompanyUsingPOST + if (operation === 'create') { + const addresses = (this.getNodeParameter('addressesUi', i) as IDataObject).addressesValues as IDataObject[]; + const faxes = (this.getNodeParameter('faxesUi', i) as IDataObject).faxesValues as IDataObject[]; + const phones = (this.getNodeParameter('phonesUi', i) as IDataObject).phonesValues as IDataObject[]; + const companyName = this.getNodeParameter('companyName', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: ICompany = { + company_name: companyName, + }; + keysToSnakeCase(additionalFields); + Object.assign(body, additionalFields); + if (addresses) { + body.address = keysToSnakeCase(addresses)[0] ; + } + if (faxes) { + body.fax_number = faxes[0]; + } + if (phones) { + body.phone_number = phones[0]; + } + responseData = await keapApiRequest.call(this, 'POST', '/companies', body); + } + //https://developer.infusionsoft.com/docs/rest/#!/Company/listCompaniesUsingGET + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const options = this.getNodeParameter('options', i) as IDataObject; + keysToSnakeCase(options); + Object.assign(qs, options); + if (qs.fields) { + qs.optional_properties = qs.fields; + delete qs.fields; + } + if (returnAll) { + responseData = await keapApiRequestAllItems.call(this, 'companies', 'GET', '/companies', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await keapApiRequest.call(this, 'GET', '/companies', {}, qs); + responseData = responseData.companies; + } + } + } + if (resource === 'contact') { + //https://developer.infusionsoft.com/docs/rest/#!/Contact/createOrUpdateContactUsingPUT + if (operation === 'upsert') { + const duplicateOption = this.getNodeParameter('duplicateOption', i) as string; + const addresses = (this.getNodeParameter('addressesUi', i) as IDataObject).addressesValues as IDataObject[]; + const emails = (this.getNodeParameter('emailsUi', i) as IDataObject).emailsValues as IDataObject[]; + const faxes = (this.getNodeParameter('faxesUi', i) as IDataObject).faxesValues as IDataObject[]; + const socialAccounts = (this.getNodeParameter('socialAccountsUi', i) as IDataObject).socialAccountsValues as IDataObject[]; + const phones = (this.getNodeParameter('phonesUi', i) as IDataObject).phonesValues as IDataObject[]; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: IContact = { + duplicate_option: pascalCase(duplicateOption), + }; + + if (additionalFields.anniversary) { + body.anniversary = additionalFields.anniversary as string; + } + if (additionalFields.contactType) { + body.contact_type = additionalFields.contactType as string; + } + if (additionalFields.familyName) { + body.family_name = additionalFields.familyName as string; + } + if (additionalFields.givenName) { + body.given_name = additionalFields.givenName as string; + } + if (additionalFields.jobTitle) { + body.job_title = additionalFields.jobTitle as string; + } + if (additionalFields.leadSourceId) { + body.lead_source_id = additionalFields.leadSourceId as number; + } + if (additionalFields.middleName) { + body.middle_name = additionalFields.middleName as string; + } + if (additionalFields.middleName) { + body.middle_name = additionalFields.middleName as string; + } + if (additionalFields.OptInReason) { + body.opt_in_reason = additionalFields.OptInReason as string; + } + if (additionalFields.ownerId) { + body.owner_id = additionalFields.ownerId as number; + } + if (additionalFields.preferredLocale) { + body.preferred_locale = additionalFields.preferredLocale as string; + } + if (additionalFields.preferredName) { + body.preferred_name = additionalFields.preferredName as string; + } + if (additionalFields.sourceType) { + body.source_type = additionalFields.sourceType as string; + } + if (additionalFields.spouseName) { + body.spouse_name = additionalFields.spouseName as string; + } + if (additionalFields.timezone) { + body.time_zone = additionalFields.timezone as string; + } + if (additionalFields.website) { + body.website = additionalFields.website as string; + } + if (additionalFields.ipAddress) { + body.origin = { ip_address: additionalFields.ipAddress as string }; + } + if (additionalFields.companyId) { + body.company = { id: additionalFields.companyId as number }; + } + if (addresses) { + body.addresses = keysToSnakeCase(addresses) as IAddress[]; + } + if (emails) { + body.email_addresses = emails as IEmailContact[]; + } + if (faxes) { + body.fax_numbers = faxes as IFax[]; + } + if (socialAccounts) { + body.social_accounts = socialAccounts as ISocialAccount[]; + } + if (phones) { + body.phone_numbers = phones as IPhone[]; + } + responseData = await keapApiRequest.call(this, 'PUT', '/contacts', body); + } + //https://developer.infusionsoft.com/docs/rest/#!/Contact/deleteContactUsingDELETE + if (operation === 'delete') { + const contactId = parseInt(this.getNodeParameter('contactId', i) as string, 10); + responseData = await keapApiRequest.call(this, 'DELETE', `/contacts/${contactId}`); + responseData = { success: true }; + } + //https://developer.infusionsoft.com/docs/rest/#!/Contact/getContactUsingGET + if (operation === 'get') { + const contactId = parseInt(this.getNodeParameter('contactId', i) as string, 10); + const options = this.getNodeParameter('options', i) as IDataObject; + if (options.fields) { + qs.optional_properties = options.fields as string; + } + responseData = await keapApiRequest.call(this, 'GET', `/contacts/${contactId}`, {}, qs); + } + //https://developer.infusionsoft.com/docs/rest/#!/Contact/listContactsUsingGET + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const options = this.getNodeParameter('options', i) as IDataObject; + if (options.email) { + qs.email = options.email as boolean; + } + if (options.givenName) { + qs.given_name = options.givenName as string; + } + if (options.familyName) { + qs.family_name = options.familyName as boolean; + } + if (options.order) { + qs.order = options.order as string; + } + if (options.orderDirection) { + qs.order_direction = options.orderDirection as string; + } + if (options.since) { + qs.since = options.since as string; + } + if (options.until) { + qs.until = options.until as string; + } + if (returnAll) { + responseData = await keapApiRequestAllItems.call(this, 'contacts', 'GET', '/contacts', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await keapApiRequest.call(this, 'GET', '/contacts', {}, qs); + responseData = responseData.contacts; + } + } + } + if (resource === 'contactNote') { + //https://developer.infusionsoft.com/docs/rest/#!/Note/createNoteUsingPOST + if (operation === 'create') { + const userId = this.getNodeParameter('userId', i) as number; + const contactId = parseInt(this.getNodeParameter('contactId', i) as string, 10); + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: INote = { + user_id: userId, + contact_id: contactId, + }; + keysToSnakeCase(additionalFields); + if (additionalFields.type) { + additionalFields.type = pascalCase(additionalFields.type as string); + } + Object.assign(body, additionalFields); + responseData = await keapApiRequest.call(this, 'POST', '/notes', body); + } + //https://developer.infusionsoft.com/docs/rest/#!/Note/deleteNoteUsingDELETE + if (operation === 'delete') { + const noteId = this.getNodeParameter('noteId', i) as string; + responseData = await keapApiRequest.call(this, 'DELETE', `/notes/${noteId}`); + responseData = { success: true }; + } + //https://developer.infusionsoft.com/docs/rest/#!/Note/getNoteUsingGET + if (operation === 'get') { + const noteId = this.getNodeParameter('noteId', i) as string; + responseData = await keapApiRequest.call(this, 'GET', `/notes/${noteId}`); + } + //https://developer.infusionsoft.com/docs/rest/#!/Note/listNotesUsingGET + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + keysToSnakeCase(filters); + Object.assign(qs, filters); + if (returnAll) { + responseData = await keapApiRequestAllItems.call(this, 'notes', 'GET', '/notes', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await keapApiRequest.call(this, 'GET', '/notes', {}, qs); + responseData = responseData.notes; + } + } + //https://developer.infusionsoft.com/docs/rest/#!/Note/updatePropertiesOnNoteUsingPATCH + if (operation === 'update') { + const noteId = this.getNodeParameter('noteId', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: INote = {}; + keysToSnakeCase(additionalFields); + if (additionalFields.type) { + additionalFields.type = pascalCase(additionalFields.type as string); + } + Object.assign(body, additionalFields); + responseData = await keapApiRequest.call(this, 'PATCH', `/notes/${noteId}`, body); + } + } + if (resource === 'contactTag') { + //https://developer.infusionsoft.com/docs/rest/#!/Contact/applyTagsToContactIdUsingPOST + if (operation === 'create') { + const contactId = parseInt(this.getNodeParameter('contactId', i) as string, 10); + const tagIds = this.getNodeParameter('tagIds', i) as number[]; + const body: IDataObject = { + tagIds, + }; + responseData = await keapApiRequest.call(this, 'POST', `/contacts/${contactId}/tags`, body); + } + //https://developer.infusionsoft.com/docs/rest/#!/Contact/removeTagsFromContactUsingDELETE_1 + if (operation === 'delete') { + const contactId = parseInt(this.getNodeParameter('contactId', i) as string, 10); + const tagIds = this.getNodeParameter('tagIds', i) as string; + qs.ids = tagIds; + responseData = await keapApiRequest.call(this, 'DELETE', `/contacts/${contactId}/tags`, {}, qs); + responseData = { success: true }; + } + //https://developer.infusionsoft.com/docs/rest/#!/Contact/listAppliedTagsUsingGET + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const contactId = parseInt(this.getNodeParameter('contactId', i) as string, 10); + if (returnAll) { + responseData = await keapApiRequestAllItems.call(this, 'tags', 'GET', `/contacts/${contactId}/tags`, {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await keapApiRequest.call(this, 'GET', `/contacts/${contactId}/tags`, {}, qs); + responseData = responseData.tags; + } + } + } + if (resource === 'ecommerceOrder') { + //https://developer.infusionsoft.com/docs/rest/#!/E-Commerce/createOrderUsingPOST + if (operation === 'create') { + const contactId = parseInt(this.getNodeParameter('contactId', i) as string, 10); + const orderDate = this.getNodeParameter('orderDate', i) as string; + const orderTitle = this.getNodeParameter('orderTitle', i) as string; + const orderType = this.getNodeParameter('orderType', i) as string; + const orderItems = (this.getNodeParameter('orderItemsUi', i) as IDataObject).orderItemsValues as IDataObject[]; + const shippingAddress = (this.getNodeParameter('addressUi', i) as IDataObject).addressValues as IDataObject; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: IEcommerceOrder = { + contact_id: contactId, + order_date: orderDate, + order_title: orderTitle, + order_type: pascalCase(orderType), + }; + if (additionalFields.promoCodes) { + additionalFields.promoCodes = (additionalFields.promoCodes as string).split(',') as string[]; + } + keysToSnakeCase(additionalFields); + Object.assign(body, additionalFields); + body.order_items = keysToSnakeCase(orderItems) as IItem[]; + if (shippingAddress) { + body.shipping_address = keysToSnakeCase(shippingAddress)[0] as IShippingAddress; + } + responseData = await keapApiRequest.call(this, 'POST', '/orders', body); + } + //https://developer.infusionsoft.com/docs/rest/#!/E-Commerce/deleteOrderUsingDELETE + if (operation === 'delete') { + const orderId = parseInt(this.getNodeParameter('orderId', i) as string, 10); + responseData = await keapApiRequest.call(this, 'DELETE', `/orders/${orderId}`); + responseData = { success: true }; + } + //https://developer.infusionsoft.com/docs/rest/#!/E-Commerce/getOrderUsingGET + if (operation === 'get') { + const orderId = parseInt(this.getNodeParameter('orderId', i) as string, 10); + responseData = await keapApiRequest.call(this, 'get', `/orders/${orderId}`); + } + //https://developer.infusionsoft.com/docs/rest/#!/E-Commerce/listOrdersUsingGET + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const options = this.getNodeParameter('options', i) as IDataObject; + keysToSnakeCase(options); + Object.assign(qs, options); + if (returnAll) { + responseData = await keapApiRequestAllItems.call(this, 'orders', 'GET', '/orders', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await keapApiRequest.call(this, 'GET', '/orders', {}, qs); + responseData = responseData.orders; + } + } + } + if (resource === 'ecommerceProduct') { + //https://developer.infusionsoft.com/docs/rest/#!/Product/createProductUsingPOST + if (operation === 'create') { + const productName = this.getNodeParameter('productName', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: IEcommerceProduct = { + product_name: productName, + }; + keysToSnakeCase(additionalFields); + Object.assign(body, additionalFields); + responseData = await keapApiRequest.call(this, 'POST', '/products', body); + } + //https://developer.infusionsoft.com/docs/rest/#!/Product/deleteProductUsingDELETE + if (operation === 'delete') { + const productId = this.getNodeParameter('productId', i) as string; + responseData = await keapApiRequest.call(this, 'DELETE', `/products/${productId}`); + responseData = { success: true }; + } + //https://developer.infusionsoft.com/docs/rest/#!/Product/retrieveProductUsingGET + if (operation === 'get') { + const productId = this.getNodeParameter('productId', i) as string; + responseData = await keapApiRequest.call(this, 'get', `/products/${productId}`); + } + //https://developer.infusionsoft.com/docs/rest/#!/Product/listProductsUsingGET + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + keysToSnakeCase(filters); + Object.assign(qs, filters); + if (returnAll) { + responseData = await keapApiRequestAllItems.call(this, 'products', 'GET', '/products', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await keapApiRequest.call(this, 'GET', '/products', {}, qs); + responseData = responseData.products; + } + } + } + if (resource === 'email') { + //https://developer.infusionsoft.com/docs/rest/#!/Email/createEmailUsingPOST + if (operation === 'createRecord') { + const sentFromAddress = this.getNodeParameter('sentFromAddress', i) as string; + const sendToAddress = this.getNodeParameter('sentToAddress', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: IDataObject = { + sent_to_address: sendToAddress, + sent_from_address: sentFromAddress, + }; + Object.assign(body, additionalFields); + keysToSnakeCase(body as IDataObject); + responseData = await keapApiRequest.call(this, 'POST', '/emails', body); + } + //https://developer.infusionsoft.com/docs/rest/#!/Email/deleteEmailUsingDELETE + if (operation === 'deleteRecord') { + const emailRecordId = parseInt(this.getNodeParameter('emailRecordId', i) as string, 10); + responseData = await keapApiRequest.call(this, 'DELETE', `/emails/${emailRecordId}`); + responseData = { success: true }; + } + //https://developer.infusionsoft.com/docs/rest/#!/Email/listEmailsUsingGET + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + keysToSnakeCase(filters); + Object.assign(qs, filters); + if (returnAll) { + responseData = await keapApiRequestAllItems.call(this, 'emails', 'GET', '/emails', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await keapApiRequest.call(this, 'GET', '/emails', {}, qs); + responseData = responseData.emails; + } + } + //https://developer.infusionsoft.com/docs/rest/#!/Email/deleteEmailUsingDELETE + if (operation === 'send') { + const userId = this.getNodeParameter('userId', i) as number; + const contactIds = ((this.getNodeParameter('contactIds', i) as string).split(',') as string[]).map((e) => (parseInt(e, 10))); + const subject = this.getNodeParameter('subject', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: IEmail = { + user_id: userId, + contacts: contactIds, + subject, + }; + keysToSnakeCase(additionalFields); + Object.assign(body, additionalFields); + + const attachmentsUi = this.getNodeParameter('attachmentsUi', i) as IDataObject; + let attachments: IAttachment[] = []; + if (attachmentsUi) { + if (attachmentsUi.attachmentsValues) { + keysToSnakeCase(attachmentsUi.attachmentsValues as IDataObject); + attachments = attachmentsUi.attachmentsValues as IAttachment[]; + } + if (attachmentsUi.attachmentsBinary + && (attachmentsUi.attachmentsBinary as IDataObject).length) { + + if (items[i].binary === undefined) { + throw new Error('No binary data exists on item!'); + } + + for (const { property } of attachmentsUi.attachmentsBinary as IDataObject[]) { + + const item = items[i].binary as IBinaryKeyData; + + if (item[property as string] === undefined) { + throw new Error(`Binary data property "${property}" does not exists on item!`); + } + + attachments.push({ + file_data: item[property as string].data, + file_name: item[property as string].fileName, + }); + } + } + body.attachments = attachments; + } + + responseData = await keapApiRequest.call(this, 'POST', '/emails/queue', body); + responseData = { success: true }; + } + } + if (resource === 'file') { + //https://developer.infusionsoft.com/docs/rest/#!/File/deleteFileUsingDELETE + if (operation === 'delete') { + const fileId = parseInt(this.getNodeParameter('fileId', i) as string, 10); + responseData = await keapApiRequest.call(this, 'DELETE', `/files/${fileId}`); + responseData = { success: true }; + } + //https://developer.infusionsoft.com/docs/rest/#!/File/listFilesUsingGET + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + keysToSnakeCase(filters); + Object.assign(qs, filters); + if (qs.permission) { + qs.permission = (qs.permission as string).toUpperCase(); + } + if (qs.type) { + qs.type = titleCase(qs.type as string); + } + if (qs.viewable) { + qs.viewable = (qs.viewable as string).toUpperCase(); + } + if (returnAll) { + responseData = await keapApiRequestAllItems.call(this, 'files', 'GET', '/files', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await keapApiRequest.call(this, 'GET', '/files', {}, qs); + responseData = responseData.files; + } + } + //https://developer.infusionsoft.com/docs/rest/#!/File/createFileUsingPOST + if (operation === 'upload') { + const binaryData = this.getNodeParameter('binaryData', i) as boolean; + const fileAssociation = this.getNodeParameter('fileAssociation', i) as string; + const isPublic = this.getNodeParameter('isPublic', i) as boolean; + const body: IFile = { + is_public: isPublic, + file_association: fileAssociation.toUpperCase(), + }; + if (fileAssociation === 'contact') { + const contactId = parseInt(this.getNodeParameter('contactId', i) as string, 10); + body.contact_id = contactId; + } + if (binaryData) { + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string; + + if (items[i].binary === undefined) { + throw new Error('No binary data exists on item!'); + } + + const item = items[i].binary as IBinaryKeyData; + + if (item[binaryPropertyName as string] === undefined) { + throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`); + } + + body.file_data = item[binaryPropertyName as string].data; + body.file_name = item[binaryPropertyName as string].fileName; + + } else { + const fileName = this.getNodeParameter('fileName', i) as string; + const fileData = this.getNodeParameter('fileData', i) as string; + body.file_name = fileName; + body.file_data = fileData; + } + responseData = await keapApiRequest.call(this, 'POST', '/files', body); + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Keap/KeapTrigger.node.ts b/packages/nodes-base/nodes/Keap/KeapTrigger.node.ts new file mode 100644 index 000000000..d7bb614d6 --- /dev/null +++ b/packages/nodes-base/nodes/Keap/KeapTrigger.node.ts @@ -0,0 +1,196 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeTypeDescription, + INodeType, + IWebhookResponseData, + ILoadOptionsFunctions, + INodePropertyOptions, +} from 'n8n-workflow'; + +import { + keapApiRequest, +} from './GenericFunctions'; + +import { + titleCase, + } from 'change-case'; + +export class KeapTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Keap Trigger', + name: 'keapTrigger', + icon: 'file:keap.png', + group: ['trigger'], + version: 1, + subtitle: '={{$parameter["eventId"]}}', + description: 'Starts the workflow when Infusionsoft events occure.', + defaults: { + name: 'Keap Trigger', + color: '#79af53', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'keapOAuth2Api', + required: true, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Event', + name: 'eventId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getEvents', + }, + default: '', + required: true, + }, + { + displayName: 'RAW Data', + name: 'rawData', + type: 'boolean', + default: false, + description: `Returns the data exactly in the way it got received from the API.`, + }, + ], + }; + + methods = { + loadOptions: { + // Get all the event types to display them to user so that he can + // select them easily + async getEvents(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const hooks = await keapApiRequest.call(this, 'GET', '/hooks/event_keys'); + for (const hook of hooks) { + const hookName = hook; + const hookId = hook; + returnData.push({ + name: titleCase((hookName as string).replace('.', ' ')), + value: hookId as string, + }); + } + return returnData; + }, + }, + }; + + // @ts-ignore (because of request) + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const eventId = this.getNodeParameter('eventId') as string; + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + + const responseData = await keapApiRequest.call(this, 'GET', '/hooks', {}); + + for (const existingData of responseData) { + if (existingData.hookUrl === webhookUrl + && existingData.eventKey === eventId + && existingData.status === 'Verified') { + // The webhook exists already + webhookData.webhookId = existingData.key; + return true; + } + } + + return false; + }, + async create(this: IHookFunctions): Promise { + const eventId = this.getNodeParameter('eventId') as string; + const webhookData = this.getWorkflowStaticData('node'); + const webhookUrl = this.getNodeWebhookUrl('default'); + + const body = { + eventKey: eventId, + hookUrl: webhookUrl, + }; + + const responseData = await keapApiRequest.call(this, 'POST', '/hooks', body); + + if (responseData.key === undefined) { + // Required data is missing so was not successful + return false; + } + + webhookData.webhookId = responseData.key as string; + + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + + if (webhookData.webhookId !== undefined) { + + try { + await keapApiRequest.call(this, 'DELETE', `/hooks/${webhookData.webhookId}`); + } catch (e) { + return false; + } + + // Remove from the static workflow data so that it is clear + // that no webhooks are registred anymore + delete webhookData.webhookId; + } + + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const rawData = this.getNodeParameter('rawData') as boolean; + const headers = this.getHeaderData() as IDataObject; + const bodyData = this.getBodyData() as IDataObject; + + if (headers['x-hook-secret']) { + // Is a create webhook confirmation request + const res = this.getResponseObject(); + res.set('x-hook-secret', headers['x-hook-secret'] as string); + res.status(200).end(); + return { + noWebhookResponse: true, + }; + } + + if (rawData) { + return { + workflowData: [ + this.helpers.returnJsonArray(bodyData), + ], + }; + } + + const responseData: IDataObject[] = []; + for (const data of bodyData.object_keys as IDataObject[]) { + responseData.push({ + eventKey: bodyData.event_key, + objectType: bodyData.object_type, + id: data.id, + timestamp: data.timestamp, + apiUrl: data.apiUrl, + }); + } + return { + workflowData: [ + this.helpers.returnJsonArray(responseData), + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Keap/keap.png b/packages/nodes-base/nodes/Keap/keap.png new file mode 100644 index 000000000..4cf546680 Binary files /dev/null and b/packages/nodes-base/nodes/Keap/keap.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 2e6afca3d..dbf0ac449 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -70,6 +70,7 @@ "dist/credentials/JiraSoftwareCloudApi.credentials.js", "dist/credentials/JiraSoftwareServerApi.credentials.js", "dist/credentials/JotFormApi.credentials.js", + "dist/credentials/KeapOAuth2Api.credentials.js", "dist/credentials/LinkFishApi.credentials.js", "dist/credentials/MailchimpApi.credentials.js", "dist/credentials/MailgunApi.credentials.js", @@ -190,6 +191,8 @@ "dist/nodes/InvoiceNinja/InvoiceNinjaTrigger.node.js", "dist/nodes/Jira/JiraSoftwareCloud.node.js", "dist/nodes/JotForm/JotFormTrigger.node.js", + "dist/nodes/Keap/Keap.node.js", + "dist/nodes/Keap/KeapTrigger.node.js", "dist/nodes/LinkFish/LinkFish.node.js", "dist/nodes/Mailchimp/Mailchimp.node.js", "dist/nodes/Mailchimp/MailchimpTrigger.node.js",