From 89ed3c4a6d2e06caa3fe255d0a7bd1a8cfbf8610 Mon Sep 17 00:00:00 2001 From: Rupenieks <32895755+Rupenieks@users.noreply.github.com> Date: Wed, 2 Sep 2020 10:50:14 +0200 Subject: [PATCH 1/6] :sparkles: Recursive listing for FTP/SFTP (#903) * :zap: Add allowUnauthorizedCerts to Postgres-Node * :zap: Added recursive directory listing for SFTP * :zap: Added recursive listing for FTP * Removed unused imports * :zap: Fixed creating an instance of both ftp/sftp both regardless of which is used Co-authored-by: Jan Oberhauser --- packages/nodes-base/nodes/Ftp.node.ts | 79 ++++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 8 deletions(-) diff --git a/packages/nodes-base/nodes/Ftp.node.ts b/packages/nodes-base/nodes/Ftp.node.ts index 49f828f2ed..84106640ac 100644 --- a/packages/nodes-base/nodes/Ftp.node.ts +++ b/packages/nodes-base/nodes/Ftp.node.ts @@ -220,6 +220,21 @@ export class Ftp implements INodeType { description: 'Path of directory to list contents of.', required: true, }, + { + displayName: 'Recursive', + displayOptions: { + show: { + operation: [ + 'list', + ], + }, + }, + name: 'recursive', + type: 'boolean', + default: false, + description: 'Return object representing all directories / objects recursively found within SFTP server', + required: true, + }, ], }; @@ -234,6 +249,7 @@ export class Ftp implements INodeType { let credentials: ICredentialDataDecryptedObject | undefined = undefined; const protocol = this.getNodeParameter('protocol', 0) as string; + if (protocol === 'sftp') { credentials = this.getCredentials('sftp'); } else { @@ -244,11 +260,11 @@ export class Ftp implements INodeType { throw new Error('Failed to get credentials!'); } - let ftp: ftpClient; - let sftp: sftpClient; + let ftp : ftpClient; + let sftp : sftpClient; + if (protocol === 'sftp') { sftp = new sftpClient(); - await sftp.connect({ host: credentials.host as string, port: credentials.port as number, @@ -258,7 +274,6 @@ export class Ftp implements INodeType { } else { ftp = new ftpClient(); - await ftp.connect({ host: credentials.host as string, port: credentials.port as number, @@ -286,8 +301,15 @@ export class Ftp implements INodeType { const path = this.getNodeParameter('path', i) as string; if (operation === 'list') { - responseData = await sftp!.list(path); - returnItems.push.apply(returnItems, this.helpers.returnJsonArray(responseData as unknown as IDataObject[])); + const recursive = this.getNodeParameter('recursive', i) as boolean; + + if (recursive) { + responseData = await callRecursiveList(path, sftp); + returnItems.push.apply(returnItems, this.helpers.returnJsonArray(responseData as unknown as IDataObject[])); + } else { + responseData = await sftp!.list(path); + returnItems.push.apply(returnItems, this.helpers.returnJsonArray(responseData as unknown as IDataObject[])); + } } if (operation === 'download') { @@ -347,8 +369,15 @@ export class Ftp implements INodeType { const path = this.getNodeParameter('path', i) as string; if (operation === 'list') { - responseData = await ftp!.list(path); - returnItems.push.apply(returnItems, this.helpers.returnJsonArray(responseData as unknown as IDataObject[])); + const recursive = this.getNodeParameter('recursive', i) as boolean; + + if (recursive) { + responseData = await callRecursiveList(path, ftp); + returnItems.push.apply(returnItems, this.helpers.returnJsonArray(responseData as unknown as IDataObject[])); + } else { + responseData = await ftp!.list(path); + returnItems.push.apply(returnItems, this.helpers.returnJsonArray(responseData as unknown as IDataObject[])); + } } if (operation === 'download') { @@ -432,3 +461,37 @@ export class Ftp implements INodeType { return [returnItems]; } } +async function callRecursiveList(path: string, client : sftpClient | ftpClient) { + const pathArray : string[] = [path]; + let currentPath = path; + const directoryItems : sftpClient.FileInfo[] = []; + let index = 0; + + do { + // tslint:disable-next-line: array-type + const returnData : sftpClient.FileInfo[] | (string | ftpClient.ListingElement)[] = await client.list(pathArray[index]); + + // @ts-ignore + returnData.map((item : sftpClient.FileInfo) => { + if ((pathArray[index] as string).endsWith('/')) { + currentPath = `${pathArray[index]}${item.name}`; + } else { + currentPath = `${pathArray[index]}/${item.name}`; + } + + // Is directory + if (item.type === 'd') { + pathArray.push(currentPath); + } + + //@ts-ignore + item.path = currentPath; + directoryItems.push(item); + }); + index++; + + } while (index <= pathArray.length - 1); + + + return directoryItems; +} From 0e1a4e53094bdffaa62358c09524a394b3342723 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 2 Sep 2020 10:50:55 +0200 Subject: [PATCH 2/6] :zap: Normalize FTP-Data --- packages/nodes-base/nodes/Ftp.node.ts | 47 ++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/packages/nodes-base/nodes/Ftp.node.ts b/packages/nodes-base/nodes/Ftp.node.ts index 84106640ac..ae44dbc871 100644 --- a/packages/nodes-base/nodes/Ftp.node.ts +++ b/packages/nodes-base/nodes/Ftp.node.ts @@ -17,6 +17,24 @@ import { import * as ftpClient from 'promise-ftp'; import * as sftpClient from 'ssh2-sftp-client'; +interface ReturnFtpItem { + type: string; + name: string; + size: number; + accessTime: Date; + modifyTime: Date; + rights: { + user: string; + group: string; + other: string; + }; + owner: string | number; + group: string | number; + target: string; + sticky?: boolean; + path: string; +} + export class Ftp implements INodeType { description: INodeTypeDescription = { displayName: 'FTP', @@ -304,10 +322,11 @@ export class Ftp implements INodeType { const recursive = this.getNodeParameter('recursive', i) as boolean; if (recursive) { - responseData = await callRecursiveList(path, sftp); + responseData = await callRecursiveList(path, sftp!, normalizeSFtpItem); returnItems.push.apply(returnItems, this.helpers.returnJsonArray(responseData as unknown as IDataObject[])); } else { responseData = await sftp!.list(path); + responseData.forEach(item => normalizeSFtpItem(item as sftpClient.FileInfo, path)); returnItems.push.apply(returnItems, this.helpers.returnJsonArray(responseData as unknown as IDataObject[])); } } @@ -372,10 +391,11 @@ export class Ftp implements INodeType { const recursive = this.getNodeParameter('recursive', i) as boolean; if (recursive) { - responseData = await callRecursiveList(path, ftp); + responseData = await callRecursiveList(path, ftp!, normalizeFtpItem); returnItems.push.apply(returnItems, this.helpers.returnJsonArray(responseData as unknown as IDataObject[])); } else { responseData = await ftp!.list(path); + responseData.forEach(item => normalizeFtpItem(item as ftpClient.ListingElement, path)); returnItems.push.apply(returnItems, this.helpers.returnJsonArray(responseData as unknown as IDataObject[])); } } @@ -461,7 +481,25 @@ export class Ftp implements INodeType { return [returnItems]; } } -async function callRecursiveList(path: string, client : sftpClient | ftpClient) { + + +function normalizeFtpItem(input: ftpClient.ListingElement, path: string) { + const item = input as unknown as ReturnFtpItem; + item.modifyTime = input.date; + item.path = `${path}${path.endsWith('/') ? '' : '/'}${item.name}`; + // @ts-ignore + item.date = undefined; +} + + +function normalizeSFtpItem(input: sftpClient.FileInfo, path: string) { + const item = input as unknown as ReturnFtpItem; + item.accessTime = new Date(input.accessTime); + item.modifyTime = new Date(input.modifyTime); + item.path = `${path}${path.endsWith('/') ? '' : '/'}${item.name}`; +} + +async function callRecursiveList(path: string, client: sftpClient | ftpClient, normalizeFunction: (input: ftpClient.ListingElement & sftpClient.FileInfo, path: string) => void) { const pathArray : string[] = [path]; let currentPath = path; const directoryItems : sftpClient.FileInfo[] = []; @@ -484,8 +522,7 @@ async function callRecursiveList(path: string, client : sftpClient | ftpClient) pathArray.push(currentPath); } - //@ts-ignore - item.path = currentPath; + normalizeFunction(item as ftpClient.ListingElement & sftpClient.FileInfo, currentPath); directoryItems.push(item); }); index++; From e5a5e1ed11a37022558b3553c9b2f8926c7e7433 Mon Sep 17 00:00:00 2001 From: Rupenieks <32895755+Rupenieks@users.noreply.github.com> Date: Wed, 2 Sep 2020 12:25:11 +0200 Subject: [PATCH 3/6] :sparkles: Add Customer.io-Node (#833) * :construction: Descriptions, node function, generic function changes * :white_check_mark: Finished functionality, added icon * :zap: Added new campaign operations, acommodated for 2 different authentications * :zap: Fixed number defaults not being numbers but empty strings --- .../credentials/CustomerIoApi.credentials.ts | 21 +- .../nodes/CustomerIo/CampaignDescription.ts | 199 ++++++++++ .../nodes/CustomerIo/CustomerDescription.ts | 326 ++++++++++++++++ .../nodes/CustomerIo/CustomerIo.node.ts | 359 ++++++++++++++++++ .../CustomerIo/CustomerIoTrigger.node.ts | 10 +- .../nodes/CustomerIo/EventDescription.ts | 296 +++++++++++++++ .../nodes/CustomerIo/GenericFunctions.ts | 30 +- .../nodes/CustomerIo/SegmentDescription.ts | 71 ++++ .../nodes/CustomerIo/customerio.png | Bin 0 -> 2662 bytes packages/nodes-base/package.json | 1 + 10 files changed, 1301 insertions(+), 12 deletions(-) create mode 100644 packages/nodes-base/nodes/CustomerIo/CampaignDescription.ts create mode 100644 packages/nodes-base/nodes/CustomerIo/CustomerDescription.ts create mode 100644 packages/nodes-base/nodes/CustomerIo/CustomerIo.node.ts create mode 100644 packages/nodes-base/nodes/CustomerIo/EventDescription.ts create mode 100644 packages/nodes-base/nodes/CustomerIo/SegmentDescription.ts create mode 100644 packages/nodes-base/nodes/CustomerIo/customerio.png diff --git a/packages/nodes-base/credentials/CustomerIoApi.credentials.ts b/packages/nodes-base/credentials/CustomerIoApi.credentials.ts index 44cfcefa1a..78a29dd2dc 100644 --- a/packages/nodes-base/credentials/CustomerIoApi.credentials.ts +++ b/packages/nodes-base/credentials/CustomerIoApi.credentials.ts @@ -10,11 +10,26 @@ export class CustomerIoApi implements ICredentialType { documentationUrl = 'customerIo'; properties = [ { - displayName: 'App API Key', - name: 'apiKey', + displayName: 'Tracking API Key', + name: 'trackingApiKey', type: 'string' as NodePropertyTypes, default: '', + description: 'Required for tracking API.', + required: true + }, + { + displayName: 'Tracking Site ID', + name: 'trackingSiteId', + type: 'string' as NodePropertyTypes, + default: '', + description: 'Required for tracking API.' + }, + { + displayName: 'App API Key', + name: 'appApiKey', + type: 'string' as NodePropertyTypes, + default: '', + description: 'Required for App API.' }, - ]; } diff --git a/packages/nodes-base/nodes/CustomerIo/CampaignDescription.ts b/packages/nodes-base/nodes/CustomerIo/CampaignDescription.ts new file mode 100644 index 0000000000..91cf22f737 --- /dev/null +++ b/packages/nodes-base/nodes/CustomerIo/CampaignDescription.ts @@ -0,0 +1,199 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const campaignOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'campaign', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Get Metrics', + value: 'getMetrics', + }, + ], + default: 'get', + description: 'The operation to perform', + }, +] as INodeProperties[]; + +export const campaignFields = [ +/* -------------------------------------------------------------------------- */ +/* campaign:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Campaign ID', + name: 'campaignId', + type: 'number', + required: true, + default: 0, + displayOptions: { + show: { + resource: [ + 'campaign', + ], + operation: [ + 'get' + ] + }, + }, + description: 'The unique identifier for the campaign', + }, +/* -------------------------------------------------------------------------- */ +/* campaign:getMetrics */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Campaign ID', + name: 'campaignId', + type: 'number', + required: true, + default: 0, + displayOptions: { + show: { + resource: [ + 'campaign', + ], + operation: [ + 'getMetrics' + ] + }, + }, + description: 'The unique identifier for the campaign', + }, + { + displayName: 'Period', + name: 'period', + type: 'options', + default: 'days', + displayOptions: { + show: { + resource: [ + 'campaign', + ], + operation: [ + 'getMetrics' + ] + }, + }, + description: 'Specify metric period', + options: [ + { + name: 'Hours', + value: 'hours' + }, + { + name: 'Days', + value: 'days' + }, + { + name: 'Weeks', + value: 'weeks' + }, + { + name: 'Months', + value: 'months' + }, + ] + }, + { + displayName: 'JSON Parameters', + name: 'jsonParameters', + type: 'boolean', + default: false, + description: '', + displayOptions: { + show: { + resource: [ + 'campaign', + ], + operation: [ + 'getMetrics' + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'campaign', + ], + operation: [ + 'getMetrics' + ], + jsonParameters: [ + false, + ], + }, + }, + options: [ + { + displayName: 'Steps', + name: 'steps', + type: 'number', + default: 0, + description: 'Integer specifying how many steps to return. Defaults to the maximum number of timeperiods available, or 12 when using the months period. Maximum timeperiods available are 24 hours, 45 days, 12 weeks and 120 months', + typeOptions: { + minValue: 0, + maxValue: 120 + } + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + default: 'empty', + description: 'Specify metric type', + options: [ + { + name: 'Empty', + value: 'empty' + }, + { + name: 'Email', + value: 'email' + }, + { + name: 'Webhook', + value: 'webhook' + }, + { + name: 'twilio', + value: 'twilio' + }, + { + name: 'Urban Airship', + value: 'urbanAirship' + }, + { + name: 'Slack', + value: 'slack' + }, + { + name: 'Push', + value: 'push' + }, + ] + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/CustomerIo/CustomerDescription.ts b/packages/nodes-base/nodes/CustomerIo/CustomerDescription.ts new file mode 100644 index 0000000000..47d030997f --- /dev/null +++ b/packages/nodes-base/nodes/CustomerIo/CustomerDescription.ts @@ -0,0 +1,326 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const customerOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'customer', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a customer.', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a customer.', + }, + { + name: 'Update', + value: 'update', + description: 'Update a customer.', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const customerFields = [ + +/* -------------------------------------------------------------------------- */ +/* customer:create/delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'ID', + name: 'id', + type: 'number', + required: true, + default: 0, + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'create', 'delete' + ] + }, + }, + description: 'The unique identifier for the customer.', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'create' + ] + }, + }, + description: 'The email address of the user.', + }, + { + displayName: 'Created at', + name: 'createdAt', + type: 'dateTime', + default: '', + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'create' + ] + }, + }, + description: 'The UNIX timestamp from when the user was created.', + }, + { + displayName: 'JSON Parameters', + name: 'jsonParameters', + type: 'boolean', + default: false, + description: '', + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'create' + ], + }, + }, + }, + { + displayName: ' Additional Fields', + name: 'additionalFieldsJson', + type: 'json', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'create' + ], + jsonParameters: [ + true, + ], + }, + }, + description: 'Object of values to set as described here.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'create' + ], + jsonParameters: [ + false, + ], + }, + }, + options: [ + { + displayName: 'Custom Properties', + name: 'customProperties', + type: 'fixedCollection', + description: 'Custom Properties', + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Property', + name: 'customProperty', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + required: true, + default: '', + description: 'Property name.', + placeholder: 'Plan' + }, + + { + displayName: 'Value', + name: 'value', + type: 'string', + required: true, + default: '', + description: 'Property value.', + placeholder: 'Basic' + }, + ], + }, + ] + }, + ], + }, + +/* -------------------------------------------------------------------------- */ +/* customer:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'ID', + name: 'id', + type: 'number', + required: true, + default: 0, + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'update' + ] + }, + }, + description: 'The unique identifier for the customer.', + }, + { + displayName: 'JSON Parameters', + name: 'jsonParameters', + type: 'boolean', + default: false, + description: '', + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'update' + ], + }, + }, + }, + { + displayName: ' Additional Fields', + name: 'additionalFieldsJson', + type: 'json', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'update' + ], + jsonParameters: [ + true, + ], + }, + }, + description: 'Object of values to set as described here.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'update' + ], + jsonParameters: [ + false, + ], + }, + }, + options: [ + { + displayName: 'Custom Properties', + name: 'customProperties', + type: 'fixedCollection', + description: 'Custom Properties', + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Property', + name: 'customProperty', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + required: true, + default: '', + description: 'Property name.', + placeholder: 'Plan' + }, + + { + displayName: 'Value', + name: 'value', + type: 'string', + required: true, + default: '', + description: 'Property value.', + placeholder: 'Basic' + }, + ], + }, + ] + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'The email address of the user.', + }, + { + displayName: 'Created at', + name: 'createdAt', + type: 'dateTime', + default: '', + description: 'The UNIX timestamp from when the user was created.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/CustomerIo/CustomerIo.node.ts b/packages/nodes-base/nodes/CustomerIo/CustomerIo.node.ts new file mode 100644 index 0000000000..f3bd6364c9 --- /dev/null +++ b/packages/nodes-base/nodes/CustomerIo/CustomerIo.node.ts @@ -0,0 +1,359 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; +import { + IDataObject, + INodeTypeDescription, + INodeExecutionData, + INodeType, + ILoadOptionsFunctions, + INodePropertyOptions, +} from 'n8n-workflow'; +import { customerIoApiRequest, validateJSON } from './GenericFunctions'; +import { campaignOperations, campaignFields } from './CampaignDescription'; +import { customerOperations, customerFields } from './CustomerDescription'; +import { eventOperations, eventFields } from './EventDescription'; +import { segmentOperations, segmentFields } from './SegmentDescription'; +import { DateTime } from '../DateTime.node'; + + +export class CustomerIo implements INodeType { + description: INodeTypeDescription = { + displayName: 'Customer.io', + name: 'customerio', + icon: 'file:customerio.png', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Customer.io API', + defaults: { + name: 'CustomerIo', + color: '#ffcd00', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'customerIoApi', + required: true, + } + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Customer', + value: 'customer', + }, + { + name: 'Event', + value: 'event', + }, + { + name: 'Campaign', + value: 'campaign', + }, + { + name: 'Segment', + value: 'segment', + }, + ], + default: 'customer', + description: 'Resource to consume.', + }, + // CAMPAIGN + ...campaignOperations, + ...campaignFields, + // CUSTOMER + ...customerOperations, + ...customerFields, + // EVENT + ...eventOperations, + ...eventFields, + // SEGMENT + ...segmentOperations, + ...segmentFields + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const returnData: IDataObject[] = []; + const items = this.getInputData(); + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + const body : IDataObject = {}; + + for (let i = 0; i < items.length; i++) { + + if (resource === 'campaign') { + if (operation === 'get') { + const campaignId = this.getNodeParameter('campaignId', i) as number; + const endpoint = `/campaigns/${campaignId}`; + + responseData = await customerIoApiRequest.call(this, 'GET', endpoint, body, 'beta'); + } + + if (operation === 'getAll') { + const endpoint = `/campaigns`; + + responseData = await customerIoApiRequest.call(this, 'GET', endpoint, body, 'beta'); + responseData = responseData.campaigns; + } + + if (operation === 'getMetrics') { + const campaignId = this.getNodeParameter('campaignId', i) as number; + const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; + + if (jsonParameters) { + const additionalFieldsJson = this.getNodeParameter('additionalFieldsJson', i) as string; + + if (additionalFieldsJson !== '') { + + if (validateJSON(additionalFieldsJson) !== undefined) { + + Object.assign(body, JSON.parse(additionalFieldsJson)); + + } else { + throw new Error('Additional fields must be a valid JSON'); + } + } + } else { + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const period = this.getNodeParameter('period', i) as string; + let endpoint = `/campaigns/${campaignId}/metrics`; + + if (period !== 'days') { + endpoint = `${endpoint}?period=${period}`; + } + if (additionalFields.steps) { + body.steps = additionalFields.steps as number; + } + if (additionalFields.type) { + if (additionalFields.type === 'urbanAirship') { + additionalFields.type = 'urban_airship'; + } else { + body.type = additionalFields.type as string; + } + } + + responseData = await customerIoApiRequest.call(this, 'GET', endpoint, body, 'beta'); + responseData = responseData.metric; + } + } + } + + if (resource === 'customer') { + if (operation === 'create') { + const id = this.getNodeParameter('id', i) as number; + const email = this.getNodeParameter('email', i) as string; + const createdAt = this.getNodeParameter('createdAt', i) as string; + const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; + + body.email = email; + body.created_at = new Date(createdAt).getTime() / 1000; + + if (jsonParameters) { + const additionalFieldsJson = this.getNodeParameter('additionalFieldsJson', i) as string; + + if (additionalFieldsJson !== '') { + + if (validateJSON(additionalFieldsJson) !== undefined) { + + Object.assign(body, JSON.parse(additionalFieldsJson)); + + } else { + throw new Error('Additional fields must be a valid JSON'); + } + } + } else { + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (additionalFields.customProperties) { + const data : any = {}; + //@ts-ignore + additionalFields.customProperties.customProperty.map(property => { + data[property.key] = property.value; + }); + + body.data = data; + } + } + + const endpoint = `/customers/${id}`; + + responseData = await customerIoApiRequest.call(this, 'PUT', endpoint, body, 'tracking'); + } + + if (operation === 'update') { + const id = this.getNodeParameter('id', i) as number; + const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; + + if (jsonParameters) { + const additionalFieldsJson = this.getNodeParameter('additionalFieldsJson', i) as string; + + if (additionalFieldsJson !== '') { + + if (validateJSON(additionalFieldsJson) !== undefined) { + + Object.assign(body, JSON.parse(additionalFieldsJson)); + + } else { + throw new Error('Additional fields must be a valid JSON'); + } + } + } else { + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (additionalFields.customProperties) { + const data : any = {}; + //@ts-ignore + additionalFields.customProperties.customProperty.map(property => { + data[property.key] = property.value; + }); + + body.data = data; + } + + if (additionalFields.email) { + body.email = additionalFields.email as string; + } + + if (additionalFields.createdAt) { + body.created_at = new Date(additionalFields.createdAt as string).getTime() / 1000; + } + } + + const endpoint = `/customers/${id}`; + + responseData = await customerIoApiRequest.call(this, 'PUT', endpoint, body, 'tracking'); + } + + if (operation === 'delete') { + const id = this.getNodeParameter('id', i) as number; + + body.id = id; + + const endpoint = `/customers/${id}`; + + responseData = await customerIoApiRequest.call(this, 'DELETE', endpoint, body, 'tracking'); + } + } + + if (resource === 'event') { + if (operation === 'track') { + const id = this.getNodeParameter('id', i) as number; + const name = this.getNodeParameter('name', i) as string; + const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; + + body.name = name; + + if (jsonParameters) { + const additionalFieldsJson = this.getNodeParameter('additionalFieldsJson', i) as string; + + if (additionalFieldsJson !== '') { + + if (validateJSON(additionalFieldsJson) !== undefined) { + + Object.assign(body, JSON.parse(additionalFieldsJson)); + + } else { + throw new Error('Additional fields must be a valid JSON'); + } + } + } else { + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const data : any = {}; + + if (additionalFields.customAttributes) { + //@ts-ignore + additionalFields.customAttributes.customAttribute.map(property => { + data[property.key] = property.value; + }); + } + + if (additionalFields.type) { + data.type = additionalFields.type as string; + } + + body.data = data; + } + + const endpoint = `/customers/${id}/events`; + + responseData = await customerIoApiRequest.call(this, 'POST', endpoint, body, 'tracking'); + } + + if (operation === 'trackAnonymous') { + const name = this.getNodeParameter('name', i) as string; + const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; + + body.name = name; + + if (jsonParameters) { + const additionalFieldsJson = this.getNodeParameter('additionalFieldsJson', i) as string; + + if (additionalFieldsJson !== '') { + + if (validateJSON(additionalFieldsJson) !== undefined) { + + Object.assign(body, JSON.parse(additionalFieldsJson)); + + } else { + throw new Error('Additional fields must be a valid JSON'); + } + } + } else { + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const data : any = {}; + + if (additionalFields.customAttributes) { + //@ts-ignore + additionalFields.customAttributes.customAttribute.map(property => { + data[property.key] = property.value; + }); + } + body.data = data; + } + + const endpoint = `/events`; + responseData = await customerIoApiRequest.call(this, 'POST', endpoint, body, 'tracking'); + } + } + + if (resource === 'segment') { + const id = this.getNodeParameter('id', i) as number; + const ids = this.getNodeParameter('ids', i) as string; + const idArray : string[] = []; + + ids.split(',').map(id => { + idArray.push(id); + }); + + body.id = id; + body.ids = idArray; + + let endpoint = ``; + + if (operation === 'add') { + endpoint = `/segments/${id}/add_customers`; + } else { + endpoint = `/segments/${id}/remove_customers`; + } + + responseData = await customerIoApiRequest.call(this, 'POST', endpoint, body, 'tracking'); + } + + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else { + returnData.push(responseData as unknown as IDataObject); + } + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/CustomerIo/CustomerIoTrigger.node.ts b/packages/nodes-base/nodes/CustomerIo/CustomerIoTrigger.node.ts index 875e77bd3e..08990d17b8 100644 --- a/packages/nodes-base/nodes/CustomerIo/CustomerIoTrigger.node.ts +++ b/packages/nodes-base/nodes/CustomerIo/CustomerIoTrigger.node.ts @@ -11,7 +11,7 @@ import { } from 'n8n-workflow'; import { - apiRequest, + customerIoApiRequest, eventExists, } from './GenericFunctions'; @@ -34,7 +34,7 @@ export class CustomerIoTrigger implements INodeType { description: 'Starts the workflow on a Customer.io update. (Beta)', defaults: { name: 'Customer.io Trigger', - color: '#7131ff', + color: '#ffcd00', }, inputs: [], outputs: ['main'], @@ -237,7 +237,7 @@ export class CustomerIoTrigger implements INodeType { const endpoint = '/reporting_webhooks'; - let { reporting_webhooks: webhooks } = await apiRequest.call(this, 'GET', endpoint, {}); + let { reporting_webhooks: webhooks } = await customerIoApiRequest.call(this, 'GET', endpoint, {}, 'beta'); if (webhooks === null) { webhooks = []; @@ -295,7 +295,7 @@ export class CustomerIoTrigger implements INodeType { events: data, }; - webhook = await apiRequest.call(this, 'POST', endpoint, body); + webhook = await customerIoApiRequest.call(this, 'POST', endpoint, body, 'beta'); const webhookData = this.getWorkflowStaticData('node'); webhookData.webhookId = webhook.id as string; @@ -307,7 +307,7 @@ export class CustomerIoTrigger implements INodeType { if (webhookData.webhookId !== undefined) { const endpoint = `/reporting_webhooks/${webhookData.webhookId}`; try { - await apiRequest.call(this, 'DELETE', endpoint, {}); + await customerIoApiRequest.call(this, 'DELETE', endpoint, {}, 'beta'); } catch (e) { return false; } diff --git a/packages/nodes-base/nodes/CustomerIo/EventDescription.ts b/packages/nodes-base/nodes/CustomerIo/EventDescription.ts new file mode 100644 index 0000000000..7b57004a9e --- /dev/null +++ b/packages/nodes-base/nodes/CustomerIo/EventDescription.ts @@ -0,0 +1,296 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const eventOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'event', + ], + }, + }, + options: [ + { + name: 'Track', + value: 'track', + description: 'Track a customer event.', + }, + { + name: 'Track Anonymous', + value: 'trackAnonymous', + description: 'Track an anonymous event.', + }, + ], + default: 'track', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const eventFields = [ + +/* -------------------------------------------------------------------------- */ +/* event:track */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'ID', + name: 'id', + type: 'number', + required: true, + default: 0, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'track' + ] + }, + }, + description: 'The unique identifier for the customer.', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'track' + ] + }, + }, + description: 'Name of the event to track.', + }, + { + displayName: 'JSON Parameters', + name: 'jsonParameters', + type: 'boolean', + default: false, + description: '', + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'track' + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFieldsJson', + type: 'json', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'track' + ], + jsonParameters: [ + true, + ], + }, + }, + description: 'Object of values to set as described here.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'track' + ], + jsonParameters: [ + false + ] + }, + }, + options: [ + { + displayName: 'Custom Attributes', + name: 'customAttributes', + type: 'fixedCollection', + description: 'Custom Properties', + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Attribute', + name: 'customAttribute', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + required: true, + default: '', + description: 'Attribute name.', + placeholder: 'Price' + }, + + { + displayName: 'Value', + name: 'value', + type: 'string', + required: true, + default: '', + description: 'Attribute value.', + placeholder: '25.50' + }, + ], + }, + ] + }, + { + displayName: 'Type', + name: 'type', + type: 'string', + default: '', + description: 'Used to change event type. For Page View events set to "page".', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* event:track anonymous */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Name', + name: 'name', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'trackAnonymous' + ] + }, + }, + description: 'The unique identifier for the customer.', + }, + { + displayName: 'JSON Parameters', + name: 'jsonParameters', + type: 'boolean', + default: false, + description: '', + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'trackAnonymous' + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFieldsJson', + type: 'json', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'trackAnonymous' + ], + jsonParameters: [ + true, + ], + }, + }, + description: 'Object of values to set as described here.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'trackAnonymous' + ], + jsonParameters: [ + false + ] + }, + }, + options: [ + { + displayName: 'Custom Attributes', + name: 'customAttributes', + type: 'fixedCollection', + description: 'Custom Properties', + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Attribute', + name: 'customAttribute', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + required: true, + default: '', + description: 'Attribute name.', + placeholder: 'Price' + }, + + { + displayName: 'Value', + name: 'value', + type: 'string', + required: true, + default: '', + description: 'Attribute value.', + placeholder: '25.50' + }, + ], + }, + ] + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/CustomerIo/GenericFunctions.ts b/packages/nodes-base/nodes/CustomerIo/GenericFunctions.ts index ba6a388acb..9cea18a4c9 100644 --- a/packages/nodes-base/nodes/CustomerIo/GenericFunctions.ts +++ b/packages/nodes-base/nodes/CustomerIo/GenericFunctions.ts @@ -16,7 +16,7 @@ import { get, } from 'lodash'; -export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: object, query?: IDataObject): Promise { // tslint:disable-line:no-any +export async function customerIoApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: object, baseApi? : string, query?: IDataObject): Promise { // tslint:disable-line:no-any const credentials = this.getCredentials('customerIoApi'); if (credentials === undefined) { @@ -28,14 +28,26 @@ export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoa const options: OptionsWithUri = { headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${credentials.apiKey}`, }, method, body, - qs: query, - uri: `https://beta-api.customer.io/v1/api${endpoint}`, + uri: '', json: true, }; + + if (baseApi === 'tracking') { + options.uri = `https://track.customer.io/api/v1${endpoint}`; + const basicAuthKey = Buffer.from(`${credentials.trackingSiteId}:${credentials.trackingApiKey}`).toString('base64'); + Object.assign(options.headers, {'Authorization': `Basic ${basicAuthKey}`}); + } else if (baseApi === 'api') { + options.uri = `https://api.customer.io/v1/api${endpoint}`; + const basicAuthKey = Buffer.from(`${credentials.trackingSiteId}:${credentials.trackingApiKey}`).toString('base64'); + Object.assign(options.headers, {'Authorization': `Basic ${basicAuthKey}`}); + } else if (baseApi === 'beta') { + options.uri = `https://beta-api.customer.io/v1/api${endpoint}`; + Object.assign(options.headers, {'Authorization': `Bearer ${credentials.appApiKey as string}`}); + } + try { return await this.helpers.request!(options); } catch (error) { @@ -63,3 +75,13 @@ export function eventExists(currentEvents: string[], webhookEvents: IDataObject) } return true; } + +export function validateJSON(json: string | undefined): any { // tslint:disable-line:no-any + let result; + try { + result = JSON.parse(json!); + } catch (exception) { + result = undefined; + } + return result; +} diff --git a/packages/nodes-base/nodes/CustomerIo/SegmentDescription.ts b/packages/nodes-base/nodes/CustomerIo/SegmentDescription.ts new file mode 100644 index 0000000000..a968790882 --- /dev/null +++ b/packages/nodes-base/nodes/CustomerIo/SegmentDescription.ts @@ -0,0 +1,71 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const segmentOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'segment', + ], + }, + }, + options: [ + { + name: 'Add Customer', + value: 'add', + }, + { + name: 'Remove Customer', + value: 'remove', + }, + ], + default: 'add', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const segmentFields = [ + +/* -------------------------------------------------------------------------- */ +/* segment:add */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'ID', + name: 'id', + type: 'number', + required: true, + default: 0, + displayOptions: { + show: { + resource: [ + 'segment', + ], + operation: [ + 'add', 'remove' + ] + }, + }, + description: 'The unique identifier of the segment.', + }, + { + displayName: 'IDs', + name: 'ids', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'segment', + ], + operation: [ + 'add', 'remove' + ] + }, + }, + description: 'A list of customer ids to add to the segment.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/CustomerIo/customerio.png b/packages/nodes-base/nodes/CustomerIo/customerio.png new file mode 100644 index 0000000000000000000000000000000000000000..8ff1d817bf69f36ef80b8a87b28236a90c55b910 GIT binary patch literal 2662 zcmV-s3YqnZP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 zVoOIv0RI600RN!9r;`8x00(qQO+^Rf2nz`cE{~YuJ^%m-YDq*vRA}Dqnr)0-MH$C` z&&)aZb>F(XrQK~yx0Jq6p}ZJkp=rSYwHkyFNzs@nVgey*1VYjX(h!vZK@zA+qJbDA z3egll6{A3TX*GywEh)4>so<7&E8E?+d-vtub7sa5=iIyZ?w)P;?k*rnpPUbS&OK-5 zH~)F&nP+BJoO68AOnuw{r=b6z?G*G!W!{HSrlmR}iOje(GYf42P{{aZfXo>BOhG$v zv7)6z4DX`!0XY12aw|#AN1`SP;~MmFK|7p9G()@Sd4PdG5%dx{h=4bX#&yj79{F?2 zT`^s5nf8oW*0|@#9J~uNf)XTh<>*I;b*MC*o9MV>M)aOOv;{@?tsHs?Pa%S{P+g`V z3g-wdbH2mE&ETd9J-xx<3@Cc;;m||)9ypw-C1&)Y?{WAi9K06f9M5QT%jC#F;F)Sw-raUlcS5U6WZIERA47qKIgbE=a; zy9urYXrS0l={*#6R_qWc9iVhzw5ajLpAe!3T9PinjQJ>%Hhdp*d=f!pJdt1qDefOF zs`6W9#!TcWTg@~+=tApU%^QxaXit2ZacIox1>gIoiBe_FP}p|HP$>3_WhZizd~um8!c57;V{`* zH3g^&GLR7uM4_rKQKFUjcMt7$@#ohqGJkpqw(SK4a|#ET~YW^F(!fohWG5SX>0s$>d13>`sv$z$6& zvaK~wRJqRpmgt;_Hn{Q9X#{7V4f@K0DS+n7X;_POh@PCx8e_??rg>dlhy&<-i_+i( z*?>cPG5M4wCKS%-E`n7>$y zpDRO`YZz79L7PgQrE@r&yq*^QYIuA{&{w$yOVee{y_3))`h?tqc!ZWYx6`~HETW^9 z9_yy}Exb&Emx*9Qa_!{W`C~=s1sDYdp^= zm*6D^8r{!#_FXyPE?6hVpfxd#Gl(L)ob2+ko#ad6Z~Ip@h+WcPNbHP%$}U>z&!3@*flI1>vj znrPo9M0vdXFMfF&yWeKGgjNZ2iU_7i^8)6c8{a1Tbj1OHn=%U+FSElbr;znOIJ;wC zek9`_`?kEi1*Ji%(4!a<4@4bR;T+ZgD34vw@W_o6hY4Z#+ZclyKY#=iuy8G23TMX# zq_M9@>Ry|kv&w6>F02+?oRAMc`|z?Q&$kaXI$sHjU*ExR?quKyN+S}(S%bA$8~+`r zqMG3Wp1O}mze}lzmqH6i2iW`Xu|kSqN;G!RwI)Gk5;toMP9s~2J=YHGQsPDNTN$w} zr3-$$=!$!n7aN^eiHj44j?p%c)f>3*D%$5KJLl2E{NrhMJVDQXvdyEbVG)9m#q(Lb zfFO+dL{Tbm!3`|FEO|>Od50QX`Y)Q;X@GO%S3q!7y!Sa><<& zgu;oSElweOP2u8Wuk?qOylvl~zwYJ>GsjXU9m_nXL!%6{G=dF4;imz@BG#fk{7mef z6XkJAao#dong~O1tldc4(g}CYlJk^k!dtEz9>^>qUCJHzue@tz{zxj!BoI>`XG)W8 zAm2*91wW0SCf7nkE7?Z07dv-ybqZs6{T+%Sc7$bLJQ4APO2>LTlyK9)>+js!deM%K zfkvmy4D914a|jkPj?aIUwYNo&CVcrsJ5146n=>n}9+}%~@#|Wl)M-jn8l+(XpZ;cL z2{tuo!8y{teEnhRTEXB^^oMa62HIojIJ4(*<7VG`3l!;B%Q0{XrPYc3+Lj^{5?DP!~Jx> zK`M`*#<@DyzN@bl!FX>5&C zk;trxJ$uSF{S#Xt&V){Y5;ZTDcG?r`%s^Ms#iOuto?jV-GiWmn{gK!w^520@L7##? zf&C910w=n*-_gzh001R)MObuXVRU6WV{&C-bY%cCFfuSLFgPtSF;p-)Ix#RhG&U_T zG&(Ra)JeMs0000bbVXQnWMOn=I&E)cX=ZrsyIXW>gIy5#dFf=+a UFr^d&y8r+H07*qoM6N<$f-!mH*8l(j literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 02886773dd..4cb5b4a2cd 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -217,6 +217,7 @@ "dist/nodes/CrateDb/CrateDb.node.js", "dist/nodes/Cron.node.js", "dist/nodes/Crypto.node.js", + "dist/nodes/CustomerIo/CustomerIo.node.js", "dist/nodes/CustomerIo/CustomerIoTrigger.node.js", "dist/nodes/DateTime.node.js", "dist/nodes/Discord/Discord.node.js", From 512fe4ea70acdc2b74b96adf869b3ea3f28537be Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 2 Sep 2020 12:32:12 +0200 Subject: [PATCH 4/6] :zap: Some improvements to Customer.io Node --- .../credentials/CustomerIoApi.credentials.ts | 6 +- .../nodes/CustomerIo/CampaignDescription.ts | 128 ++++++------ .../nodes/CustomerIo/CustomerDescription.ts | 187 +++--------------- .../nodes/CustomerIo/CustomerIo.node.ts | 138 +++++-------- .../nodes/CustomerIo/EventDescription.ts | 47 +++-- .../nodes/CustomerIo/GenericFunctions.ts | 8 +- .../nodes/CustomerIo/SegmentDescription.ts | 14 +- 7 files changed, 180 insertions(+), 348 deletions(-) diff --git a/packages/nodes-base/credentials/CustomerIoApi.credentials.ts b/packages/nodes-base/credentials/CustomerIoApi.credentials.ts index 78a29dd2dc..70387fd066 100644 --- a/packages/nodes-base/credentials/CustomerIoApi.credentials.ts +++ b/packages/nodes-base/credentials/CustomerIoApi.credentials.ts @@ -15,21 +15,21 @@ export class CustomerIoApi implements ICredentialType { type: 'string' as NodePropertyTypes, default: '', description: 'Required for tracking API.', - required: true + required: true, }, { displayName: 'Tracking Site ID', name: 'trackingSiteId', type: 'string' as NodePropertyTypes, default: '', - description: 'Required for tracking API.' + description: 'Required for tracking API.', }, { displayName: 'App API Key', name: 'appApiKey', type: 'string' as NodePropertyTypes, default: '', - description: 'Required for App API.' + description: 'Required for App API.', }, ]; } diff --git a/packages/nodes-base/nodes/CustomerIo/CampaignDescription.ts b/packages/nodes-base/nodes/CustomerIo/CampaignDescription.ts index 91cf22f737..0893a9f219 100644 --- a/packages/nodes-base/nodes/CustomerIo/CampaignDescription.ts +++ b/packages/nodes-base/nodes/CustomerIo/CampaignDescription.ts @@ -32,9 +32,9 @@ export const campaignOperations = [ ] as INodeProperties[]; export const campaignFields = [ -/* -------------------------------------------------------------------------- */ -/* campaign:get */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* campaign:get */ + /* -------------------------------------------------------------------------- */ { displayName: 'Campaign ID', name: 'campaignId', @@ -47,15 +47,15 @@ export const campaignFields = [ 'campaign', ], operation: [ - 'get' + 'get', ] }, }, description: 'The unique identifier for the campaign', }, -/* -------------------------------------------------------------------------- */ -/* campaign:getMetrics */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* campaign:getMetrics */ + /* -------------------------------------------------------------------------- */ { displayName: 'Campaign ID', name: 'campaignId', @@ -68,7 +68,7 @@ export const campaignFields = [ 'campaign', ], operation: [ - 'getMetrics' + 'getMetrics', ] }, }, @@ -85,7 +85,7 @@ export const campaignFields = [ 'campaign', ], operation: [ - 'getMetrics' + 'getMetrics', ] }, }, @@ -93,19 +93,19 @@ export const campaignFields = [ options: [ { name: 'Hours', - value: 'hours' + value: 'hours', }, { name: 'Days', - value: 'days' + value: 'days', }, { name: 'Weeks', - value: 'weeks' + value: 'weeks', }, { name: 'Months', - value: 'months' + value: 'months', }, ] }, @@ -121,7 +121,7 @@ export const campaignFields = [ 'campaign', ], operation: [ - 'getMetrics' + 'getMetrics', ], }, }, @@ -146,54 +146,54 @@ export const campaignFields = [ }, }, options: [ - { - displayName: 'Steps', - name: 'steps', - type: 'number', - default: 0, - description: 'Integer specifying how many steps to return. Defaults to the maximum number of timeperiods available, or 12 when using the months period. Maximum timeperiods available are 24 hours, 45 days, 12 weeks and 120 months', - typeOptions: { - minValue: 0, - maxValue: 120 - } - }, - { - displayName: 'Type', - name: 'type', - type: 'options', - default: 'empty', - description: 'Specify metric type', - options: [ - { - name: 'Empty', - value: 'empty' - }, - { - name: 'Email', - value: 'email' - }, - { - name: 'Webhook', - value: 'webhook' - }, - { - name: 'twilio', - value: 'twilio' - }, - { - name: 'Urban Airship', - value: 'urbanAirship' - }, - { - name: 'Slack', - value: 'slack' - }, - { - name: 'Push', - value: 'push' - }, - ] - }, - ], - }, + { + displayName: 'Steps', + name: 'steps', + type: 'number', + default: 0, + description: 'Integer specifying how many steps to return. Defaults to the maximum number of timeperiods available, or 12 when using the months period. Maximum timeperiods available are 24 hours, 45 days, 12 weeks and 120 months', + typeOptions: { + minValue: 0, + maxValue: 120, + } + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + default: 'empty', + description: 'Specify metric type', + options: [ + { + name: 'Empty', + value: 'empty', + }, + { + name: 'Email', + value: 'email', + }, + { + name: 'Push', + value: 'push', + }, + { + name: 'Slack', + value: 'slack', + }, + { + name: 'twilio', + value: 'twilio', + }, + { + name: 'Urban Airship', + value: 'urbanAirship', + }, + { + name: 'Webhook', + value: 'webhook', + }, + ] + }, + ], + }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/CustomerIo/CustomerDescription.ts b/packages/nodes-base/nodes/CustomerIo/CustomerDescription.ts index 47d030997f..a6a9576a92 100644 --- a/packages/nodes-base/nodes/CustomerIo/CustomerDescription.ts +++ b/packages/nodes-base/nodes/CustomerIo/CustomerDescription.ts @@ -14,53 +14,31 @@ export const customerOperations = [ }, options: [ { - name: 'Create', - value: 'create', - description: 'Create a customer.', + name: 'Create/Update', + value: 'upsert', + description: 'Create/Update a customer.', }, { name: 'Delete', value: 'delete', description: 'Delete a customer.', - }, - { - name: 'Update', - value: 'update', - description: 'Update a customer.', }, ], - default: 'create', + default: 'upsert', description: 'The operation to perform.', }, ] as INodeProperties[]; export const customerFields = [ -/* -------------------------------------------------------------------------- */ -/* customer:create/delete */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* customer:delete */ + /* -------------------------------------------------------------------------- */ { displayName: 'ID', name: 'id', - type: 'number', - required: true, - default: 0, - displayOptions: { - show: { - resource: [ - 'customer', - ], - operation: [ - 'create', 'delete' - ] - }, - }, - description: 'The unique identifier for the customer.', - }, - { - displayName: 'Email', - name: 'email', type: 'string', + required: true, default: '', displayOptions: { show: { @@ -68,144 +46,29 @@ export const customerFields = [ 'customer', ], operation: [ - 'create' + 'delete', ] }, }, - description: 'The email address of the user.', - }, - { - displayName: 'Created at', - name: 'createdAt', - type: 'dateTime', - default: '', - displayOptions: { - show: { - resource: [ - 'customer', - ], - operation: [ - 'create' - ] - }, - }, - description: 'The UNIX timestamp from when the user was created.', - }, - { - displayName: 'JSON Parameters', - name: 'jsonParameters', - type: 'boolean', - default: false, - description: '', - displayOptions: { - show: { - resource: [ - 'customer', - ], - operation: [ - 'create' - ], - }, - }, - }, - { - displayName: ' Additional Fields', - name: 'additionalFieldsJson', - type: 'json', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - displayOptions: { - show: { - resource: [ - 'customer', - ], - operation: [ - 'create' - ], - jsonParameters: [ - true, - ], - }, - }, - description: 'Object of values to set as described here.', - }, - { - displayName: 'Additional Fields', - name: 'additionalFields', - type: 'collection', - placeholder: 'Add Field', - default: {}, - displayOptions: { - show: { - resource: [ - 'customer', - ], - operation: [ - 'create' - ], - jsonParameters: [ - false, - ], - }, - }, - options: [ - { - displayName: 'Custom Properties', - name: 'customProperties', - type: 'fixedCollection', - description: 'Custom Properties', - typeOptions: { - multipleValues: true, - }, - options: [ - { - displayName: 'Property', - name: 'customProperty', - values: [ - { - displayName: 'Key', - name: 'key', - type: 'string', - required: true, - default: '', - description: 'Property name.', - placeholder: 'Plan' - }, - - { - displayName: 'Value', - name: 'value', - type: 'string', - required: true, - default: '', - description: 'Property value.', - placeholder: 'Basic' - }, - ], - }, - ] - }, - ], + description: 'The unique identifier for the customer.', }, -/* -------------------------------------------------------------------------- */ -/* customer:update */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* customer:upsert */ + /* -------------------------------------------------------------------------- */ { displayName: 'ID', name: 'id', - type: 'number', + type: 'string', required: true, - default: 0, + default: '', displayOptions: { show: { resource: [ 'customer', ], operation: [ - 'update' + 'upsert', ] }, }, @@ -223,7 +86,7 @@ export const customerFields = [ 'customer', ], operation: [ - 'update' + 'upsert', ], }, }, @@ -242,7 +105,7 @@ export const customerFields = [ 'customer', ], operation: [ - 'update' + 'upsert', ], jsonParameters: [ true, @@ -263,7 +126,7 @@ export const customerFields = [ 'customer', ], operation: [ - 'update' + 'upsert', ], jsonParameters: [ false, @@ -290,18 +153,18 @@ export const customerFields = [ type: 'string', required: true, default: '', - description: 'Property name.', - placeholder: 'Plan' + description: 'Property name.', + placeholder: 'Plan', }, { displayName: 'Value', name: 'value', - type: 'string', - required: true, + type: 'string', + required: true, default: '', - description: 'Property value.', - placeholder: 'Basic' + description: 'Property value.', + placeholder: 'Basic', }, ], }, diff --git a/packages/nodes-base/nodes/CustomerIo/CustomerIo.node.ts b/packages/nodes-base/nodes/CustomerIo/CustomerIo.node.ts index f3bd6364c9..f664820bbc 100644 --- a/packages/nodes-base/nodes/CustomerIo/CustomerIo.node.ts +++ b/packages/nodes-base/nodes/CustomerIo/CustomerIo.node.ts @@ -6,21 +6,18 @@ import { INodeTypeDescription, INodeExecutionData, INodeType, - ILoadOptionsFunctions, - INodePropertyOptions, } from 'n8n-workflow'; import { customerIoApiRequest, validateJSON } from './GenericFunctions'; import { campaignOperations, campaignFields } from './CampaignDescription'; import { customerOperations, customerFields } from './CustomerDescription'; import { eventOperations, eventFields } from './EventDescription'; import { segmentOperations, segmentFields } from './SegmentDescription'; -import { DateTime } from '../DateTime.node'; export class CustomerIo implements INodeType { description: INodeTypeDescription = { displayName: 'Customer.io', - name: 'customerio', + name: 'customerIo', icon: 'file:customerio.png', group: ['output'], version: 1, @@ -64,29 +61,29 @@ export class CustomerIo implements INodeType { default: 'customer', description: 'Resource to consume.', }, - // CAMPAIGN - ...campaignOperations, - ...campaignFields, - // CUSTOMER - ...customerOperations, - ...customerFields, - // EVENT - ...eventOperations, - ...eventFields, - // SEGMENT - ...segmentOperations, - ...segmentFields + // CAMPAIGN + ...campaignOperations, + ...campaignFields, + // CUSTOMER + ...customerOperations, + ...customerFields, + // EVENT + ...eventOperations, + ...eventFields, + // SEGMENT + ...segmentOperations, + ...segmentFields ], }; async execute(this: IExecuteFunctions): Promise { const returnData: IDataObject[] = []; const items = this.getInputData(); - let responseData; const resource = this.getNodeParameter('resource', 0) as string; const operation = this.getNodeParameter('operation', 0) as string; - const body : IDataObject = {}; + const body: IDataObject = {}; + let responseData; for (let i = 0; i < items.length; i++) { if (resource === 'campaign') { @@ -95,6 +92,7 @@ export class CustomerIo implements INodeType { const endpoint = `/campaigns/${campaignId}`; responseData = await customerIoApiRequest.call(this, 'GET', endpoint, body, 'beta'); + responseData = responseData.campaign; } if (operation === 'getAll') { @@ -147,48 +145,8 @@ export class CustomerIo implements INodeType { } if (resource === 'customer') { - if (operation === 'create') { - const id = this.getNodeParameter('id', i) as number; - const email = this.getNodeParameter('email', i) as string; - const createdAt = this.getNodeParameter('createdAt', i) as string; - const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; - body.email = email; - body.created_at = new Date(createdAt).getTime() / 1000; - - if (jsonParameters) { - const additionalFieldsJson = this.getNodeParameter('additionalFieldsJson', i) as string; - - if (additionalFieldsJson !== '') { - - if (validateJSON(additionalFieldsJson) !== undefined) { - - Object.assign(body, JSON.parse(additionalFieldsJson)); - - } else { - throw new Error('Additional fields must be a valid JSON'); - } - } - } else { - const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; - - if (additionalFields.customProperties) { - const data : any = {}; - //@ts-ignore - additionalFields.customProperties.customProperty.map(property => { - data[property.key] = property.value; - }); - - body.data = data; - } - } - - const endpoint = `/customers/${id}`; - - responseData = await customerIoApiRequest.call(this, 'PUT', endpoint, body, 'tracking'); - } - - if (operation === 'update') { + if (operation === 'upsert') { const id = this.getNodeParameter('id', i) as number; const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; @@ -209,7 +167,7 @@ export class CustomerIo implements INodeType { const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; if (additionalFields.customProperties) { - const data : any = {}; + const data: any = {}; // tslint:disable-line:no-any //@ts-ignore additionalFields.customProperties.customProperty.map(property => { data[property.key] = property.value; @@ -230,6 +188,8 @@ export class CustomerIo implements INodeType { const endpoint = `/customers/${id}`; responseData = await customerIoApiRequest.call(this, 'PUT', endpoint, body, 'tracking'); + + responseData = Object.assign({ id }, body); } if (operation === 'delete') { @@ -239,17 +199,21 @@ export class CustomerIo implements INodeType { const endpoint = `/customers/${id}`; - responseData = await customerIoApiRequest.call(this, 'DELETE', endpoint, body, 'tracking'); + await customerIoApiRequest.call(this, 'DELETE', endpoint, body, 'tracking'); + + responseData = { + success: true, + }; } } if (resource === 'event') { if (operation === 'track') { - const id = this.getNodeParameter('id', i) as number; - const name = this.getNodeParameter('name', i) as string; + const customerId = this.getNodeParameter('customerId', i) as number; + const eventName = this.getNodeParameter('eventName', i) as string; const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; - body.name = name; + body.name = eventName; if (jsonParameters) { const additionalFieldsJson = this.getNodeParameter('additionalFieldsJson', i) as string; @@ -257,16 +221,14 @@ export class CustomerIo implements INodeType { if (additionalFieldsJson !== '') { if (validateJSON(additionalFieldsJson) !== undefined) { - Object.assign(body, JSON.parse(additionalFieldsJson)); - } else { throw new Error('Additional fields must be a valid JSON'); } } } else { const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; - const data : any = {}; + const data: any = {}; // tslint:disable-line:no-any if (additionalFields.customAttributes) { //@ts-ignore @@ -282,16 +244,19 @@ export class CustomerIo implements INodeType { body.data = data; } - const endpoint = `/customers/${id}/events`; + const endpoint = `/customers/${customerId}/events`; - responseData = await customerIoApiRequest.call(this, 'POST', endpoint, body, 'tracking'); + await customerIoApiRequest.call(this, 'POST', endpoint, body, 'tracking'); + responseData = { + success: true, + }; } if (operation === 'trackAnonymous') { - const name = this.getNodeParameter('name', i) as string; + const eventName = this.getNodeParameter('eventName', i) as string; const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; - body.name = name; + body.name = eventName; if (jsonParameters) { const additionalFieldsJson = this.getNodeParameter('additionalFieldsJson', i) as string; @@ -308,7 +273,7 @@ export class CustomerIo implements INodeType { } } else { const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; - const data : any = {}; + const data: any = {}; // tslint:disable-line:no-any if (additionalFields.customAttributes) { //@ts-ignore @@ -320,31 +285,34 @@ export class CustomerIo implements INodeType { } const endpoint = `/events`; - responseData = await customerIoApiRequest.call(this, 'POST', endpoint, body, 'tracking'); + await customerIoApiRequest.call(this, 'POST', endpoint, body, 'tracking'); + + responseData = { + success: true, + }; } } if (resource === 'segment') { - const id = this.getNodeParameter('id', i) as number; - const ids = this.getNodeParameter('ids', i) as string; - const idArray : string[] = []; + const segmentId = this.getNodeParameter('segmentId', i) as number; + const customerIds = this.getNodeParameter('customerIds', i) as string; - ids.split(',').map(id => { - idArray.push(id); - }); + body.id = segmentId; + body.ids = customerIds.split(','); - body.id = id; - body.ids = idArray; - - let endpoint = ``; + let endpoint = ''; if (operation === 'add') { - endpoint = `/segments/${id}/add_customers`; + endpoint = `/segments/${segmentId}/add_customers`; } else { - endpoint = `/segments/${id}/remove_customers`; + endpoint = `/segments/${segmentId}/remove_customers`; } responseData = await customerIoApiRequest.call(this, 'POST', endpoint, body, 'tracking'); + + responseData = { + success: true, + }; } if (Array.isArray(responseData)) { diff --git a/packages/nodes-base/nodes/CustomerIo/EventDescription.ts b/packages/nodes-base/nodes/CustomerIo/EventDescription.ts index 7b57004a9e..ac73dde9f9 100644 --- a/packages/nodes-base/nodes/CustomerIo/EventDescription.ts +++ b/packages/nodes-base/nodes/CustomerIo/EventDescription.ts @@ -35,26 +35,26 @@ export const eventFields = [ /* event:track */ /* -------------------------------------------------------------------------- */ { - displayName: 'ID', - name: 'id', - type: 'number', + displayName: 'Customer ID', + name: 'customerId', + type: 'string', required: true, - default: 0, + default: '', displayOptions: { show: { resource: [ 'event', ], operation: [ - 'track' + 'track', ] }, }, description: 'The unique identifier for the customer.', }, { - displayName: 'Name', - name: 'name', + displayName: 'Event Name', + name: 'eventName', type: 'string', default: '', displayOptions: { @@ -63,7 +63,7 @@ export const eventFields = [ 'event', ], operation: [ - 'track' + 'track', ] }, }, @@ -81,7 +81,7 @@ export const eventFields = [ 'event', ], operation: [ - 'track' + 'track', ], }, }, @@ -100,7 +100,7 @@ export const eventFields = [ 'event', ], operation: [ - 'track' + 'track', ], jsonParameters: [ true, @@ -121,10 +121,10 @@ export const eventFields = [ 'event', ], operation: [ - 'track' + 'track', ], jsonParameters: [ - false + false, ] }, }, @@ -149,7 +149,7 @@ export const eventFields = [ required: true, default: '', description: 'Attribute name.', - placeholder: 'Price' + placeholder: 'Price', }, { @@ -159,7 +159,7 @@ export const eventFields = [ required: true, default: '', description: 'Attribute value.', - placeholder: '25.50' + placeholder: '25.50', }, ], }, @@ -178,8 +178,8 @@ export const eventFields = [ /* event:track anonymous */ /* -------------------------------------------------------------------------- */ { - displayName: 'Name', - name: 'name', + displayName: 'Event Name', + name: 'eventName', type: 'string', required: true, default: '', @@ -189,7 +189,7 @@ export const eventFields = [ 'event', ], operation: [ - 'trackAnonymous' + 'trackAnonymous', ] }, }, @@ -207,7 +207,7 @@ export const eventFields = [ 'event', ], operation: [ - 'trackAnonymous' + 'trackAnonymous', ], }, }, @@ -226,7 +226,7 @@ export const eventFields = [ 'event', ], operation: [ - 'trackAnonymous' + 'trackAnonymous', ], jsonParameters: [ true, @@ -247,10 +247,10 @@ export const eventFields = [ 'event', ], operation: [ - 'trackAnonymous' + 'trackAnonymous', ], jsonParameters: [ - false + false, ] }, }, @@ -275,9 +275,8 @@ export const eventFields = [ required: true, default: '', description: 'Attribute name.', - placeholder: 'Price' + placeholder: 'Price', }, - { displayName: 'Value', name: 'value', @@ -285,7 +284,7 @@ export const eventFields = [ required: true, default: '', description: 'Attribute value.', - placeholder: '25.50' + placeholder: '25.50', }, ], }, diff --git a/packages/nodes-base/nodes/CustomerIo/GenericFunctions.ts b/packages/nodes-base/nodes/CustomerIo/GenericFunctions.ts index 9cea18a4c9..c58bea8883 100644 --- a/packages/nodes-base/nodes/CustomerIo/GenericFunctions.ts +++ b/packages/nodes-base/nodes/CustomerIo/GenericFunctions.ts @@ -16,7 +16,7 @@ import { get, } from 'lodash'; -export async function customerIoApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: object, baseApi? : string, query?: IDataObject): Promise { // tslint:disable-line:no-any +export async function customerIoApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: object, baseApi?: string, query?: IDataObject): Promise { // tslint:disable-line:no-any const credentials = this.getCredentials('customerIoApi'); if (credentials === undefined) { @@ -38,14 +38,14 @@ export async function customerIoApiRequest(this: IHookFunctions | IExecuteFuncti if (baseApi === 'tracking') { options.uri = `https://track.customer.io/api/v1${endpoint}`; const basicAuthKey = Buffer.from(`${credentials.trackingSiteId}:${credentials.trackingApiKey}`).toString('base64'); - Object.assign(options.headers, {'Authorization': `Basic ${basicAuthKey}`}); + Object.assign(options.headers, { 'Authorization': `Basic ${basicAuthKey}` }); } else if (baseApi === 'api') { options.uri = `https://api.customer.io/v1/api${endpoint}`; const basicAuthKey = Buffer.from(`${credentials.trackingSiteId}:${credentials.trackingApiKey}`).toString('base64'); - Object.assign(options.headers, {'Authorization': `Basic ${basicAuthKey}`}); + Object.assign(options.headers, { 'Authorization': `Basic ${basicAuthKey}` }); } else if (baseApi === 'beta') { options.uri = `https://beta-api.customer.io/v1/api${endpoint}`; - Object.assign(options.headers, {'Authorization': `Bearer ${credentials.appApiKey as string}`}); + Object.assign(options.headers, { 'Authorization': `Bearer ${credentials.appApiKey as string}` }); } try { diff --git a/packages/nodes-base/nodes/CustomerIo/SegmentDescription.ts b/packages/nodes-base/nodes/CustomerIo/SegmentDescription.ts index a968790882..9586c13730 100644 --- a/packages/nodes-base/nodes/CustomerIo/SegmentDescription.ts +++ b/packages/nodes-base/nodes/CustomerIo/SegmentDescription.ts @@ -33,8 +33,8 @@ export const segmentFields = [ /* segment:add */ /* -------------------------------------------------------------------------- */ { - displayName: 'ID', - name: 'id', + displayName: 'Segment ID', + name: 'segmentId', type: 'number', required: true, default: 0, @@ -44,15 +44,16 @@ export const segmentFields = [ 'segment', ], operation: [ - 'add', 'remove' + 'add', + 'remove', ] }, }, description: 'The unique identifier of the segment.', }, { - displayName: 'IDs', - name: 'ids', + displayName: 'Customer IDs', + name: 'customerIds', type: 'string', required: true, default: '', @@ -62,7 +63,8 @@ export const segmentFields = [ 'segment', ], operation: [ - 'add', 'remove' + 'add', + 'remove', ] }, }, From b01621bc8093c9f92f98608f1a8faad454743a25 Mon Sep 17 00:00:00 2001 From: Rupenieks <32895755+Rupenieks@users.noreply.github.com> Date: Wed, 2 Sep 2020 13:04:43 +0200 Subject: [PATCH 5/6] :bug: Fixed multiple labelIds not working on message -> getAll (#916) --- packages/nodes-base/nodes/Google/Gmail/Gmail.node.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/Google/Gmail/Gmail.node.ts b/packages/nodes-base/nodes/Google/Gmail/Gmail.node.ts index 81645908bc..924ad85c7b 100644 --- a/packages/nodes-base/nodes/Google/Gmail/Gmail.node.ts +++ b/packages/nodes-base/nodes/Google/Gmail/Gmail.node.ts @@ -476,7 +476,7 @@ export class Gmail implements INodeType { if (qs.labelIds == '') { delete qs.labelIds; } else { - qs.labelIds = (qs.labelIds as string[]).join(','); + qs.labelIds = qs.labelIds as string[]; } } From ebc52acdbc8e1346d60f9481bd2926f2dcfe3732 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 2 Sep 2020 13:05:43 +0200 Subject: [PATCH 6/6] :bug: Fix Label-ID field --- packages/nodes-base/nodes/Google/Gmail/MessageDescription.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/Google/Gmail/MessageDescription.ts b/packages/nodes-base/nodes/Google/Gmail/MessageDescription.ts index cd23aa0fb3..10ea8e37d4 100644 --- a/packages/nodes-base/nodes/Google/Gmail/MessageDescription.ts +++ b/packages/nodes-base/nodes/Google/Gmail/MessageDescription.ts @@ -442,7 +442,7 @@ export const messageFields = [ typeOptions: { loadOptionsMethod: 'getLabels', }, - default: '', + default: [], description: 'Only return messages with labels that match all of the specified label IDs.', }, {