diff --git a/packages/nodes-base/credentials/ActiveCampaignApi.credentials.ts b/packages/nodes-base/credentials/ActiveCampaignApi.credentials.ts new file mode 100644 index 0000000000..cb47b5f6ed --- /dev/null +++ b/packages/nodes-base/credentials/ActiveCampaignApi.credentials.ts @@ -0,0 +1,24 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + + +export class ActiveCampaignApi implements ICredentialType { + name = 'activeCampaignApi'; + displayName = 'ActiveCampaign API'; + properties = [ + { + displayName: 'API URL', + name: 'apiUrl', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/ActiveCampaign/ActiveCampaign.node.ts b/packages/nodes-base/nodes/ActiveCampaign/ActiveCampaign.node.ts new file mode 100644 index 0000000000..f8a610463f --- /dev/null +++ b/packages/nodes-base/nodes/ActiveCampaign/ActiveCampaign.node.ts @@ -0,0 +1,491 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; +import { + IDataObject, + INodeTypeDescription, + INodeExecutionData, + INodeType, +} from 'n8n-workflow'; + +import { + activeCampaignApiRequest, + activeCampaignApiRequestAllItems, +} from './GenericFunctions'; + +interface CustomProperty { + name: string; + value: string; +} + + +/** + * Add the additional fields to the body + * + * @param {IDataObject} body The body object to add fields to + * @param {IDataObject} additionalFields The fields to add + */ +function addAdditionalFields(body: IDataObject, additionalFields: IDataObject) { + for (const key of Object.keys(additionalFields)) { + if (key === 'customProperties' && (additionalFields.customProperties as IDataObject).property !== undefined) { + for (const customProperty of (additionalFields.customProperties as IDataObject)!.property! as CustomProperty[]) { + body[customProperty.name] = customProperty.value; + } + } else { + body[key] = additionalFields[key]; + } + } +} + +export class ActiveCampaign implements INodeType { + description: INodeTypeDescription = { + displayName: 'ActiveCampaign', + name: 'activeCampaign', + icon: 'file:activeCampaign.png', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Create and edit data in ActiveCampaign', + defaults: { + name: 'ActiveCampaign', + color: '#356ae6', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'activeCampaignApi', + required: true, + } + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Contact', + value: 'contact', + }, + ], + default: 'contact', + description: 'The resource to operate on.', + }, + + + + // ---------------------------------- + // operations + // ---------------------------------- + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'contact', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a contact', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a contact', + }, + { + name: 'Get', + value: 'get', + description: 'Get data of a contact', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get data of all contact', + }, + { + name: 'Update', + value: 'update', + description: 'Update a contact', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, + + + + // ---------------------------------- + // contact + // ---------------------------------- + + // ---------------------------------- + // contact:create + // ---------------------------------- + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'contact', + ], + }, + }, + description: 'The email of the contact to create', + }, + { + displayName: 'Update if exists', + name: 'updateIfExists', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'contact', + ], + }, + }, + default: false, + description: 'Update user if it exists already. If not set and user exists it will error instead.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'contact', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: 'The first name of the contact to create', + }, + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + default: '', + description: 'The last name of the contact to create', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + description: 'Phone number of the contact.', + }, + ], + }, + + // ---------------------------------- + // contact:delete + // ---------------------------------- + { + displayName: 'Contact ID', + name: 'contactId', + type: 'number', + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'contact', + ], + }, + }, + default: 0, + required: true, + description: 'ID of the contact to delete.', + }, + + // ---------------------------------- + // person:get + // ---------------------------------- + { + displayName: 'Contact ID', + name: 'contactId', + type: 'number', + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'contact', + ], + }, + }, + default: 0, + required: true, + description: 'ID of the contact to get.', + }, + + // ---------------------------------- + // 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: 500, + }, + default: 100, + description: 'How many results to return.', + }, + + // ---------------------------------- + // contact:update + // ---------------------------------- + { + displayName: 'Contact ID', + name: 'contactId', + type: 'number', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'contact', + ], + }, + }, + default: 0, + required: true, + description: 'ID of the contact to update.', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + description: 'The fields to update.', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'contact', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'Email of the contact.', + }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: 'First name of the contact', + }, + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + default: '', + description: 'Last name of the contact', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + description: 'Phone number of the contact.', + }, + ], + }, + + ], + }; + + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + + let resource: string; + let operation: string; + + // For Post + let body: IDataObject; + // For Query string + let qs: IDataObject; + + let requestMethod: string; + let endpoint: string; + let returnAll = false; + let dataKey: string | undefined; + + for (let i = 0; i < items.length; i++) { + dataKey = undefined; + resource = this.getNodeParameter('resource', 0) as string; + operation = this.getNodeParameter('operation', 0) as string; + + requestMethod = 'GET'; + endpoint = ''; + body = {} as IDataObject; + qs = {} as IDataObject; + + if (resource === 'contact') { + if (operation === 'create') { + // ---------------------------------- + // contact:create + // ---------------------------------- + + requestMethod = 'POST'; + + const updateIfExists = this.getNodeParameter('updateIfExists', i) as boolean; + if (updateIfExists === true) { + endpoint = '/api/3/contact/sync'; + } else { + endpoint = '/api/3/contacts'; + } + + dataKey = 'contact'; + body.contact = { + email: this.getNodeParameter('email', i) as string, + } as IDataObject; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + addAdditionalFields(body.contact as IDataObject, additionalFields); + + } else if (operation === 'delete') { + // ---------------------------------- + // contact:delete + // ---------------------------------- + + requestMethod = 'DELETE'; + + const contactId = this.getNodeParameter('contactId', i) as number; + endpoint = `/api/3/contacts/${contactId}`; + + } else if (operation === 'get') { + // ---------------------------------- + // contact:get + // ---------------------------------- + + requestMethod = 'GET'; + + const contactId = this.getNodeParameter('contactId', i) as number; + endpoint = `/api/3/contacts/${contactId}`; + + } else if (operation === 'getAll') { + // ---------------------------------- + // persons:getAll + // ---------------------------------- + + requestMethod = 'GET'; + + returnAll = this.getNodeParameter('returnAll', i) as boolean; + if (returnAll === false) { + qs.limit = this.getNodeParameter('limit', i) as number; + } + + dataKey = 'contacts'; + endpoint = `/api/3/contacts`; + + } else if (operation === 'update') { + // ---------------------------------- + // contact:update + // ---------------------------------- + + requestMethod = 'PUT'; + + const contactId = this.getNodeParameter('contactId', i) as number; + endpoint = `/api/3/contacts/${contactId}`; + + dataKey = 'contact'; + body.contact = {} as IDataObject; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + addAdditionalFields(body.contact as IDataObject, updateFields); + + } + } else { + throw new Error(`The resource "${resource}" is not known!`); + } + + let responseData; + if (returnAll === true) { + responseData = await activeCampaignApiRequestAllItems.call(this, requestMethod, endpoint, body, qs, dataKey); + } else { + responseData = await activeCampaignApiRequest.call(this, requestMethod, endpoint, body, qs, dataKey); + } + + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else { + returnData.push(responseData as IDataObject); + } + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/ActiveCampaign/GenericFunctions.ts b/packages/nodes-base/nodes/ActiveCampaign/GenericFunctions.ts new file mode 100644 index 0000000000..169c9f1146 --- /dev/null +++ b/packages/nodes-base/nodes/ActiveCampaign/GenericFunctions.ts @@ -0,0 +1,116 @@ +import { + IExecuteFunctions, + IHookFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +import { OptionsWithUri } from 'request'; + + +/** + * Make an API request to ActiveCampaign + * + * @param {IHookFunctions} this + * @param {string} method + * @param {string} url + * @param {object} body + * @returns {Promise} + */ +export async function activeCampaignApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, body: IDataObject, query?: IDataObject, dataKey?: string): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('activeCampaignApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + if (query === undefined) { + query = {}; + } + + const options: OptionsWithUri = { + headers: { + 'Api-Token': credentials.apiKey, + }, + method, + qs: query, + uri: `${credentials.apiUrl}${endpoint}`, + json: true + }; + + if (Object.keys(body).length !== 0) { + options.body = body; + } + + try { + const responseData = await this.helpers.request(options); + + if (responseData.success === false) { + throw new Error(`ActiveCampaign error response: ${responseData.error} (${responseData.error_info})`); + } + + if (dataKey === undefined) { + return responseData; + } else { + return responseData[dataKey] as IDataObject; + } + + } catch (error) { + if (error.statusCode === 403) { + // Return a clear error + throw new Error('The ActiveCampaign credentials are not valid!'); + } + + // If that data does not exist for some reason return the actual error + throw error; + } +} + + + +/** + * Make an API request to paginated ActiveCampaign endpoint + * and return all results + * + * @export + * @param {(IHookFunctions | IExecuteFunctions)} this + * @param {string} method + * @param {string} endpoint + * @param {IDataObject} body + * @param {IDataObject} [query] + * @returns {Promise} + */ +export async function activeCampaignApiRequestAllItems(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, body: IDataObject, query?: IDataObject, dataKey?: string): Promise { // tslint:disable-line:no-any + + if (query === undefined) { + query = {}; + } + query.limit = 100; + query.offset = 0; + + const returnData: IDataObject[] = []; + + let responseData; + + let itemsReceived = 0; + do { + responseData = await activeCampaignApiRequest.call(this, method, endpoint, body, query); + + if (dataKey === undefined) { + returnData.push.apply(returnData, responseData); + itemsReceived += returnData.length; + } else { + returnData.push.apply(returnData, responseData[dataKey]); + itemsReceived += responseData[dataKey].length; + } + + query.offset = itemsReceived; + } while ( + responseData.meta !== undefined && + responseData.meta.total !== undefined && + responseData.meta.total > itemsReceived + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/ActiveCampaign/activeCampaign.png b/packages/nodes-base/nodes/ActiveCampaign/activeCampaign.png new file mode 100644 index 0000000000..4a40c91583 Binary files /dev/null and b/packages/nodes-base/nodes/ActiveCampaign/activeCampaign.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 92a3e0198d..a57403a323 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -25,6 +25,7 @@ ], "n8n": { "credentials": [ + "dist/credentials/ActiveCampaignApi.credentials.js", "dist/credentials/AirtableApi.credentials.js", "dist/credentials/AsanaApi.credentials.js", "dist/credentials/ChargebeeApi.credentials.js", @@ -46,6 +47,7 @@ "dist/credentials/TwilioApi.credentials.js" ], "nodes": [ + "dist/nodes/ActiveCampaign/ActiveCampaign.node.js", "dist/nodes/Airtable/Airtable.node.js", "dist/nodes/Asana/Asana.node.js", "dist/nodes/Asana/AsanaTrigger.node.js",