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",