From e209077160ca9126f75f10a4cb7846c18adbedaf Mon Sep 17 00:00:00 2001 From: Erin Date: Thu, 2 Jul 2020 16:41:59 -0400 Subject: [PATCH 01/39] :sparkles: ConvertKit Trigger and Regular Node --- .../credentials/ConvertKitApi.credentials.ts | 21 ++ .../nodes/ConvertKit/ConvertKit.node.ts | 289 ++++++++++++++++++ .../ConvertKit/ConvertKitTrigger.node.ts | 163 ++++++++++ .../nodes/ConvertKit/FieldDescription.ts | 83 +++++ .../nodes/ConvertKit/FormDescription.ts | 174 +++++++++++ .../nodes/ConvertKit/GenericFunctions.ts | 44 +++ .../nodes/ConvertKit/SequenceDescription.ts | 174 +++++++++++ .../nodes/ConvertKit/TagDescription.ts | 204 +++++++++++++ .../nodes/ConvertKit/convertKit.png | Bin 0 -> 4958 bytes packages/nodes-base/package.json | 3 + 10 files changed, 1155 insertions(+) create mode 100644 packages/nodes-base/credentials/ConvertKitApi.credentials.ts create mode 100644 packages/nodes-base/nodes/ConvertKit/ConvertKit.node.ts create mode 100644 packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts create mode 100644 packages/nodes-base/nodes/ConvertKit/FieldDescription.ts create mode 100644 packages/nodes-base/nodes/ConvertKit/FormDescription.ts create mode 100644 packages/nodes-base/nodes/ConvertKit/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts create mode 100644 packages/nodes-base/nodes/ConvertKit/TagDescription.ts create mode 100644 packages/nodes-base/nodes/ConvertKit/convertKit.png diff --git a/packages/nodes-base/credentials/ConvertKitApi.credentials.ts b/packages/nodes-base/credentials/ConvertKitApi.credentials.ts new file mode 100644 index 00000000000..1685c11a244 --- /dev/null +++ b/packages/nodes-base/credentials/ConvertKitApi.credentials.ts @@ -0,0 +1,21 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + + +export class ConvertKitApi implements ICredentialType { + name = 'convertKitApi'; + displayName = 'ConvertKit Api'; + properties = [ + { + displayName: 'API Secret', + name: 'apiSecret', + type: 'string' as NodePropertyTypes, + default: '', + typeOptions: { + password: true, + }, + }, + ]; +} diff --git a/packages/nodes-base/nodes/ConvertKit/ConvertKit.node.ts b/packages/nodes-base/nodes/ConvertKit/ConvertKit.node.ts new file mode 100644 index 00000000000..71f58a41728 --- /dev/null +++ b/packages/nodes-base/nodes/ConvertKit/ConvertKit.node.ts @@ -0,0 +1,289 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeTypeDescription, + INodeType, +} from 'n8n-workflow'; + +import { + convertKitApiRequest, +} from './GenericFunctions'; + +import { + fieldOperations, + fieldFields, +} from './FieldDescription'; + +import { + formOperations, + formFields, +} from './FormDescription'; + +import { + sequenceOperations, + sequenceFields, +} from './SequenceDescription'; + +import { + tagOperations, + tagFields, +} from './TagDescription'; + +export class ConvertKit implements INodeType { + description: INodeTypeDescription = { + displayName: 'ConvertKit', + name: 'convertKit', + icon: 'file:convertKit.png', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume ConvertKit API.', + defaults: { + name: 'ConvertKit', + color: '#fb6970', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'convertKitApi', + required: true, + } + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Field', + value: 'field', + }, + { + name: 'Form', + value: 'form', + }, + { + name: 'Sequence', + value: 'sequence', + }, + { + name: 'Tag', + value: 'tag', + }, + ], + default: 'field', + description: 'The resource to operate on.' + }, + //-------------------- + // Field Description + //-------------------- + ...fieldOperations, + ...fieldFields, + //-------------------- + // FormDescription + //-------------------- + ...formOperations, + ...formFields, + //-------------------- + // Sequence Description + //-------------------- + ...sequenceOperations, + ...sequenceFields, + //-------------------- + // Tag Description + //-------------------- + ...tagOperations, + ...tagFields, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + + let method = ''; + let endpoint = ''; + const qs: IDataObject = {}; + let responseData; + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + const fullOperation = `${resource}/${operation}`; + + for (let i = 0; i < items.length; i++) { + //-------------------- + // Field Operations + //-------------------- + if(resource === 'field') { + //--------- + // Update + //--------- + if(operation === 'update') { + qs.label = this.getNodeParameter('label', i) as string; + + const id = this.getNodeParameter('id', i) as string; + + method = 'PUT'; + endpoint = `/custom_fields/${id}`; + //--------- + // Get All + //--------- + } else if(operation === 'getAll') { + method = 'GET'; + endpoint = '/custom_fields'; + //--------- + // Create + //--------- + } else if(operation === 'create') { + qs.label = this.getNodeParameter('label', i) as string; + + method = 'POST'; + endpoint = '/custom_fields'; + //--------- + // Delete + //--------- + } else if(operation === 'delete') { + const id = this.getNodeParameter('id', i) as string; + + method = 'DELETE'; + endpoint = `/custom_fields/${id}`; + } else { + throw new Error(`The operation "${operation}" is not known!`); + } + //-------------------------------------------- + // Form, Sequence, and Tag Operations + //-------------------------------------------- + } else if(['form', 'sequence', 'tag'].includes(resource)) { + //----------------- + // Add Subscriber + //----------------- + if(operation === 'addSubscriber') { + qs.email= this.getNodeParameter('email', i) as string; + const id = this.getNodeParameter('id', i); + + const additionalParams = this.getNodeParameter('additionalFields', 0) as IDataObject; + + if(additionalParams.firstName) { + qs.first_name = additionalParams.firstName; + } + + if(additionalParams.fields !== undefined) { + const fields = {} as IDataObject; + const fieldsParams = additionalParams.fields as IDataObject; + const field = fieldsParams?.field as IDataObject[]; + + for(let j = 0; j < field.length; j++) { + const key = field[j].key as string; + const value = field[j].value as string; + + fields[key] = value; + } + + qs.fields = fields; + } + + if(resource === 'form') { + method = 'POST'; + endpoint = `/forms/${id}/subscribe`; + } else if(resource === 'sequence') { + method = 'POST'; + endpoint = `/sequences/${id}/subscribe`; + } else if(resource === 'tag') { + method = 'POST'; + endpoint = `/tags/${id}/subscribe`; + } + //----------------- + // Get All + //----------------- + } else if(operation === 'getAll') { + method = 'GET'; + if(resource === 'form') { + endpoint = '/forms'; + } else if(resource === 'tag') { + endpoint = '/tags'; + } else if(resource === 'sequence') { + endpoint = '/sequences'; + } + //-------------------- + // Get Subscriptions + //-------------------- + } else if(operation === 'getSubscriptions') { + const id = this.getNodeParameter('id', i); + const additionalParams = this.getNodeParameter('additionalFields', 0) as IDataObject; + if(additionalParams.subscriberState) { + qs.subscriber_state = additionalParams.subscriberState; + } + + method = 'GET'; + if(resource === 'form') { + endpoint = `/forms/${id}/subscriptions`; + } else if(resource === 'tag') { + endpoint = `/tags/${id}/subscriptions`; + } else if(resource === 'sequence') { + endpoint = `/sequences/${id}/subscriptions`; + } + //------------ + // Create Tag + //------------ + } else if(operation === 'create') { + const name = this.getNodeParameter('name', i); + qs.tag = { name, }; + + method = 'POST'; + endpoint = '/tags'; + //------------ + // Remove Tag + //------------ + } else if(operation === 'removeSubscriber') { + const id = this.getNodeParameter('id', i); + + qs.email = this.getNodeParameter('email', i); + + method = 'POST'; + endpoint = `/tags/${id}/unsubscribe`; + } else { + throw new Error(`The operation "${operation}" is not known!`); + } + } else { + throw new Error(`The resource "${resource}" is not known!`); + } + + responseData = await convertKitApiRequest.call(this, method, endpoint, {}, qs); + + if(fullOperation === 'field/getAll') { + responseData = responseData.custom_fields; + } else if(['form/addSubscriber', 'tag/addSubscriber', 'sequence/addSubscriber'].includes(fullOperation)) { + responseData = responseData.subscription; + } else if(fullOperation === 'form/getAll') { + responseData = responseData.forms; + } else if(['form/getSubscriptions', 'tag/getSubscriptions'].includes(fullOperation)) { + responseData = responseData.subscriptions; + } else if(fullOperation === 'tag/getAll') { + responseData = responseData.tags; + } else if(fullOperation === 'sequence/getAll') { + responseData = responseData.courses; + } + + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } else { + if(method === 'GET') { + returnData.push( { } ); + } else { + returnData.push( { success: true } ); + } + } + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts b/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts new file mode 100644 index 00000000000..5b1c6c22745 --- /dev/null +++ b/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts @@ -0,0 +1,163 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeTypeDescription, + INodeType, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + convertKitApiRequest, +} from './GenericFunctions'; + + +export class ConvertKitTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'ConvertKit Trigger', + name: 'convertKitTrigger', + icon: 'file:convertKit.png', + subtitle: '={{$parameter["event"]}}', + group: ['trigger'], + version: 1, + description: 'Handle ConvertKit events via webhooks', + defaults: { + name: 'ConvertKit Trigger', + color: '#fb6970', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'convertKitApi', + required: true, + } + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Event', + name: 'event', + type: 'options', + required: true, + default: 'subscriberActivated', + description: 'The events that can trigger the webhook and whether they are enabled.', + options: [ + { + name: 'Subscriber Activated', + value: 'subscriberActivated', + description: 'Whether the webhook is triggered when a subscriber is activated.', + }, + { + name: 'Link Clicked', + value: 'linkClicked', + description: 'Whether the webhook is triggered when a link is clicked.', + }, + ], + }, + { + displayName: 'Initiating Link', + name: 'link', + type: 'string', + required: true, + default: '', + description: 'The URL of the initiating link', + displayOptions: { + show: { + event: [ + 'linkClicked', + ], + }, + }, + }, + ], + }; + + // @ts-ignore (because of request) + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + + if(webhookData.webhookId) { + return true; + } + return false; + }, + + async create(this: IHookFunctions): Promise { + let webhook; + const webhookUrl = this.getNodeWebhookUrl('default'); + const event = this.getNodeParameter('event', 0); + const endpoint = '/automations/hooks'; + + const qs: IDataObject = {}; + + try { + qs.target_url = webhookUrl; + + if(event === 'subscriberActivated') { + qs.event = { + name: 'subscriber.subscriber_activate', + }; + } else if(event === 'linkClicked') { + const link = this.getNodeParameter('link', 0) as string; + qs.event = { + name: 'subscriber.link_click', + initiator_value: link, + }; + } + webhook = await convertKitApiRequest.call(this, 'POST', endpoint, {}, qs); + } catch (error) { + throw error; + } + + if (webhook.rule.id === undefined) { + return false; + } + + const webhookData = this.getWorkflowStaticData('node'); + webhookData.webhookId = webhook.rule.id as string; + webhookData.events = event; + return true; + }, + + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId !== undefined) { + const endpoint = `/automations/hooks/${webhookData.webhookId}`; + try { + await convertKitApiRequest.call(this, 'DELETE', endpoint, {}, {}); + } catch (error) { + return false; + } + delete webhookData.webhookId; + delete webhookData.events; + } + return true; + }, + }, + }; + + + async webhook(this: IWebhookFunctions): Promise { + const returnData: IDataObject[] = []; + returnData.push(this.getBodyData()); + + return { + workflowData: [ + this.helpers.returnJsonArray(returnData), + ], + }; + } +} diff --git a/packages/nodes-base/nodes/ConvertKit/FieldDescription.ts b/packages/nodes-base/nodes/ConvertKit/FieldDescription.ts new file mode 100644 index 00000000000..8966e60efa7 --- /dev/null +++ b/packages/nodes-base/nodes/ConvertKit/FieldDescription.ts @@ -0,0 +1,83 @@ +import { + INodeProperties +} from 'n8n-workflow'; + +export const fieldOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'field', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a field.', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a field.', + }, + { + name: 'Get All', + value: 'getAll', + description: `List all of your account's custom fields.`, + }, + { + name: 'Update', + value: 'update', + description: 'Update a field.', + }, + ], + default: 'update', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const fieldFields = [ + { + displayName: 'Field ID', + name: 'id', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'field', + ], + operation: [ + 'update', + 'delete', + ], + }, + }, + default: '', + description: 'The ID of your custom field.', + }, + { + displayName: 'Label', + name: 'label', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'field', + ], + operation: [ + 'update', + 'create', + ], + }, + }, + default: '', + description: 'The label of the custom field.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/ConvertKit/FormDescription.ts b/packages/nodes-base/nodes/ConvertKit/FormDescription.ts new file mode 100644 index 00000000000..3e266d18a5d --- /dev/null +++ b/packages/nodes-base/nodes/ConvertKit/FormDescription.ts @@ -0,0 +1,174 @@ +import { + INodeProperties +} from 'n8n-workflow'; + +export const formOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'form', + ], + }, + }, + options: [ + { + name: 'Add Subscriber', + value: 'addSubscriber', + description: 'Add a subscriber.', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get a list of all the forms for your account.', + }, + { + name: 'Get Subscriptions', + value: 'getSubscriptions', + description: 'List subscriptions to a form including subscriber data.', + }, + ], + default: 'addSubscriber', + description: 'The operations to perform.', + }, +] as INodeProperties[]; + +export const formFields = [ + { + displayName: 'Email Address', + name: 'email', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'form', + ], + operation: [ + 'addSubscriber', + ], + }, + }, + default: '', + description: `The subscriber's email address.`, + }, + { + displayName: 'Form ID', + name: 'id', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'form', + ], + operation: [ + 'addSubscriber', + 'getSubscriptions', + ], + }, + }, + default: '', + description: 'Form ID.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'form', + ], + operation: [ + 'addSubscriber', + ], + }, + }, + options: [ + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: `The subscriber's first name.`, + }, + { + displayName: 'Custom Fields', + name: 'fields', + placeholder: 'Add Custom Field', + description: 'Object of key/value pairs for custom fields (the custom field must exist before you can use it here).', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'field', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field Key', + name: 'key', + type: 'string', + default: '', + placeholder: 'last_name', + description: `The field's key.`, + }, + { + displayName: 'Field Value', + name: 'value', + type: 'string', + default: '', + placeholder: 'Doe', + description: 'Value of the field.', + }, + ], + }, + ], + }, + ], + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'form', + ], + operation: [ + 'getSubscriptions', + ], + }, + }, + options: [ + { + displayName: 'Subscriber State', + name: 'subscriberState', + type: 'options', + options: [ + { + name: 'Active', + value: 'active', + }, + { + name: 'Cancelled', + value: 'cancelled', + }, + ], + default: 'active', + }, + ], + description: 'Receive only active subscribers or cancelled subscribers.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/ConvertKit/GenericFunctions.ts b/packages/nodes-base/nodes/ConvertKit/GenericFunctions.ts new file mode 100644 index 00000000000..525aed043a6 --- /dev/null +++ b/packages/nodes-base/nodes/ConvertKit/GenericFunctions.ts @@ -0,0 +1,44 @@ +import { + OptionsWithUri +} from 'request'; + +import { + IExecuteFunctions, + ILoadOptionsFunctions, + IExecuteSingleFunctions, +} from 'n8n-core'; + +import { + IDataObject, + IHookFunctions +} from 'n8n-workflow'; + +export async function convertKitApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IHookFunctions, + method: string, endpoint: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('convertKitApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + let options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + qs, + body, + uri: uri ||`https://api.convertkit.com/v3${endpoint}`, + json: true, + }; + options = Object.assign({}, options, option); + if (Object.keys(options.body).length === 0) { + delete options.body; + } + try { + qs.api_secret = credentials.apiSecret; + + return await this.helpers.request!(options); + } catch (error) { + throw new Error(`ConvertKit error response: ${error.message}`); + } +} diff --git a/packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts b/packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts new file mode 100644 index 00000000000..1b82b741fa2 --- /dev/null +++ b/packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts @@ -0,0 +1,174 @@ +import { + INodeProperties +} from 'n8n-workflow'; + +export const sequenceOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'sequence', + ], + }, + }, + options: [ + { + name: 'Add Subscriber', + value: 'addSubscriber', + description: 'Add a subscriber.', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Returns a list of sequences for the account.', + }, + { + name: 'Get Subscriptions', + value: 'getSubscriptions', + description: 'List subscriptions to a sequence including subscriber data.', + }, + ], + default: 'addSubscriber', + description: 'The operations to perform.', + }, +] as INodeProperties[]; + +export const sequenceFields = [ + { + displayName: 'Email Address', + name: 'email', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'sequence', + ], + operation: [ + 'addSubscriber', + ], + }, + }, + default: '', + description: `The subscriber's email address.`, + }, + { + displayName: 'Sequence ID', + name: 'id', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'sequence', + ], + operation: [ + 'addSubscriber', + 'getSubscriptions', + ], + }, + }, + default: '', + description: 'Sequence ID.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'sequence', + ], + operation: [ + 'addSubscriber', + ], + }, + }, + options: [ + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: `The subscriber's first name.`, + }, + { + displayName: 'Custom Fields', + name: 'fields', + placeholder: 'Add Custom Field', + description: 'Object of key/value pairs for custom fields (the custom field must exist before you can use it here).', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'field', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field Key', + name: 'key', + type: 'string', + default: '', + placeholder: 'last_name', + description: `The field's key.`, + }, + { + displayName: 'Field Value', + name: 'value', + type: 'string', + default: '', + placeholder: 'Doe', + description: 'Value of the field.', + }, + ], + }, + ], + }, + ], + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'sequence', + ], + operation: [ + 'getSubscriptions', + ], + }, + }, + options: [ + { + displayName: 'Subscriber State', + name: 'subscriberState', + type: 'options', + options: [ + { + name: 'Active', + value: 'active', + }, + { + name: 'Cancelled', + value: 'cancelled', + }, + ], + default: 'active', + }, + ], + description: 'Receive only active subscribers or cancelled subscribers.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/ConvertKit/TagDescription.ts b/packages/nodes-base/nodes/ConvertKit/TagDescription.ts new file mode 100644 index 00000000000..a1a718ba3cd --- /dev/null +++ b/packages/nodes-base/nodes/ConvertKit/TagDescription.ts @@ -0,0 +1,204 @@ +import { + INodeProperties +} from 'n8n-workflow'; + +export const tagOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'tag', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a tag.', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Returns a list of tags for the account.', + }, + { + name: 'Get Subscriptions', + value: 'getSubscriptions', + description: 'List subscriptions to a tag including subscriber data.', + }, + { + name: 'Remove Subscriber', + value: 'removeSubscriber', + description: 'Remove a tag from a subscriber.', + }, + { + name: 'Add Subscriber', + value: 'addSubscriber', + description: 'Add a tag to a subscriber.', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const tagFields = [ + { + displayName: 'Name', + name: 'name', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'tag', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: 'Tag name.', + }, + { + displayName: 'Email Address', + name: 'email', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'tag', + ], + operation: [ + 'addSubscriber', + 'removeSubscriber', + ], + }, + }, + default: '', + description: 'Subscriber email address.', + }, + { + displayName: 'Tag ID', + name: 'id', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'tag', + ], + operation: [ + 'addSubscriber', + 'removeSubscriber', + 'getSubscriptions', + ], + }, + }, + default: '', + description: 'Tag ID.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'tag', + ], + operation: [ + 'addSubscriber', + ], + }, + }, + options: [ + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: 'Subscriber first name.', + }, + { + displayName: 'Custom Fields', + name: 'fields', + placeholder: 'Add Custom Field', + description: 'Object of key/value pairs for custom fields (the custom field must exist before you can use it here).', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'field', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field Key', + name: 'key', + type: 'string', + default: '', + placeholder: 'last_name', + description: `The field's key.`, + }, + { + displayName: 'Field Value', + name: 'value', + type: 'string', + default: '', + placeholder: 'Doe', + description: 'Value of the field.', + }, + ], + }, + ], + }, + ], + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'tag', + ], + operation: [ + 'getSubscriptions', + ], + }, + }, + options: [ + { + displayName: 'Subscriber State', + name: 'subscriberState', + type: 'options', + options: [ + { + name: 'Active', + value: 'active', + }, + { + name: 'Cancelled', + value: 'cancelled', + }, + ], + default: 'active', + }, + ], + description: 'Receive only active subscribers or cancelled subscribers.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/ConvertKit/convertKit.png b/packages/nodes-base/nodes/ConvertKit/convertKit.png new file mode 100644 index 0000000000000000000000000000000000000000..2dc9cf3ee52a2d64f636c43724bc9d1b81cb6372 GIT binary patch literal 4958 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 Date: Thu, 9 Jul 2020 11:36:28 +0200 Subject: [PATCH 02/39] Contentful integration --- .../ContentfulDeliveryApi.credentials.ts | 22 +++ .../nodes/Contentful/ GenericFunctions.ts | 34 ++++ .../nodes/Contentful/AssetDescription.ts | 49 ++++++ .../Contentful/ContentTypeDescription.ts | 49 ++++++ .../nodes/Contentful/Contentful.node.ts | 142 ++++++++++++++++ .../nodes/Contentful/EntryDescription.ts | 83 ++++++++++ .../nodes/Contentful/LocaleDescription.ts | 29 ++++ .../Contentful/SearchParameterDescription.ts | 37 +++++ .../nodes/Contentful/SpaceDescription.ts | 29 ++++ .../nodes/Contentful/contentful.png | Bin 0 -> 4281 bytes .../nodes/Contentful/resolveResponse.ts | 156 ++++++++++++++++++ packages/nodes-base/package.json | 2 + 12 files changed, 632 insertions(+) create mode 100644 packages/nodes-base/credentials/ContentfulDeliveryApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Contentful/ GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Contentful/AssetDescription.ts create mode 100644 packages/nodes-base/nodes/Contentful/ContentTypeDescription.ts create mode 100644 packages/nodes-base/nodes/Contentful/Contentful.node.ts create mode 100644 packages/nodes-base/nodes/Contentful/EntryDescription.ts create mode 100644 packages/nodes-base/nodes/Contentful/LocaleDescription.ts create mode 100644 packages/nodes-base/nodes/Contentful/SearchParameterDescription.ts create mode 100644 packages/nodes-base/nodes/Contentful/SpaceDescription.ts create mode 100644 packages/nodes-base/nodes/Contentful/contentful.png create mode 100644 packages/nodes-base/nodes/Contentful/resolveResponse.ts diff --git a/packages/nodes-base/credentials/ContentfulDeliveryApi.credentials.ts b/packages/nodes-base/credentials/ContentfulDeliveryApi.credentials.ts new file mode 100644 index 00000000000..ce620fcdbb1 --- /dev/null +++ b/packages/nodes-base/credentials/ContentfulDeliveryApi.credentials.ts @@ -0,0 +1,22 @@ +import { ICredentialType, NodePropertyTypes } from 'n8n-workflow'; + +export class ContentfulDeliveryApi implements ICredentialType { + name = 'contentfulDeliveryApi'; + displayName = 'Delivery API'; + properties = [ + { + displayName: 'Space Id', + name: 'space_id', + type: 'string' as NodePropertyTypes, + default: '', + description: 'The id for the Cotentful space.' + }, + { + displayName: 'Access Token', + name: 'access_token', + type: 'string' as NodePropertyTypes, + default: '', + description: 'Access token that has access to the space' + } + ]; +} diff --git a/packages/nodes-base/nodes/Contentful/ GenericFunctions.ts b/packages/nodes-base/nodes/Contentful/ GenericFunctions.ts new file mode 100644 index 00000000000..c59bab1449c --- /dev/null +++ b/packages/nodes-base/nodes/Contentful/ GenericFunctions.ts @@ -0,0 +1,34 @@ +import { IExecuteFunctions } from 'n8n-core'; +import { OptionsWithUrl } from 'request'; + +/** + * @param {IExecuteFunctions} that Reference to the system's execute functions + * @param {string} endpoint? Endpoint of api call + * @param {string} environmentId? Id of contentful environment (eg. master, staging, etc.) + * @param {Record} qs? Query string, can be used for search parameters + */ +export const contentfulApiRequest = async ( + that: IExecuteFunctions, + endpoint?: string, + environmentId?: string, + qs?: Record +) => { + const credentials = that.getCredentials('contentfulDeliveryApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + let url = `https://cdn.contentful.com/spaces/${credentials.space_id}`; + if (environmentId) url = `${url}/environments/${environmentId}`; + if (endpoint) url = `${url}${endpoint}`; + qs = qs || {}; + qs.access_token = credentials.access_token as string; + + const res = await that.helpers.request!({ + url, + method: 'GET', + qs + } as OptionsWithUrl); + + return JSON.parse(res); +}; diff --git a/packages/nodes-base/nodes/Contentful/AssetDescription.ts b/packages/nodes-base/nodes/Contentful/AssetDescription.ts new file mode 100644 index 00000000000..18ccba8257d --- /dev/null +++ b/packages/nodes-base/nodes/Contentful/AssetDescription.ts @@ -0,0 +1,49 @@ +import { INodeProperties, INodePropertyOptions } from "n8n-workflow"; + +export const resource = { + name: "Asset", + value: "asset", +} as INodePropertyOptions; + +export const operations = [ + { + displayName: "Operation", + name: "operation", + type: "options", + displayOptions: { + show: { + resource: [resource.value], + }, + }, + options: [ + { + name: "Get Assets", + value: "get_assets", + }, + { + name: "Get Single Asset", + value: "get_asset", + }, + ], + default: "get_assets", + description: "The operation to perform.", + }, +] as INodeProperties[]; + +export const fields = [ + { + displayName: "Asset Id", + name: "asset_id", + type: "string", + default: "", + placeholder: "", + description: "", + required: true, + displayOptions: { + show: { + resource: [resource.value], + operation: ["get_asset"], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Contentful/ContentTypeDescription.ts b/packages/nodes-base/nodes/Contentful/ContentTypeDescription.ts new file mode 100644 index 00000000000..0a2ebfd1e78 --- /dev/null +++ b/packages/nodes-base/nodes/Contentful/ContentTypeDescription.ts @@ -0,0 +1,49 @@ +import { INodeProperties, INodePropertyOptions } from "n8n-workflow"; + +export const resource = { + name: "Content Types", + value: "content_type", +} as INodePropertyOptions; + +export const operations = [ + { + displayName: "Operation", + name: "operation", + type: "options", + displayOptions: { + show: { + resource: [resource.value], + }, + }, + options: [ + { + name: "Get Content types", + value: "get_content_types", + }, + { + name: "Get Single Content Type", + value: "get_content_type", + }, + ], + default: "get_content_types", + description: "The operation to perform.", + }, +] as INodeProperties[]; + +export const fields = [ + { + displayName: "Content Type Id", + name: "content_type_id", + type: "string", + default: "", + placeholder: "", + description: "", + required: true, + displayOptions: { + show: { + resource: [resource.value], + operation: ["get_content_type"], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Contentful/Contentful.node.ts b/packages/nodes-base/nodes/Contentful/Contentful.node.ts new file mode 100644 index 00000000000..866cddb268b --- /dev/null +++ b/packages/nodes-base/nodes/Contentful/Contentful.node.ts @@ -0,0 +1,142 @@ +import { IExecuteFunctions } from 'n8n-core'; +import { IDataObject, INodeExecutionData, INodeType, INodeTypeDescription, NodePropertyTypes } from 'n8n-workflow'; + +import { contentfulApiRequest } from './ GenericFunctions'; +import resolveResponse from './resolveResponse'; + +import * as SpaceDescription from './SpaceDescription'; +import * as ContentTypeDescription from './ContentTypeDescription'; +import * as EntryDescription from './EntryDescription'; +import * as AssetDescription from './AssetDescription'; +import * as LocaleDescription from './LocaleDescription'; +import * as SearchParameterDescription from './SearchParameterDescription'; + +export class Contentful implements INodeType { + description: INodeTypeDescription = { + displayName: 'Contentful', + name: 'contentful', + icon: 'file:contentful.png', + group: ['input'], + version: 1, + description: "Access data through Contentful's Content Delivery API", + defaults: { + name: 'Contentful', + color: '#2E75D4' + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'contentfulDeliveryApi', + required: true + } + ], + properties: [ + // Common fields: + { + displayName: 'Environment Id', + name: 'environment_id', + type: 'string' as NodePropertyTypes, + default: 'master', + description: + 'The id for the Contentful environment (e.g. master, staging, etc.). Depending on your plan, you might not have environments. In that case use "master".' + }, + + // Resources: + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + SpaceDescription.resource, + ContentTypeDescription.resource, + EntryDescription.resource, + AssetDescription.resource, + LocaleDescription.resource + ], + default: '', + description: 'The resource to operate on.' + }, + + // Operations: + ...SpaceDescription.operations, + ...ContentTypeDescription.operations, + ...EntryDescription.operations, + ...AssetDescription.operations, + ...LocaleDescription.operations, + + // Resource specific fields: + ...SpaceDescription.fields, + ...ContentTypeDescription.fields, + ...EntryDescription.fields, + ...AssetDescription.fields, + ...LocaleDescription.fields, + + // Options: + ...SearchParameterDescription.fields + ] + }; + + async execute(this: IExecuteFunctions): Promise { + const environmentId = this.getNodeParameter('environment_id', 0) as string; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const qs: Record = {}; + + for (let i = 0; i < items.length; i++) { + if (resource === 'space') { + if (operation === 'get_space') { + const res = await contentfulApiRequest(this); + returnData.push(res); + } + } else if (resource === 'content_type') { + if (operation === 'get_content_types') { + const res = await contentfulApiRequest(this, '/content_types', environmentId); + const resolvedData = resolveResponse(res, {}); + returnData.push(...resolvedData); + } else if (operation === 'get_content_type') { + const id = this.getNodeParameter('content_type_id', 0) as string; + const res = await contentfulApiRequest(this, `/content_types/${id}`, environmentId); + returnData.push(...res.items); + } + } else if (resource === 'entry') { + if (operation === 'get_entries') { + const shouldResolve = this.getNodeParameter('resolve', 0) as boolean; + if (shouldResolve) qs.include = this.getNodeParameter('include', 0) as number; + const searchParameters = this.getNodeParameter('search_parameters', 0) as IDataObject; + if (searchParameters.parameters && Array.isArray(searchParameters.parameters)) { + searchParameters.parameters.forEach(parameter => { + const { name, value } = parameter as { name: string; value: string }; + qs[name] = value; + }); + } + const res = await contentfulApiRequest(this, '/entries', environmentId, qs); + const resolvedData = shouldResolve ? resolveResponse(res, {}) : res.items; + returnData.push(...resolvedData); + } else if (operation === 'get_entry') { + const id = this.getNodeParameter('entry_id', 0) as string; + const res = await contentfulApiRequest(this, `/entries/${id}`, environmentId); + returnData.push(res); + } + } else if (resource === 'asset') { + if (operation === 'get_assets') { + const res = await contentfulApiRequest(this, '/assets', environmentId); + returnData.push(...res.items); + } else if (operation === 'get_asset') { + const id = this.getNodeParameter('asset_id', 0) as string; + const res = await contentfulApiRequest(this, `/assets/${id}`, environmentId); + returnData.push(res); + } + } else if (resource === 'locale') { + if (operation === 'get_locales') { + const res = await contentfulApiRequest(this, '/locales', environmentId); + returnData.push(res); + } + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Contentful/EntryDescription.ts b/packages/nodes-base/nodes/Contentful/EntryDescription.ts new file mode 100644 index 00000000000..781bcc6fe1a --- /dev/null +++ b/packages/nodes-base/nodes/Contentful/EntryDescription.ts @@ -0,0 +1,83 @@ +import { INodeProperties, INodePropertyOptions } from 'n8n-workflow'; + +export const resource = { + name: 'Entry', + value: 'entry' +} as INodePropertyOptions; + +export const operations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [resource.value] + } + }, + options: [ + { + name: 'Get Entries', + value: 'get_entries' + }, + { + name: 'Get Single Entry', + value: 'get_entry' + } + ], + default: 'get_entries', + description: 'The operation to perform.' + } +] as INodeProperties[]; + +export const fields = [ + { + displayName: 'Resolve', + name: 'resolve', + type: 'boolean', + default: false, + description: 'Linked entries can be automatically resolved in the results if you click activate this feature.', + displayOptions: { + show: { + resource: [resource.value], + operation: ['get_entries'] + } + } + }, + { + displayName: 'Include', + name: 'include', + type: 'number', + default: 1, + placeholder: '', + description: + "When you have related content (e.g. entries with links to image assets) it's possible to include them in the results. Using the include parameter, you can specify the number of levels of entries to include in the results. A lower number might improve performance.", + typeOptions: { + minValue: 0, + maxValue: 10 + }, + displayOptions: { + show: { + resource: [resource.value], + operation: ['get_entries'], + resolve: [true] + } + } + }, + + { + displayName: 'Entry Id', + name: 'entry_id', + type: 'string', + default: '', + placeholder: '', + description: '', + required: true, + displayOptions: { + show: { + resource: [resource.value], + operation: ['get_entry'] + } + } + } +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Contentful/LocaleDescription.ts b/packages/nodes-base/nodes/Contentful/LocaleDescription.ts new file mode 100644 index 00000000000..87e7f6b9320 --- /dev/null +++ b/packages/nodes-base/nodes/Contentful/LocaleDescription.ts @@ -0,0 +1,29 @@ +import { INodeProperties, INodePropertyOptions } from "n8n-workflow"; + +export const resource = { + name: "Locale", + value: "locale", +} as INodePropertyOptions; + +export const operations = [ + { + displayName: "Operation", + name: "operation", + type: "options", + displayOptions: { + show: { + resource: [resource.value], + }, + }, + options: [ + { + name: "Get Locales", + value: "get_locales", + }, + ], + default: "get_locales", + description: "The operation to perform.", + }, +] as INodeProperties[]; + +export const fields = [] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Contentful/SearchParameterDescription.ts b/packages/nodes-base/nodes/Contentful/SearchParameterDescription.ts new file mode 100644 index 00000000000..7557cbb7552 --- /dev/null +++ b/packages/nodes-base/nodes/Contentful/SearchParameterDescription.ts @@ -0,0 +1,37 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const fields = [ + { + displayName: 'Search Parameters', + name: 'search_parameters', + description: 'You can use a variety of query parameters to search and filter items.', + placeholder: 'Add parameter', + type: 'fixedCollection', + typeOptions: { + multipleValues: true + }, + default: {}, + options: [ + { + displayName: 'Parameters', + name: 'parameters', + values: [ + { + displayName: 'Parameter Name', + name: 'name', + type: 'string', + default: '', + description: 'Name of the search parameter to set.' + }, + { + displayName: 'Parameter Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the search parameter to set.' + } + ] + } + ] + } +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Contentful/SpaceDescription.ts b/packages/nodes-base/nodes/Contentful/SpaceDescription.ts new file mode 100644 index 00000000000..2baabee0993 --- /dev/null +++ b/packages/nodes-base/nodes/Contentful/SpaceDescription.ts @@ -0,0 +1,29 @@ +import { INodeProperties } from "n8n-workflow"; + +export const resource = { + name: "Space", + value: "space", +}; + +export const operations = [ + { + displayName: "Operation", + name: "operation", + type: "options", + displayOptions: { + show: { + resource: [resource.value], + }, + }, + options: [ + { + name: "Get Space", + value: "get_space", + }, + ], + default: "get_space", + description: "The operation to perform.", + }, +] as INodeProperties[]; + +export const fields = [] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Contentful/contentful.png b/packages/nodes-base/nodes/Contentful/contentful.png new file mode 100644 index 0000000000000000000000000000000000000000..4448da65ae1c71a2243e4fd6545568a6b1d54358 GIT binary patch literal 4281 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 PivW< + object && object.sys && object.sys.type === 'Link'; + +/** + * findNormalizableLinkInArray + * + * @param array + * @param predicate + * @return {*} + */ +const findNormalizableLinkInArray = (array, predicate) => { + for (let i = 0, len = array.length; i < len; i++) { + if (predicate(array[i])) { + return array[i]; + } + } + return UNRESOLVED_LINK; +}; + +/** + * getLink Function + * + * @param response + * @param link + * @return {undefined} + */ +const getLink = (allEntries, link) => { + const { linkType: type, id } = link.sys; + + const predicate = ({ sys }) => sys.type === type && sys.id === id; + + return findNormalizableLinkInArray(allEntries, predicate); +}; + +/** + * cleanUpLinks Function + * - Removes unresolvable links from Arrays and Objects + * + * @param {Object[]|Object} input + */ +const cleanUpLinks = input => { + if (Array.isArray(input)) { + return input.filter(val => val !== UNRESOLVED_LINK); + } + for (const key in input) { + if (input[key] === UNRESOLVED_LINK) { + delete input[key]; + } + } + return input; +}; + +/** + * walkMutate Function + * @param input + * @param predicate + * @param mutator + * @return {*} + */ +const walkMutate = (input, predicate, mutator, removeUnresolved) => { + if (predicate(input)) { + return mutator(input); + } + + if (input && typeof input === 'object') { + for (const key in input) { + if (input.hasOwnProperty(key)) { + input[key] = walkMutate( + input[key], + predicate, + mutator, + removeUnresolved + ); + } + } + if (removeUnresolved) { + input = cleanUpLinks(input); + } + } + return input; +}; + +const normalizeLink = (allEntries, link, removeUnresolved) => { + const resolvedLink = getLink(allEntries, link); + if (resolvedLink === UNRESOLVED_LINK) { + return removeUnresolved ? resolvedLink : link; + } + return resolvedLink; +}; + +const makeEntryObject = (item, itemEntryPoints) => { + if (!Array.isArray(itemEntryPoints)) { + return item; + } + + const entryPoints = Object.keys(item).filter( + ownKey => itemEntryPoints.indexOf(ownKey) !== -1 + ); + + return entryPoints.reduce((entryObj, entryPoint) => { + entryObj[entryPoint] = item[entryPoint]; + return entryObj; + }, {}); +}; + +/** + * resolveResponse Function + * Resolves contentful response to normalized form. + * @param {Object} response Contentful response + * @param {Object} options + * @param {Boolean} options.removeUnresolved - Remove unresolved links default:false + * @param {Array} options.itemEntryPoints - Resolve links only in those item properties + * @return {Object} + */ +const resolveResponse = (response, options) => { + options = options || {}; + if (!response.items) { + return []; + } + const responseClone = cloneDeep(response); + const allIncludes = Object.keys(responseClone.includes || {}).reduce( + (all, type) => [...all, ...response.includes[type]], + [] + ); + + const allEntries = [...responseClone.items, ...allIncludes]; + + allEntries.forEach(item => { + const entryObject = makeEntryObject(item, options.itemEntryPoints); + + Object.assign( + item, + walkMutate( + entryObject, + isLink, + link => normalizeLink(allEntries, link, options.removeUnresolved), + options.removeUnresolved + ) + ); + }); + + return responseClone.items; +}; + +export default resolveResponse; diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 37b9adecf23..45b703b9d89 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -43,6 +43,7 @@ "dist/credentials/ClockifyApi.credentials.js", "dist/credentials/CockpitApi.credentials.js", "dist/credentials/CodaApi.credentials.js", + "dist/credentials/ContentfulDeliveryApi.credentials.js", "dist/credentials/CopperApi.credentials.js", "dist/credentials/CalendlyApi.credentials.js", "dist/credentials/DisqusApi.credentials.js", @@ -159,6 +160,7 @@ "dist/nodes/Clockify/ClockifyTrigger.node.js", "dist/nodes/Cockpit/Cockpit.node.js", "dist/nodes/Coda/Coda.node.js", + "dist/nodes/Contentful/Contentful.node.js", "dist/nodes/Copper/CopperTrigger.node.js", "dist/nodes/Cron.node.js", "dist/nodes/Crypto.node.js", From 1a785581ffd100853567c12fb5a1e376c075af2e Mon Sep 17 00:00:00 2001 From: Sven Schmidt <0xsven@gmail.com> Date: Thu, 9 Jul 2020 12:10:22 +0200 Subject: [PATCH 03/39] Handle preview api --- .../ContentfulDeliveryApi.credentials.ts | 11 ++++++++++- .../nodes/Contentful/ GenericFunctions.ts | 15 +++++++++++++-- .../nodes/Contentful/Contentful.node.ts | 19 ++++++++++++++++++- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/packages/nodes-base/credentials/ContentfulDeliveryApi.credentials.ts b/packages/nodes-base/credentials/ContentfulDeliveryApi.credentials.ts index ce620fcdbb1..d9078f0cc10 100644 --- a/packages/nodes-base/credentials/ContentfulDeliveryApi.credentials.ts +++ b/packages/nodes-base/credentials/ContentfulDeliveryApi.credentials.ts @@ -9,13 +9,22 @@ export class ContentfulDeliveryApi implements ICredentialType { name: 'space_id', type: 'string' as NodePropertyTypes, default: '', + required: true, description: 'The id for the Cotentful space.' }, { - displayName: 'Access Token', + displayName: 'Content Delivery API - access token', name: 'access_token', type: 'string' as NodePropertyTypes, default: '', + required: true, + description: 'Access token that has access to the space' + }, + { + displayName: 'Content Preview API - access token', + name: 'access_token_preview', + type: 'string' as NodePropertyTypes, + default: '', description: 'Access token that has access to the space' } ]; diff --git a/packages/nodes-base/nodes/Contentful/ GenericFunctions.ts b/packages/nodes-base/nodes/Contentful/ GenericFunctions.ts index c59bab1449c..246c43c220d 100644 --- a/packages/nodes-base/nodes/Contentful/ GenericFunctions.ts +++ b/packages/nodes-base/nodes/Contentful/ GenericFunctions.ts @@ -18,11 +18,22 @@ export const contentfulApiRequest = async ( throw new Error('No credentials got returned!'); } - let url = `https://cdn.contentful.com/spaces/${credentials.space_id}`; + const source = that.getNodeParameter('source', 0) as string; + const isPreview = source === 'preview_api'; + let accessToken = credentials.access_token as string; + if (isPreview) { + accessToken = credentials.access_token_preview as string; + console.log('accessToken', accessToken); + if (!accessToken) { + throw new Error('No access token for preview API set in credentials!'); + } + } + + let url = `https://${isPreview ? 'preview' : 'cdn'}.contentful.com/spaces/${credentials.space_id}`; if (environmentId) url = `${url}/environments/${environmentId}`; if (endpoint) url = `${url}${endpoint}`; qs = qs || {}; - qs.access_token = credentials.access_token as string; + qs.access_token = accessToken; const res = await that.helpers.request!({ url, diff --git a/packages/nodes-base/nodes/Contentful/Contentful.node.ts b/packages/nodes-base/nodes/Contentful/Contentful.node.ts index 866cddb268b..e16fb51f29a 100644 --- a/packages/nodes-base/nodes/Contentful/Contentful.node.ts +++ b/packages/nodes-base/nodes/Contentful/Contentful.node.ts @@ -33,11 +33,28 @@ export class Contentful implements INodeType { ], properties: [ // Common fields: + { + displayName: 'Source', + name: 'source', + type: 'options' as NodePropertyTypes, + default: 'Delivery API', + description: 'Pick where your data comes from, delivery or preview API', + options: [ + { + name: 'Delivery API', + value: 'delivery_api' + }, + { + name: 'Preview API', + value: 'preview_api' + } + ] + }, { displayName: 'Environment Id', name: 'environment_id', type: 'string' as NodePropertyTypes, - default: 'master', + default: '', description: 'The id for the Contentful environment (e.g. master, staging, etc.). Depending on your plan, you might not have environments. In that case use "master".' }, From 784cafffa077dce01611f2d5ea508bf0c83f0a0a Mon Sep 17 00:00:00 2001 From: Sven Schmidt <0xsven@gmail.com> Date: Thu, 9 Jul 2020 12:17:19 +0200 Subject: [PATCH 04/39] Renaming a file --- packages/nodes-base/nodes/Contentful/Contentful.node.ts | 2 +- .../Contentful/{ GenericFunctions.ts => GenericFunctions.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/nodes-base/nodes/Contentful/{ GenericFunctions.ts => GenericFunctions.ts} (100%) diff --git a/packages/nodes-base/nodes/Contentful/Contentful.node.ts b/packages/nodes-base/nodes/Contentful/Contentful.node.ts index e16fb51f29a..930534e5f7c 100644 --- a/packages/nodes-base/nodes/Contentful/Contentful.node.ts +++ b/packages/nodes-base/nodes/Contentful/Contentful.node.ts @@ -1,7 +1,7 @@ import { IExecuteFunctions } from 'n8n-core'; import { IDataObject, INodeExecutionData, INodeType, INodeTypeDescription, NodePropertyTypes } from 'n8n-workflow'; -import { contentfulApiRequest } from './ GenericFunctions'; +import { contentfulApiRequest } from './GenericFunctions'; import resolveResponse from './resolveResponse'; import * as SpaceDescription from './SpaceDescription'; diff --git a/packages/nodes-base/nodes/Contentful/ GenericFunctions.ts b/packages/nodes-base/nodes/Contentful/GenericFunctions.ts similarity index 100% rename from packages/nodes-base/nodes/Contentful/ GenericFunctions.ts rename to packages/nodes-base/nodes/Contentful/GenericFunctions.ts From 6573642fc1e7c27b031c34af0fda4ecba1c722ec Mon Sep 17 00:00:00 2001 From: Sven Schmidt <0xsven@gmail.com> Date: Thu, 9 Jul 2020 16:29:06 +0200 Subject: [PATCH 05/39] Fixing a typo --- .../nodes-base/credentials/ContentfulDeliveryApi.credentials.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/credentials/ContentfulDeliveryApi.credentials.ts b/packages/nodes-base/credentials/ContentfulDeliveryApi.credentials.ts index d9078f0cc10..c8db4ada741 100644 --- a/packages/nodes-base/credentials/ContentfulDeliveryApi.credentials.ts +++ b/packages/nodes-base/credentials/ContentfulDeliveryApi.credentials.ts @@ -10,7 +10,7 @@ export class ContentfulDeliveryApi implements ICredentialType { type: 'string' as NodePropertyTypes, default: '', required: true, - description: 'The id for the Cotentful space.' + description: 'The id for the Contentful space.' }, { displayName: 'Content Delivery API - access token', From bc0b349faa91cc92ac2ae50e08e3db8bffcdfb8a Mon Sep 17 00:00:00 2001 From: ricardo Date: Tue, 4 Aug 2020 15:07:54 -0400 Subject: [PATCH 06/39] :zap: Improvements to Contenful-Node --- .../credentials/ContentfulApi.credentials.ts | 36 +++ .../ContentfulDeliveryApi.credentials.ts | 31 --- .../nodes/Contentful/AssetDescription.ts | 228 +++++++++++++--- .../Contentful/ContentTypeDescription.ts | 100 ++++--- .../nodes/Contentful/Contentful.node.ts | 251 +++++++++++++----- .../nodes/Contentful/EntryDescription.ts | 214 +++++++++++---- .../nodes/Contentful/GenericFunctions.ts | 100 ++++--- .../nodes/Contentful/LocaleDescription.ts | 109 ++++++-- .../nodes/Contentful/SpaceDescription.ts | 46 ++-- .../nodes/Contentful/resolveResponse.ts | 156 ----------- packages/nodes-base/package.json | 2 +- 11 files changed, 810 insertions(+), 463 deletions(-) create mode 100644 packages/nodes-base/credentials/ContentfulApi.credentials.ts delete mode 100644 packages/nodes-base/credentials/ContentfulDeliveryApi.credentials.ts delete mode 100644 packages/nodes-base/nodes/Contentful/resolveResponse.ts diff --git a/packages/nodes-base/credentials/ContentfulApi.credentials.ts b/packages/nodes-base/credentials/ContentfulApi.credentials.ts new file mode 100644 index 00000000000..d08b97e647d --- /dev/null +++ b/packages/nodes-base/credentials/ContentfulApi.credentials.ts @@ -0,0 +1,36 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +//https://www.contentful.com/developers/docs/references/authentication/ +export class ContentfulApi implements ICredentialType { + name = 'contentfulApi'; + displayName = 'Contenful API'; + properties = [ + { + displayName: 'Space ID', + name: 'spaceId', + type: 'string' as NodePropertyTypes, + default: '', + required: true, + description: 'The id for the Contentful space.' + }, + { + displayName: 'Content Delivery API Access token', + name: 'ContentDeliveryaccessToken', + type: 'string' as NodePropertyTypes, + default: '', + required: true, + description: 'Access token that has access to the space' + }, + { + displayName: 'Content Preview API Access token', + name: 'ContentPreviewaccessToken', + type: 'string' as NodePropertyTypes, + default: '', + required: true, + description: 'Access token that has access to the space' + }, + ]; +} diff --git a/packages/nodes-base/credentials/ContentfulDeliveryApi.credentials.ts b/packages/nodes-base/credentials/ContentfulDeliveryApi.credentials.ts deleted file mode 100644 index c8db4ada741..00000000000 --- a/packages/nodes-base/credentials/ContentfulDeliveryApi.credentials.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ICredentialType, NodePropertyTypes } from 'n8n-workflow'; - -export class ContentfulDeliveryApi implements ICredentialType { - name = 'contentfulDeliveryApi'; - displayName = 'Delivery API'; - properties = [ - { - displayName: 'Space Id', - name: 'space_id', - type: 'string' as NodePropertyTypes, - default: '', - required: true, - description: 'The id for the Contentful space.' - }, - { - displayName: 'Content Delivery API - access token', - name: 'access_token', - type: 'string' as NodePropertyTypes, - default: '', - required: true, - description: 'Access token that has access to the space' - }, - { - displayName: 'Content Preview API - access token', - name: 'access_token_preview', - type: 'string' as NodePropertyTypes, - default: '', - description: 'Access token that has access to the space' - } - ]; -} diff --git a/packages/nodes-base/nodes/Contentful/AssetDescription.ts b/packages/nodes-base/nodes/Contentful/AssetDescription.ts index 18ccba8257d..dfd523806ea 100644 --- a/packages/nodes-base/nodes/Contentful/AssetDescription.ts +++ b/packages/nodes-base/nodes/Contentful/AssetDescription.ts @@ -1,49 +1,197 @@ -import { INodeProperties, INodePropertyOptions } from "n8n-workflow"; +import { + INodeProperties, + INodePropertyOptions, +} from 'n8n-workflow'; export const resource = { - name: "Asset", - value: "asset", + name: 'Asset', + value: 'asset', } as INodePropertyOptions; export const operations = [ - { - displayName: "Operation", - name: "operation", - type: "options", - displayOptions: { - show: { - resource: [resource.value], - }, - }, - options: [ - { - name: "Get Assets", - value: "get_assets", - }, - { - name: "Get Single Asset", - value: "get_asset", - }, - ], - default: "get_assets", - description: "The operation to perform.", - }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + resource.value, + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + ], + default: 'getAll', + description: 'The operation to perform.', + }, ] as INodeProperties[]; export const fields = [ - { - displayName: "Asset Id", - name: "asset_id", - type: "string", - default: "", - placeholder: "", - description: "", - required: true, - displayOptions: { - show: { - resource: [resource.value], - operation: ["get_asset"], - }, - }, - }, + { + displayName: 'Environment ID', + name: 'environmentId', + type: 'string', + displayOptions: { + show: { + resource: [ + resource.value, + ], + operation: [ + 'get', + 'getAll', + ], + }, + }, + default: 'master', + description: 'The id for the Contentful environment (e.g. master, staging, etc.). Depending on your plan, you might not have environments. In that case use "master".' + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + resource.value, + ], + }, + }, + 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: [ + resource.value, + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + resource.value, + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Equal', + name: 'equal', + type: 'string', + default: '', + placeholder: 'fields.title=n8n', + description: 'Search for all data that matches the condition: {attribute}={value}. Attribute can use dot notation.', + }, + { + displayName: 'Exclude', + name: 'exclude', + type: 'string', + default: '', + placeholder: 'fields.tags[nin]=accessories,flowers', + description: 'Search for all data that matches the condition: {attribute}[nin]={value}. Attribute can use dot notation.', + }, + { + displayName: 'Exist', + name: 'exist', + type: 'string', + default: '', + placeholder: 'fields.tags[exists]=true', + description: 'Search for all data that matches the condition: {attribute}[exists]={value}. Attribute can use dot notation.', + }, + { + displayName: 'Fields', + name: 'select', + type: 'string', + placeholder: 'fields.title', + default: '', + description: 'The select operator allows you to choose what fields to return from an entity. You can choose multiple values by combining comma separated operators.', + }, + { + displayName: 'Include', + name: 'include', + type: 'string', + default: '', + placeholder: 'fields.tags[in]=accessories,flowers', + description: 'Search for all data that matches the condition: {attribute}[in]={value}. Attribute can use dot notation.', + }, + { + displayName: 'Not Equal', + name: 'notEqual', + type: 'string', + default: '', + placeholder: 'fields.title[ne]=n8n', + description: 'Search for all data that matches the condition: {attribute}[ne]={value}. Attribute can use dot notation.', + }, + { + displayName: 'Order', + name: 'order', + type: 'string', + default: '', + placeholder: 'sys.createdAt', + description: 'You can order items in the response by specifying the order search parameter. You can use sys properties (such as sys.createdAt) or field values (such as fields.myCustomDateField) for ordering.', + }, + { + displayName: 'Query', + name: 'query', + type: 'string', + default: '', + description: ' Full-text search is case insensitive and might return more results than expected. A query will only take values with more than 1 character.', + }, + ], + }, + { + displayName: 'Asset ID', + name: 'assetId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + resource.value + ], + operation: [ + 'get', + ], + }, + }, + }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Contentful/ContentTypeDescription.ts b/packages/nodes-base/nodes/Contentful/ContentTypeDescription.ts index 0a2ebfd1e78..6b2fa4eb3fb 100644 --- a/packages/nodes-base/nodes/Contentful/ContentTypeDescription.ts +++ b/packages/nodes-base/nodes/Contentful/ContentTypeDescription.ts @@ -1,49 +1,69 @@ -import { INodeProperties, INodePropertyOptions } from "n8n-workflow"; +import { + INodeProperties, + INodePropertyOptions, +} from 'n8n-workflow'; export const resource = { - name: "Content Types", - value: "content_type", + name: 'Content Type', + value: 'contentType', } as INodePropertyOptions; export const operations = [ - { - displayName: "Operation", - name: "operation", - type: "options", - displayOptions: { - show: { - resource: [resource.value], - }, - }, - options: [ - { - name: "Get Content types", - value: "get_content_types", - }, - { - name: "Get Single Content Type", - value: "get_content_type", - }, - ], - default: "get_content_types", - description: "The operation to perform.", - }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + resource.value, + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, ] as INodeProperties[]; export const fields = [ - { - displayName: "Content Type Id", - name: "content_type_id", - type: "string", - default: "", - placeholder: "", - description: "", - required: true, - displayOptions: { - show: { - resource: [resource.value], - operation: ["get_content_type"], - }, - }, - }, + { + displayName: 'Environment ID', + name: 'environmentId', + type: 'string', + displayOptions: { + show: { + resource: [ + resource.value, + ], + operation: [ + 'get', + ], + }, + }, + default: 'master', + description: 'The id for the Contentful environment (e.g. master, staging, etc.). Depending on your plan, you might not have environments. In that case use "master".' + }, + { + displayName: 'Content Type ID', + name: 'contentTypeId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + resource.value, + ], + operation: [ + 'get', + ], + }, + }, + }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Contentful/Contentful.node.ts b/packages/nodes-base/nodes/Contentful/Contentful.node.ts index 930534e5f7c..820e8b07a90 100644 --- a/packages/nodes-base/nodes/Contentful/Contentful.node.ts +++ b/packages/nodes-base/nodes/Contentful/Contentful.node.ts @@ -1,24 +1,34 @@ -import { IExecuteFunctions } from 'n8n-core'; -import { IDataObject, INodeExecutionData, INodeType, INodeTypeDescription, NodePropertyTypes } from 'n8n-workflow'; +import { + IExecuteFunctions, +} from 'n8n-core'; -import { contentfulApiRequest } from './GenericFunctions'; -import resolveResponse from './resolveResponse'; +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + contentfulApiRequest, + contenfulApiRequestAllItems, +} from './GenericFunctions'; import * as SpaceDescription from './SpaceDescription'; import * as ContentTypeDescription from './ContentTypeDescription'; import * as EntryDescription from './EntryDescription'; import * as AssetDescription from './AssetDescription'; import * as LocaleDescription from './LocaleDescription'; -import * as SearchParameterDescription from './SearchParameterDescription'; export class Contentful implements INodeType { description: INodeTypeDescription = { displayName: 'Contentful', name: 'contentful', + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', icon: 'file:contentful.png', group: ['input'], version: 1, - description: "Access data through Contentful's Content Delivery API", + description: 'Consume Contenful API', defaults: { name: 'Contentful', color: '#2E75D4' @@ -27,49 +37,39 @@ export class Contentful implements INodeType { outputs: ['main'], credentials: [ { - name: 'contentfulDeliveryApi', + name: 'contentfulApi', required: true - } + }, ], properties: [ - // Common fields: { displayName: 'Source', name: 'source', - type: 'options' as NodePropertyTypes, + type: 'options', default: 'Delivery API', description: 'Pick where your data comes from, delivery or preview API', options: [ { name: 'Delivery API', - value: 'delivery_api' + value: 'deliveryApi' }, { name: 'Preview API', - value: 'preview_api' - } - ] + value: 'previewApi' + }, + ], }, - { - displayName: 'Environment Id', - name: 'environment_id', - type: 'string' as NodePropertyTypes, - default: '', - description: - 'The id for the Contentful environment (e.g. master, staging, etc.). Depending on your plan, you might not have environments. In that case use "master".' - }, - // Resources: { displayName: 'Resource', name: 'resource', type: 'options', options: [ - SpaceDescription.resource, + AssetDescription.resource, ContentTypeDescription.resource, EntryDescription.resource, - AssetDescription.resource, - LocaleDescription.resource + LocaleDescription.resource, + SpaceDescription.resource, ], default: '', description: 'The resource to operate on.' @@ -88,16 +88,13 @@ export class Contentful implements INodeType { ...EntryDescription.fields, ...AssetDescription.fields, ...LocaleDescription.fields, - - // Options: - ...SearchParameterDescription.fields - ] + ], }; async execute(this: IExecuteFunctions): Promise { - const environmentId = this.getNodeParameter('environment_id', 0) as string; const resource = this.getNodeParameter('resource', 0) as string; const operation = this.getNodeParameter('operation', 0) as string; + let responseData; const items = this.getInputData(); const returnData: IDataObject[] = []; @@ -105,54 +102,164 @@ export class Contentful implements INodeType { for (let i = 0; i < items.length; i++) { if (resource === 'space') { - if (operation === 'get_space') { - const res = await contentfulApiRequest(this); - returnData.push(res); + if (operation === 'get') { + + const credentials = this.getCredentials('contentfulApi'); + + responseData = await contentfulApiRequest.call(this, 'GET', `/spaces/${credentials?.spaceId}`); } - } else if (resource === 'content_type') { - if (operation === 'get_content_types') { - const res = await contentfulApiRequest(this, '/content_types', environmentId); - const resolvedData = resolveResponse(res, {}); - returnData.push(...resolvedData); - } else if (operation === 'get_content_type') { - const id = this.getNodeParameter('content_type_id', 0) as string; - const res = await contentfulApiRequest(this, `/content_types/${id}`, environmentId); - returnData.push(...res.items); + } + if (resource === 'contentType') { + if (operation === 'get') { + + const credentials = this.getCredentials('contentfulApi'); + + const env = this.getNodeParameter('environmentId', 0) as string; + + const id = this.getNodeParameter('contentTypeId', 0) as string; + + responseData = await contentfulApiRequest.call(this, 'GET', `/spaces/${credentials?.spaceId}/environments/${env}/content_types/${id}`); } - } else if (resource === 'entry') { - if (operation === 'get_entries') { - const shouldResolve = this.getNodeParameter('resolve', 0) as boolean; - if (shouldResolve) qs.include = this.getNodeParameter('include', 0) as number; - const searchParameters = this.getNodeParameter('search_parameters', 0) as IDataObject; - if (searchParameters.parameters && Array.isArray(searchParameters.parameters)) { - searchParameters.parameters.forEach(parameter => { - const { name, value } = parameter as { name: string; value: string }; - qs[name] = value; - }); + } + if (resource === 'entry') { + + if (operation === 'get') { + + const credentials = this.getCredentials('contentfulApi'); + + const env = this.getNodeParameter('environmentId', 0) as string; + + const id = this.getNodeParameter('entryId', 0) as string; + + responseData = await contentfulApiRequest.call(this, 'GET', `/spaces/${credentials?.spaceId}/environments/${env}/entries/${id}`, {}, qs); + + } else if (operation === 'getAll') { + const credentials = this.getCredentials('contentfulApi'); + + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const env = this.getNodeParameter('environmentId', i) as string; + + Object.assign(qs, additionalFields); + + if (qs.equal) { + const [atribute, value] = (qs.equal as string).split('='); + qs[atribute] = value; + delete qs.equal; + } + + if (qs.notEqual) { + const [atribute, value] = (qs.notEqual as string).split('='); + qs[atribute] = value; + delete qs.notEqual; + } + + if (qs.include) { + const [atribute, value] = (qs.include as string).split('='); + qs[atribute] = value; + delete qs.include; + } + + if (qs.exclude) { + const [atribute, value] = (qs.exclude as string).split('='); + qs[atribute] = value; + delete qs.exclude; + } + + if (returnAll) { + responseData = await contenfulApiRequestAllItems.call(this, 'items', 'GET', `/spaces/${credentials?.spaceId}/environments/${env}/entries`, {}, qs); + } else { + const limit = this.getNodeParameter('limit', 0) as number; + qs.limit = limit; + responseData = await contentfulApiRequest.call(this, 'GET', `/spaces/${credentials?.spaceId}/environments/${env}/entries`, {}, qs); + responseData = responseData.items; } - const res = await contentfulApiRequest(this, '/entries', environmentId, qs); - const resolvedData = shouldResolve ? resolveResponse(res, {}) : res.items; - returnData.push(...resolvedData); - } else if (operation === 'get_entry') { - const id = this.getNodeParameter('entry_id', 0) as string; - const res = await contentfulApiRequest(this, `/entries/${id}`, environmentId); - returnData.push(res); } - } else if (resource === 'asset') { - if (operation === 'get_assets') { - const res = await contentfulApiRequest(this, '/assets', environmentId); - returnData.push(...res.items); - } else if (operation === 'get_asset') { - const id = this.getNodeParameter('asset_id', 0) as string; - const res = await contentfulApiRequest(this, `/assets/${id}`, environmentId); - returnData.push(res); + } + if (resource === 'asset') { + if (operation === 'get') { + + const credentials = this.getCredentials('contentfulApi'); + + const env = this.getNodeParameter('environmentId', 0) as string; + + const id = this.getNodeParameter('assetId', 0) as string; + + responseData = await contentfulApiRequest.call(this, 'GET', `/spaces/${credentials?.spaceId}/environments/${env}/assets/${id}`, {}, qs); + + } else if (operation === 'getAll') { + + const credentials = this.getCredentials('contentfulApi'); + + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const env = this.getNodeParameter('environmentId', i) as string; + + Object.assign(qs, additionalFields); + + if (qs.equal) { + const [atribute, value] = (qs.equal as string).split('='); + qs[atribute] = value; + delete qs.equal; + } + + if (qs.notEqual) { + const [atribute, value] = (qs.notEqual as string).split('='); + qs[atribute] = value; + delete qs.notEqual; + } + + if (qs.include) { + const [atribute, value] = (qs.include as string).split('='); + qs[atribute] = value; + delete qs.include; + } + + if (qs.exclude) { + const [atribute, value] = (qs.exclude as string).split('='); + qs[atribute] = value; + delete qs.exclude; + } + + if (returnAll) { + responseData = await contenfulApiRequestAllItems.call(this, 'items', 'GET', `/spaces/${credentials?.spaceId}/environments/${env}/assets`, {}, qs); + } else { + const limit = this.getNodeParameter('limit', 0) as number; + qs.limit = limit; + responseData = await contentfulApiRequest.call(this, 'GET', `/spaces/${credentials?.spaceId}/environments/${env}/assets`, {}, qs); + responseData = responseData.items; + } } - } else if (resource === 'locale') { - if (operation === 'get_locales') { - const res = await contentfulApiRequest(this, '/locales', environmentId); - returnData.push(res); + } + if (resource === 'locale') { + + if (operation === 'getAll') { + + const credentials = this.getCredentials('contentfulApi'); + + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + + const env = this.getNodeParameter('environmentId', i) as string; + + if (returnAll) { + responseData = await contenfulApiRequestAllItems.call(this, 'items', 'GET', `/spaces/${credentials?.spaceId}/environments/${env}/locales`, {}, qs); + } else { + const limit = this.getNodeParameter('limit', 0) as number; + qs.limit = limit; + responseData = await contentfulApiRequest.call(this, 'GET', `/spaces/${credentials?.spaceId}/environments/${env}/locales`, {}, qs); + responseData = responseData.items; + } } } + 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/Contentful/EntryDescription.ts b/packages/nodes-base/nodes/Contentful/EntryDescription.ts index 781bcc6fe1a..c22e3ba0d66 100644 --- a/packages/nodes-base/nodes/Contentful/EntryDescription.ts +++ b/packages/nodes-base/nodes/Contentful/EntryDescription.ts @@ -1,8 +1,14 @@ -import { INodeProperties, INodePropertyOptions } from 'n8n-workflow'; +import { + INodeProperties, + INodePropertyOptions, +} from 'n8n-workflow'; +import { type } from 'os'; +import { notEqual } from 'assert'; +import { exists } from 'fs'; export const resource = { name: 'Entry', - value: 'entry' + value: 'entry', } as INodePropertyOptions; export const operations = [ @@ -12,72 +18,190 @@ export const operations = [ type: 'options', displayOptions: { show: { - resource: [resource.value] - } + resource: [ + resource.value, + ], + }, }, options: [ { - name: 'Get Entries', - value: 'get_entries' + name: 'Get', + value: 'get', }, { - name: 'Get Single Entry', - value: 'get_entry' - } + name: 'Get All', + value: 'getAll', + }, ], - default: 'get_entries', + default: 'get', description: 'The operation to perform.' } ] as INodeProperties[]; export const fields = [ { - displayName: 'Resolve', - name: 'resolve', - type: 'boolean', - default: false, - description: 'Linked entries can be automatically resolved in the results if you click activate this feature.', + displayName: 'Environment ID', + name: 'environmentId', + type: 'string', displayOptions: { show: { - resource: [resource.value], - operation: ['get_entries'] - } - } - }, - { - displayName: 'Include', - name: 'include', - type: 'number', - default: 1, - placeholder: '', - description: - "When you have related content (e.g. entries with links to image assets) it's possible to include them in the results. Using the include parameter, you can specify the number of levels of entries to include in the results. A lower number might improve performance.", - typeOptions: { - minValue: 0, - maxValue: 10 + resource: [ + resource.value, + ], + operation: [ + 'get', + 'getAll', + ], + }, }, + default: 'master', + description: 'The id for the Contentful environment (e.g. master, staging, etc.). Depending on your plan, you might not have environments. In that case use "master".' + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', displayOptions: { show: { - resource: [resource.value], - operation: ['get_entries'], - resolve: [true] - } - } + operation: [ + 'getAll', + ], + resource: [ + resource.value, + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', }, - { - displayName: 'Entry Id', - name: 'entry_id', + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + resource.value, + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + resource.value, + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Content Type ID', + name: 'content_type', + type: 'string', + default: '', + description: 'To search for entries with a specific content type', + }, + { + displayName: 'Equal', + name: 'equal', + type: 'string', + default: '', + placeholder: 'fields.title=n8n', + description: 'Search for all data that matches the condition: {attribute}={value}. Attribute can use dot notation.', + }, + { + displayName: 'Exclude', + name: 'exclude', + type: 'string', + default: '', + placeholder: 'fields.tags[nin]=accessories,flowers', + description: 'Search for all data that matches the condition: {attribute}[nin]={value}. Attribute can use dot notation.', + }, + { + displayName: 'Exist', + name: 'exist', + type: 'string', + default: '', + placeholder: 'fields.tags[exists]=true', + description: 'Search for all data that matches the condition: {attribute}[exists]={value}. Attribute can use dot notation.', + }, + { + displayName: 'Fields', + name: 'select', + type: 'string', + placeholder: 'fields.title', + default: '', + description: 'The select operator allows you to choose what fields to return from an entity. You can choose multiple values by combining comma separated operators.', + }, + { + displayName: 'Include', + name: 'include', + type: 'string', + default: '', + placeholder: 'fields.tags[in]=accessories,flowers', + description: 'Search for all data that matches the condition: {attribute}[in]={value}. Attribute can use dot notation.', + }, + { + displayName: 'Not Equal', + name: 'notEqual', + type: 'string', + default: '', + placeholder: 'fields.title[ne]=n8n', + description: 'Search for all data that matches the condition: {attribute}[ne]={value}. Attribute can use dot notation.', + }, + { + displayName: 'Order', + name: 'order', + type: 'string', + default: '', + placeholder: 'sys.createdAt', + description: 'You can order items in the response by specifying the order search parameter. You can use sys properties (such as sys.createdAt) or field values (such as fields.myCustomDateField) for ordering.', + }, + { + displayName: 'Query', + name: 'query', + type: 'string', + default: '', + description: ' Full-text search is case insensitive and might return more results than expected. A query will only take values with more than 1 character.', + }, + ], + }, + { + displayName: 'Entry ID', + name: 'entryId', type: 'string', default: '', - placeholder: '', - description: '', required: true, displayOptions: { show: { - resource: [resource.value], - operation: ['get_entry'] - } - } + resource: [ + resource.value, + ], + operation: [ + 'get', + ], + }, + }, } ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Contentful/GenericFunctions.ts b/packages/nodes-base/nodes/Contentful/GenericFunctions.ts index 246c43c220d..218a85cc71f 100644 --- a/packages/nodes-base/nodes/Contentful/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Contentful/GenericFunctions.ts @@ -1,45 +1,75 @@ -import { IExecuteFunctions } from 'n8n-core'; -import { OptionsWithUrl } from 'request'; +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; -/** - * @param {IExecuteFunctions} that Reference to the system's execute functions - * @param {string} endpoint? Endpoint of api call - * @param {string} environmentId? Id of contentful environment (eg. master, staging, etc.) - * @param {Record} qs? Query string, can be used for search parameters - */ -export const contentfulApiRequest = async ( - that: IExecuteFunctions, - endpoint?: string, - environmentId?: string, - qs?: Record -) => { - const credentials = that.getCredentials('contentfulDeliveryApi'); +import { + OptionsWithUri, +} from 'request'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function contentfulApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const credentials = this.getCredentials('contentfulApi'); if (credentials === undefined) { throw new Error('No credentials got returned!'); } - const source = that.getNodeParameter('source', 0) as string; - const isPreview = source === 'preview_api'; - let accessToken = credentials.access_token as string; + const source = this.getNodeParameter('source', 0) as string; + const isPreview = source === 'previewApi'; + + const options: OptionsWithUri = { + method, + qs, + body, + uri: uri ||`https://${isPreview ? 'preview' : 'cdn'}.contentful.com${resource}`, + json: true + }; + if (isPreview) { - accessToken = credentials.access_token_preview as string; - console.log('accessToken', accessToken); - if (!accessToken) { - throw new Error('No access token for preview API set in credentials!'); - } + qs.access_token = credentials.ContentPreviewaccessToken as string; + } else { + qs.access_token = credentials.ContentDeliveryaccessToken as string; } - let url = `https://${isPreview ? 'preview' : 'cdn'}.contentful.com/spaces/${credentials.space_id}`; - if (environmentId) url = `${url}/environments/${environmentId}`; - if (endpoint) url = `${url}${endpoint}`; - qs = qs || {}; - qs.access_token = accessToken; + try { + return await this.helpers.request!(options); + } catch (error) { - const res = await that.helpers.request!({ - url, - method: 'GET', - qs - } as OptionsWithUrl); + let errorMessage = error; - return JSON.parse(res); -}; + if (error.response && error.response.body && error.response.body.details) { + const details = error.response.body.details; + errorMessage = details.errors.map((e: IDataObject) => e.details).join('|'); + } else if (error.response && error.response.body && error.response.body.message) { + errorMessage = error.response.body.message; + } + + throw new Error(`Contentful error response [${error.statusCode}]: ${errorMessage}`); + } + +} + +export async function contenfulApiRequestAllItems(this: ILoadOptionsFunctions | IExecuteFunctions, propertyName: string, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + query.limit = 100; + query.skip = 0; + + do { + responseData = await contentfulApiRequest.call(this, method, resource, body, query); + query.skip = (query.skip + 1) * query.limit; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + returnData.length < responseData.total + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Contentful/LocaleDescription.ts b/packages/nodes-base/nodes/Contentful/LocaleDescription.ts index 87e7f6b9320..45f2872ad22 100644 --- a/packages/nodes-base/nodes/Contentful/LocaleDescription.ts +++ b/packages/nodes-base/nodes/Contentful/LocaleDescription.ts @@ -1,29 +1,94 @@ -import { INodeProperties, INodePropertyOptions } from "n8n-workflow"; +import { + INodeProperties, + INodePropertyOptions +} from 'n8n-workflow'; export const resource = { - name: "Locale", - value: "locale", + name: 'Locale', + value: 'locale', } as INodePropertyOptions; export const operations = [ - { - displayName: "Operation", - name: "operation", - type: "options", - displayOptions: { - show: { - resource: [resource.value], - }, - }, - options: [ - { - name: "Get Locales", - value: "get_locales", - }, - ], - default: "get_locales", - description: "The operation to perform.", - }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + resource.value, + ], + }, + }, + options: [ + { + name: 'Get All', + value: 'getAll', + }, + ], + default: 'getAll', + description: 'The operation to perform.', + }, ] as INodeProperties[]; -export const fields = [] as INodeProperties[]; +export const fields = [ + { + displayName: 'Environment ID', + name: 'environmentId', + type: 'string', + displayOptions: { + show: { + resource: [ + resource.value, + ], + operation: [ + 'get', + 'getAll', + ], + }, + }, + default: 'master', + description: 'The id for the Contentful environment (e.g. master, staging, etc.). Depending on your plan, you might not have environments. In that case use "master".' + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + resource.value, + ], + }, + }, + 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: [ + resource.value, + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Contentful/SpaceDescription.ts b/packages/nodes-base/nodes/Contentful/SpaceDescription.ts index 2baabee0993..035f3c6e573 100644 --- a/packages/nodes-base/nodes/Contentful/SpaceDescription.ts +++ b/packages/nodes-base/nodes/Contentful/SpaceDescription.ts @@ -1,29 +1,33 @@ -import { INodeProperties } from "n8n-workflow"; +import { + INodeProperties, +} from 'n8n-workflow'; export const resource = { - name: "Space", - value: "space", + name: 'Space', + value: 'space', }; export const operations = [ - { - displayName: "Operation", - name: "operation", - type: "options", - displayOptions: { - show: { - resource: [resource.value], - }, - }, - options: [ - { - name: "Get Space", - value: "get_space", - }, - ], - default: "get_space", - description: "The operation to perform.", - }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + resource.value + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, ] as INodeProperties[]; export const fields = [] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Contentful/resolveResponse.ts b/packages/nodes-base/nodes/Contentful/resolveResponse.ts deleted file mode 100644 index 6542d335951..00000000000 --- a/packages/nodes-base/nodes/Contentful/resolveResponse.ts +++ /dev/null @@ -1,156 +0,0 @@ -// @ts-nocheck -// Code from https://github.com/contentful/contentful-resolve-response/blob/master/index.js -import { cloneDeep } from 'lodash'; - -const UNRESOLVED_LINK = {}; // unique object to avoid polyfill bloat using Symbol() - -/** - * isLink Function - * Checks if the object has sys.type "Link" - * @param object - */ -const isLink = (object: { sys: { type: string } }) => - object && object.sys && object.sys.type === 'Link'; - -/** - * findNormalizableLinkInArray - * - * @param array - * @param predicate - * @return {*} - */ -const findNormalizableLinkInArray = (array, predicate) => { - for (let i = 0, len = array.length; i < len; i++) { - if (predicate(array[i])) { - return array[i]; - } - } - return UNRESOLVED_LINK; -}; - -/** - * getLink Function - * - * @param response - * @param link - * @return {undefined} - */ -const getLink = (allEntries, link) => { - const { linkType: type, id } = link.sys; - - const predicate = ({ sys }) => sys.type === type && sys.id === id; - - return findNormalizableLinkInArray(allEntries, predicate); -}; - -/** - * cleanUpLinks Function - * - Removes unresolvable links from Arrays and Objects - * - * @param {Object[]|Object} input - */ -const cleanUpLinks = input => { - if (Array.isArray(input)) { - return input.filter(val => val !== UNRESOLVED_LINK); - } - for (const key in input) { - if (input[key] === UNRESOLVED_LINK) { - delete input[key]; - } - } - return input; -}; - -/** - * walkMutate Function - * @param input - * @param predicate - * @param mutator - * @return {*} - */ -const walkMutate = (input, predicate, mutator, removeUnresolved) => { - if (predicate(input)) { - return mutator(input); - } - - if (input && typeof input === 'object') { - for (const key in input) { - if (input.hasOwnProperty(key)) { - input[key] = walkMutate( - input[key], - predicate, - mutator, - removeUnresolved - ); - } - } - if (removeUnresolved) { - input = cleanUpLinks(input); - } - } - return input; -}; - -const normalizeLink = (allEntries, link, removeUnresolved) => { - const resolvedLink = getLink(allEntries, link); - if (resolvedLink === UNRESOLVED_LINK) { - return removeUnresolved ? resolvedLink : link; - } - return resolvedLink; -}; - -const makeEntryObject = (item, itemEntryPoints) => { - if (!Array.isArray(itemEntryPoints)) { - return item; - } - - const entryPoints = Object.keys(item).filter( - ownKey => itemEntryPoints.indexOf(ownKey) !== -1 - ); - - return entryPoints.reduce((entryObj, entryPoint) => { - entryObj[entryPoint] = item[entryPoint]; - return entryObj; - }, {}); -}; - -/** - * resolveResponse Function - * Resolves contentful response to normalized form. - * @param {Object} response Contentful response - * @param {Object} options - * @param {Boolean} options.removeUnresolved - Remove unresolved links default:false - * @param {Array} options.itemEntryPoints - Resolve links only in those item properties - * @return {Object} - */ -const resolveResponse = (response, options) => { - options = options || {}; - if (!response.items) { - return []; - } - const responseClone = cloneDeep(response); - const allIncludes = Object.keys(responseClone.includes || {}).reduce( - (all, type) => [...all, ...response.includes[type]], - [] - ); - - const allEntries = [...responseClone.items, ...allIncludes]; - - allEntries.forEach(item => { - const entryObject = makeEntryObject(item, options.itemEntryPoints); - - Object.assign( - item, - walkMutate( - entryObject, - isLink, - link => normalizeLink(allEntries, link, options.removeUnresolved), - options.removeUnresolved - ) - ); - }); - - return responseClone.items; -}; - -export default resolveResponse; diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 45b703b9d89..fed1f13606e 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -43,7 +43,7 @@ "dist/credentials/ClockifyApi.credentials.js", "dist/credentials/CockpitApi.credentials.js", "dist/credentials/CodaApi.credentials.js", - "dist/credentials/ContentfulDeliveryApi.credentials.js", + "dist/credentials/ContentfulApi.credentials.js", "dist/credentials/CopperApi.credentials.js", "dist/credentials/CalendlyApi.credentials.js", "dist/credentials/DisqusApi.credentials.js", From 9b15cf50bc1b7e719417a8fa9b8e693bb71f8d84 Mon Sep 17 00:00:00 2001 From: ricardo Date: Wed, 5 Aug 2020 21:55:53 -0400 Subject: [PATCH 07/39] :zap: Improvements to Converkit-Node --- .../nodes/ConvertKit/ConvertKit.node.ts | 507 ++++++++++++------ ...scription.ts => CustomFieldDescription.ts} | 61 ++- .../nodes/ConvertKit/FormDescription.ts | 74 ++- .../nodes/ConvertKit/GenericFunctions.ts | 20 +- .../nodes/ConvertKit/SequenceDescription.ts | 82 ++- .../nodes/ConvertKit/TagDescription.ts | 160 +----- .../ConvertKit/TagSubscriberDescription.ts | 219 ++++++++ 7 files changed, 794 insertions(+), 329 deletions(-) rename packages/nodes-base/nodes/ConvertKit/{FieldDescription.ts => CustomFieldDescription.ts} (52%) create mode 100644 packages/nodes-base/nodes/ConvertKit/TagSubscriberDescription.ts diff --git a/packages/nodes-base/nodes/ConvertKit/ConvertKit.node.ts b/packages/nodes-base/nodes/ConvertKit/ConvertKit.node.ts index 71f58a41728..8c9870639ea 100644 --- a/packages/nodes-base/nodes/ConvertKit/ConvertKit.node.ts +++ b/packages/nodes-base/nodes/ConvertKit/ConvertKit.node.ts @@ -1,5 +1,6 @@ import { IExecuteFunctions, + ILoadOptionsFunctions, } from 'n8n-core'; import { @@ -7,6 +8,7 @@ import { INodeExecutionData, INodeTypeDescription, INodeType, + INodePropertyOptions, } from 'n8n-workflow'; import { @@ -14,9 +16,9 @@ import { } from './GenericFunctions'; import { - fieldOperations, - fieldFields, -} from './FieldDescription'; + customFieldOperations, + customFieldFields, +} from './CustomFieldDescription'; import { formOperations, @@ -33,6 +35,11 @@ import { tagFields, } from './TagDescription'; +import { + tagSubscriberOperations, + tagSubscriberFields, +} from './TagSubscriberDescription'; + export class ConvertKit implements INodeType { description: INodeTypeDescription = { displayName: 'ConvertKit', @@ -61,8 +68,8 @@ export class ConvertKit implements INodeType { type: 'options', options: [ { - name: 'Field', - value: 'field', + name: 'Custom Field', + value: 'customField', }, { name: 'Form', @@ -76,15 +83,19 @@ export class ConvertKit implements INodeType { name: 'Tag', value: 'tag', }, + { + name: 'Tag Subscriber', + value: 'tagSubscriber', + }, ], - default: 'field', + default: 'customField', description: 'The resource to operate on.' }, //-------------------- // Field Description //-------------------- - ...fieldOperations, - ...fieldFields, + ...customFieldOperations, + ...customFieldFields, //-------------------- // FormDescription //-------------------- @@ -100,187 +111,373 @@ export class ConvertKit implements INodeType { //-------------------- ...tagOperations, ...tagFields, + //-------------------- + // Tag Subscriber Description + //-------------------- + ...tagSubscriberOperations, + ...tagSubscriberFields, ], }; + 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 convertKitApiRequest.call(this, 'GET', '/tags'); + for (const tag of tags) { + const tagName = tag.name; + const tagId = tag.id; + returnData.push({ + name: tagName, + value: tagId, + }); + } + + return returnData; + }, + // Get all the forms to display them to user so that he can + // select them easily + async getForms(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const { forms } = await convertKitApiRequest.call(this, 'GET', '/forms'); + for (const form of forms) { + const formName = form.name; + const formId = form.id; + returnData.push({ + name: formName, + value: formId, + }); + } + + return returnData; + }, + + // Get all the sequences to display them to user so that he can + // select them easily + async getSequences(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const { courses } = await convertKitApiRequest.call(this, 'GET', '/sequences'); + for (const course of courses) { + const courseName = course.name; + const courseId = course.id; + returnData.push({ + name: courseName, + value: courseId, + }); + } + + return returnData; + }, + } + }; + async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); const returnData: IDataObject[] = []; - let method = ''; - let endpoint = ''; const qs: IDataObject = {}; let responseData; const resource = this.getNodeParameter('resource', 0) as string; const operation = this.getNodeParameter('operation', 0) as string; - const fullOperation = `${resource}/${operation}`; - for (let i = 0; i < items.length; i++) { - //-------------------- - // Field Operations - //-------------------- - if(resource === 'field') { - //--------- - // Update - //--------- - if(operation === 'update') { - qs.label = this.getNodeParameter('label', i) as string; + + if (resource === 'customField') { + if (operation === 'create') { + + const label = this.getNodeParameter('label', i) as string; + + responseData = await convertKitApiRequest.call(this, 'POST', '/custom_fields', { label }, qs); + } + if (operation === 'delete') { const id = this.getNodeParameter('id', i) as string; - method = 'PUT'; - endpoint = `/custom_fields/${id}`; - //--------- - // Get All - //--------- - } else if(operation === 'getAll') { - method = 'GET'; - endpoint = '/custom_fields'; - //--------- - // Create - //--------- - } else if(operation === 'create') { - qs.label = this.getNodeParameter('label', i) as string; + responseData = await convertKitApiRequest.call(this, 'DELETE', `/custom_fields/${id}`); + } + if (operation === 'get') { - method = 'POST'; - endpoint = '/custom_fields'; - //--------- - // Delete - //--------- - } else if(operation === 'delete') { const id = this.getNodeParameter('id', i) as string; - method = 'DELETE'; - endpoint = `/custom_fields/${id}`; - } else { - throw new Error(`The operation "${operation}" is not known!`); + responseData = await convertKitApiRequest.call(this, 'GET', `/custom_fields/${id}`); } - //-------------------------------------------- - // Form, Sequence, and Tag Operations - //-------------------------------------------- - } else if(['form', 'sequence', 'tag'].includes(resource)) { - //----------------- - // Add Subscriber - //----------------- - if(operation === 'addSubscriber') { - qs.email= this.getNodeParameter('email', i) as string; - const id = this.getNodeParameter('id', i); + if (operation === 'getAll') { - const additionalParams = this.getNodeParameter('additionalFields', 0) as IDataObject; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; - if(additionalParams.firstName) { - qs.first_name = additionalParams.firstName; + responseData = await convertKitApiRequest.call(this, 'GET', `/custom_fields`); + + responseData = responseData.custom_fields; + + if (!returnAll) { + + const limit = this.getNodeParameter('limit', i) as number; + + responseData = responseData.slice(0, limit); } - - if(additionalParams.fields !== undefined) { - const fields = {} as IDataObject; - const fieldsParams = additionalParams.fields as IDataObject; - const field = fieldsParams?.field as IDataObject[]; - - for(let j = 0; j < field.length; j++) { - const key = field[j].key as string; - const value = field[j].value as string; - - fields[key] = value; - } - - qs.fields = fields; - } - - if(resource === 'form') { - method = 'POST'; - endpoint = `/forms/${id}/subscribe`; - } else if(resource === 'sequence') { - method = 'POST'; - endpoint = `/sequences/${id}/subscribe`; - } else if(resource === 'tag') { - method = 'POST'; - endpoint = `/tags/${id}/subscribe`; - } - //----------------- - // Get All - //----------------- - } else if(operation === 'getAll') { - method = 'GET'; - if(resource === 'form') { - endpoint = '/forms'; - } else if(resource === 'tag') { - endpoint = '/tags'; - } else if(resource === 'sequence') { - endpoint = '/sequences'; - } - //-------------------- - // Get Subscriptions - //-------------------- - } else if(operation === 'getSubscriptions') { - const id = this.getNodeParameter('id', i); - const additionalParams = this.getNodeParameter('additionalFields', 0) as IDataObject; - if(additionalParams.subscriberState) { - qs.subscriber_state = additionalParams.subscriberState; - } - - method = 'GET'; - if(resource === 'form') { - endpoint = `/forms/${id}/subscriptions`; - } else if(resource === 'tag') { - endpoint = `/tags/${id}/subscriptions`; - } else if(resource === 'sequence') { - endpoint = `/sequences/${id}/subscriptions`; - } - //------------ - // Create Tag - //------------ - } else if(operation === 'create') { - const name = this.getNodeParameter('name', i); - qs.tag = { name, }; - - method = 'POST'; - endpoint = '/tags'; - //------------ - // Remove Tag - //------------ - } else if(operation === 'removeSubscriber') { - const id = this.getNodeParameter('id', i); - - qs.email = this.getNodeParameter('email', i); - - method = 'POST'; - endpoint = `/tags/${id}/unsubscribe`; - } else { - throw new Error(`The operation "${operation}" is not known!`); } - } else { - throw new Error(`The resource "${resource}" is not known!`); + if (operation === 'update') { + + const id = this.getNodeParameter('id', i) as string; + + const label = this.getNodeParameter('label', i) as string; + + responseData = await convertKitApiRequest.call(this, 'PUT', `/custom_fields/${id}`, { label }); + + responseData = { success: true }; + } } - responseData = await convertKitApiRequest.call(this, method, endpoint, {}, qs); + if (resource === 'form') { + if (operation === 'addSubscriber') { - if(fullOperation === 'field/getAll') { - responseData = responseData.custom_fields; - } else if(['form/addSubscriber', 'tag/addSubscriber', 'sequence/addSubscriber'].includes(fullOperation)) { - responseData = responseData.subscription; - } else if(fullOperation === 'form/getAll') { - responseData = responseData.forms; - } else if(['form/getSubscriptions', 'tag/getSubscriptions'].includes(fullOperation)) { - responseData = responseData.subscriptions; - } else if(fullOperation === 'tag/getAll') { - responseData = responseData.tags; - } else if(fullOperation === 'sequence/getAll') { - responseData = responseData.courses; + const email = this.getNodeParameter('email', i) as string; + + const formId = this.getNodeParameter('id', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = { + email, + }; + + if (additionalFields.firstName) { + body.first_name = additionalFields.firstName as string; + } + + if (additionalFields.tags) { + body.tags = additionalFields.tags as string[]; + } + + if (additionalFields.fieldsUi) { + const fieldValues = (additionalFields.fieldsUi as IDataObject).fieldsValues as IDataObject[]; + if (fieldValues) { + body.fields = {}; + for (const fieldValue of fieldValues) { + //@ts-ignore + body.fields[fieldValue.key] = fieldValue.value; + } + } + } + + const { subscription } = await convertKitApiRequest.call(this, 'POST', `/forms/${formId}/subscribe`, body); + + responseData = subscription; + } + if (operation === 'getAll') { + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + responseData = await convertKitApiRequest.call(this, 'GET', `/forms`); + + responseData = responseData.forms; + + if (!returnAll) { + + const limit = this.getNodeParameter('limit', i) as number; + + responseData = responseData.slice(0, limit); + } + } + if (operation === 'getSubscriptions') { + + const formId = this.getNodeParameter('id', i) as string; + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (additionalFields.subscriberState) { + qs.subscriber_state = additionalFields.subscriberState as string; + } + + responseData = await convertKitApiRequest.call(this, 'GET', `/forms/${formId}/subscriptions`, {}, qs); + + responseData = responseData.subscriptions; + + if (!returnAll) { + + const limit = this.getNodeParameter('limit', i) as number; + + responseData = responseData.slice(0, limit); + } + } + } + + if (resource === 'sequence') { + if (operation === 'addSubscriber') { + + const email = this.getNodeParameter('email', i) as string; + + const sequenceId = this.getNodeParameter('id', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = { + email, + }; + + if (additionalFields.firstName) { + body.first_name = additionalFields.firstName as string; + } + + if (additionalFields.tags) { + body.tags = additionalFields.tags as string[]; + } + + if (additionalFields.fieldsUi) { + const fieldValues = (additionalFields.fieldsUi as IDataObject).fieldsValues as IDataObject[]; + if (fieldValues) { + body.fields = {}; + for (const fieldValue of fieldValues) { + //@ts-ignore + body.fields[fieldValue.key] = fieldValue.value; + } + } + } + + const { subscription } = await convertKitApiRequest.call(this, 'POST', `/sequences/${sequenceId}/subscribe`, body); + + responseData = subscription; + } + if (operation === 'getAll') { + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + responseData = await convertKitApiRequest.call(this, 'GET', `/sequences`); + + responseData = responseData.courses; + + if (!returnAll) { + + const limit = this.getNodeParameter('limit', i) as number; + + responseData = responseData.slice(0, limit); + } + } + if (operation === 'getSubscriptions') { + + const sequenceId = this.getNodeParameter('id', i) as string; + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (additionalFields.subscriberState) { + qs.subscriber_state = additionalFields.subscriberState as string; + } + + responseData = await convertKitApiRequest.call(this, 'GET', `/sequences/${sequenceId}/subscriptions`, {}, qs); + + responseData = responseData.subscriptions; + + if (!returnAll) { + + const limit = this.getNodeParameter('limit', i) as number; + + responseData = responseData.slice(0, limit); + } + } + } + + if (resource === 'tag') { + if (operation === 'create') { + + const names = ((this.getNodeParameter('name', i) as string).split(',') as string[]).map((e) => ({ name: e })); + + const body: IDataObject = { + tag: names + }; + + responseData = await convertKitApiRequest.call(this, 'POST', '/tags', body); + } + + if (operation === 'getAll') { + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + responseData = await convertKitApiRequest.call(this, 'GET', `/tags`); + + responseData = responseData.tags; + + if (!returnAll) { + + const limit = this.getNodeParameter('limit', i) as number; + + responseData = responseData.slice(0, limit); + } + } + } + + if (resource === 'tagSubscriber') { + + if (operation === 'add') { + + const tagId = this.getNodeParameter('tagId', i) as string; + + const email = this.getNodeParameter('email', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = { + email, + }; + + if (additionalFields.firstName) { + body.first_name = additionalFields.firstName as string; + } + + if (additionalFields.fieldsUi) { + const fieldValues = (additionalFields.fieldsUi as IDataObject).fieldsValues as IDataObject[]; + if (fieldValues) { + body.fields = {}; + for (const fieldValue of fieldValues) { + //@ts-ignore + body.fields[fieldValue.key] = fieldValue.value; + } + } + } + + const { subscription } = await convertKitApiRequest.call(this, 'POST', `/tags/${tagId}/subscribe`, body); + + responseData = subscription; + } + + if (operation === 'getAll') { + + const tagId = this.getNodeParameter('tagId', i) as string; + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + responseData = await convertKitApiRequest.call(this, 'GET', `/tags/${tagId}/subscriptions`); + + responseData = responseData.subscriptions; + + if (!returnAll) { + + const limit = this.getNodeParameter('limit', i) as number; + + responseData = responseData.slice(0, limit); + } + } + + if (operation === 'delete') { + + const tagId = this.getNodeParameter('tagId', i) as string; + + const email = this.getNodeParameter('email', i) as string; + + responseData= await convertKitApiRequest.call(this, 'POST', `/tags/${tagId}>/unsubscribe`, { email }); + } } if (Array.isArray(responseData)) { returnData.push.apply(returnData, responseData as IDataObject[]); } else if (responseData !== undefined) { returnData.push(responseData as IDataObject); - } else { - if(method === 'GET') { - returnData.push( { } ); - } else { - returnData.push( { success: true } ); - } } } diff --git a/packages/nodes-base/nodes/ConvertKit/FieldDescription.ts b/packages/nodes-base/nodes/ConvertKit/CustomFieldDescription.ts similarity index 52% rename from packages/nodes-base/nodes/ConvertKit/FieldDescription.ts rename to packages/nodes-base/nodes/ConvertKit/CustomFieldDescription.ts index 8966e60efa7..a8541c64937 100644 --- a/packages/nodes-base/nodes/ConvertKit/FieldDescription.ts +++ b/packages/nodes-base/nodes/ConvertKit/CustomFieldDescription.ts @@ -1,8 +1,8 @@ import { - INodeProperties + INodeProperties, } from 'n8n-workflow'; -export const fieldOperations = [ +export const customFieldOperations = [ { displayName: 'Operation', name: 'operation', @@ -10,7 +10,7 @@ export const fieldOperations = [ displayOptions: { show: { resource: [ - 'field', + 'customField', ], }, }, @@ -18,22 +18,22 @@ export const fieldOperations = [ { name: 'Create', value: 'create', - description: 'Create a field.', + description: 'Create a field', }, { name: 'Delete', value: 'delete', - description: 'Delete a field.', + description: 'Delete a field', }, { name: 'Get All', value: 'getAll', - description: `List all of your account's custom fields.`, + description: 'Get all fields', }, { name: 'Update', value: 'update', - description: 'Update a field.', + description: 'Update a field', }, ], default: 'update', @@ -41,7 +41,7 @@ export const fieldOperations = [ }, ] as INodeProperties[]; -export const fieldFields = [ +export const customFieldFields = [ { displayName: 'Field ID', name: 'id', @@ -50,7 +50,7 @@ export const fieldFields = [ displayOptions: { show: { resource: [ - 'field', + 'customField', ], operation: [ 'update', @@ -69,7 +69,7 @@ export const fieldFields = [ displayOptions: { show: { resource: [ - 'field', + 'customField', ], operation: [ 'update', @@ -80,4 +80,45 @@ export const fieldFields = [ default: '', description: 'The label of the custom field.', }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'customField', + ], + }, + }, + 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: [ + 'customField', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/ConvertKit/FormDescription.ts b/packages/nodes-base/nodes/ConvertKit/FormDescription.ts index 3e266d18a5d..f6e1a6f3e5e 100644 --- a/packages/nodes-base/nodes/ConvertKit/FormDescription.ts +++ b/packages/nodes-base/nodes/ConvertKit/FormDescription.ts @@ -18,17 +18,17 @@ export const formOperations = [ { name: 'Add Subscriber', value: 'addSubscriber', - description: 'Add a subscriber.', + description: 'Add a subscriber', }, { name: 'Get All', value: 'getAll', - description: 'Get a list of all the forms for your account.', + description: 'Get all forms', }, { name: 'Get Subscriptions', value: 'getSubscriptions', - description: 'List subscriptions to a form including subscriber data.', + description: 'List subscriptions to a form including subscriber data', }, ], default: 'addSubscriber', @@ -38,7 +38,7 @@ export const formOperations = [ export const formFields = [ { - displayName: 'Email Address', + displayName: 'Email', name: 'email', type: 'string', required: true, @@ -58,7 +58,10 @@ export const formFields = [ { displayName: 'Form ID', name: 'id', - type: 'string', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getForms', + }, required: true, displayOptions: { show: { @@ -91,16 +94,9 @@ export const formFields = [ }, }, options: [ - { - displayName: 'First Name', - name: 'firstName', - type: 'string', - default: '', - description: `The subscriber's first name.`, - }, { displayName: 'Custom Fields', - name: 'fields', + name: 'fieldsUi', placeholder: 'Add Custom Field', description: 'Object of key/value pairs for custom fields (the custom field must exist before you can use it here).', type: 'fixedCollection', @@ -110,7 +106,7 @@ export const formFields = [ default: {}, options: [ { - name: 'field', + name: 'fieldsValues', displayName: 'Custom Field', values: [ { @@ -133,8 +129,58 @@ export const formFields = [ }, ], }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: `The subscriber's first name.`, + }, ], }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + 'getSubscriptions', + ], + resource: [ + 'form', + ], + }, + }, + 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', + 'getSubscriptions', + ], + resource: [ + 'form', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, { displayName: 'Additional Fields', name: 'additionalFields', diff --git a/packages/nodes-base/nodes/ConvertKit/GenericFunctions.ts b/packages/nodes-base/nodes/ConvertKit/GenericFunctions.ts index 525aed043a6..4844045e35b 100644 --- a/packages/nodes-base/nodes/ConvertKit/GenericFunctions.ts +++ b/packages/nodes-base/nodes/ConvertKit/GenericFunctions.ts @@ -15,7 +15,9 @@ import { export async function convertKitApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IHookFunctions, method: string, endpoint: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any - const credentials = this.getCredentials('convertKitApi'); + + const credentials = this.getCredentials('convertKitApi'); + if (credentials === undefined) { throw new Error('No credentials got returned!'); } @@ -30,15 +32,29 @@ export async function convertKitApiRequest(this: IExecuteFunctions | IExecuteSin uri: uri ||`https://api.convertkit.com/v3${endpoint}`, json: true, }; + options = Object.assign({}, options, option); + if (Object.keys(options.body).length === 0) { delete options.body; } + + console.log(options); + try { + qs.api_secret = credentials.apiSecret; return await this.helpers.request!(options); + } catch (error) { - throw new Error(`ConvertKit error response: ${error.message}`); + + let errorMessage = error; + + if (error.response && error.response.body && error.response.body.message) { + errorMessage = error.response.body.message; + } + + throw new Error(`ConvertKit error response: ${errorMessage}`); } } diff --git a/packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts b/packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts index 1b82b741fa2..493b713cdac 100644 --- a/packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts +++ b/packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts @@ -1,5 +1,5 @@ import { - INodeProperties + INodeProperties, } from 'n8n-workflow'; export const sequenceOperations = [ @@ -23,7 +23,7 @@ export const sequenceOperations = [ { name: 'Get All', value: 'getAll', - description: 'Returns a list of sequences for the account.', + description: 'Get all sequences.', }, { name: 'Get Subscriptions', @@ -38,7 +38,7 @@ export const sequenceOperations = [ export const sequenceFields = [ { - displayName: 'Email Address', + displayName: 'Email', name: 'email', type: 'string', required: true, @@ -58,7 +58,10 @@ export const sequenceFields = [ { displayName: 'Sequence ID', name: 'id', - type: 'string', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getSequences', + }, required: true, displayOptions: { show: { @@ -74,6 +77,49 @@ export const sequenceFields = [ default: '', description: 'Sequence ID.', }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + 'getSubscriptions', + ], + resource: [ + 'sequence', + ], + }, + }, + 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', + 'getSubscriptions', + ], + resource: [ + 'sequence', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, { displayName: 'Additional Fields', name: 'additionalFields', @@ -91,16 +137,9 @@ export const sequenceFields = [ }, }, options: [ - { - displayName: 'First Name', - name: 'firstName', - type: 'string', - default: '', - description: `The subscriber's first name.`, - }, { displayName: 'Custom Fields', - name: 'fields', + name: 'fieldsUi', placeholder: 'Add Custom Field', description: 'Object of key/value pairs for custom fields (the custom field must exist before you can use it here).', type: 'fixedCollection', @@ -110,7 +149,7 @@ export const sequenceFields = [ default: {}, options: [ { - name: 'field', + name: 'fieldsValues', displayName: 'Custom Field', values: [ { @@ -133,6 +172,23 @@ export const sequenceFields = [ }, ], }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: `The subscriber's first name.`, + }, + { + displayName: 'Tag IDs', + name: 'tags', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + default: [], + description: 'Tags', + }, ], }, { diff --git a/packages/nodes-base/nodes/ConvertKit/TagDescription.ts b/packages/nodes-base/nodes/ConvertKit/TagDescription.ts index a1a718ba3cd..b0d18adcfca 100644 --- a/packages/nodes-base/nodes/ConvertKit/TagDescription.ts +++ b/packages/nodes-base/nodes/ConvertKit/TagDescription.ts @@ -1,5 +1,5 @@ import { - INodeProperties + INodeProperties, } from 'n8n-workflow'; export const tagOperations = [ @@ -18,27 +18,12 @@ export const tagOperations = [ { name: 'Create', value: 'create', - description: 'Create a tag.', + description: 'Create a tag', }, { name: 'Get All', value: 'getAll', - description: 'Returns a list of tags for the account.', - }, - { - name: 'Get Subscriptions', - value: 'getSubscriptions', - description: 'List subscriptions to a tag including subscriber data.', - }, - { - name: 'Remove Subscriber', - value: 'removeSubscriber', - description: 'Remove a tag from a subscriber.', - }, - { - name: 'Add Subscriber', - value: 'addSubscriber', - description: 'Add a tag to a subscriber.', + description: 'Get all tags', }, ], default: 'create', @@ -63,142 +48,47 @@ export const tagFields = [ }, }, default: '', - description: 'Tag name.', + description: 'Tag name, multiple can be added separated by comma', }, { - displayName: 'Email Address', - name: 'email', - type: 'string', - required: true, + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', displayOptions: { show: { + operation: [ + 'getAll', + ], resource: [ 'tag', ], - operation: [ - 'addSubscriber', - 'removeSubscriber', - ], }, }, - default: '', - description: 'Subscriber email address.', + default: false, + description: 'If all results should be returned or only up to a given limit.', }, { - displayName: 'Tag ID', - name: 'id', - type: 'string', - required: true, + displayName: 'Limit', + name: 'limit', + type: 'number', displayOptions: { show: { + operation: [ + 'getAll', + ], resource: [ 'tag', ], - operation: [ - 'addSubscriber', - 'removeSubscriber', - 'getSubscriptions', + returnAll: [ + false, ], }, }, - default: '', - description: 'Tag ID.', - }, - { - displayName: 'Additional Fields', - name: 'additionalFields', - type: 'collection', - placeholder: 'Add Field', - default: {}, - displayOptions: { - show: { - resource: [ - 'tag', - ], - operation: [ - 'addSubscriber', - ], - }, + typeOptions: { + minValue: 1, + maxValue: 500, }, - options: [ - { - displayName: 'First Name', - name: 'firstName', - type: 'string', - default: '', - description: 'Subscriber first name.', - }, - { - displayName: 'Custom Fields', - name: 'fields', - placeholder: 'Add Custom Field', - description: 'Object of key/value pairs for custom fields (the custom field must exist before you can use it here).', - type: 'fixedCollection', - typeOptions: { - multipleValues: true, - }, - default: {}, - options: [ - { - name: 'field', - displayName: 'Custom Field', - values: [ - { - displayName: 'Field Key', - name: 'key', - type: 'string', - default: '', - placeholder: 'last_name', - description: `The field's key.`, - }, - { - displayName: 'Field Value', - name: 'value', - type: 'string', - default: '', - placeholder: 'Doe', - description: 'Value of the field.', - }, - ], - }, - ], - }, - ], - }, - { - displayName: 'Additional Fields', - name: 'additionalFields', - type: 'collection', - placeholder: 'Add Field', - default: {}, - displayOptions: { - show: { - resource: [ - 'tag', - ], - operation: [ - 'getSubscriptions', - ], - }, - }, - options: [ - { - displayName: 'Subscriber State', - name: 'subscriberState', - type: 'options', - options: [ - { - name: 'Active', - value: 'active', - }, - { - name: 'Cancelled', - value: 'cancelled', - }, - ], - default: 'active', - }, - ], - description: 'Receive only active subscribers or cancelled subscribers.', + default: 100, + description: 'How many results to return.', }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/ConvertKit/TagSubscriberDescription.ts b/packages/nodes-base/nodes/ConvertKit/TagSubscriberDescription.ts new file mode 100644 index 00000000000..f625dfed83c --- /dev/null +++ b/packages/nodes-base/nodes/ConvertKit/TagSubscriberDescription.ts @@ -0,0 +1,219 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const tagSubscriberOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'tagSubscriber', + ], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + description: 'Add a tag to a subscriber', + }, + { + name: 'Get All', + value: 'getAll', + description: 'List subscriptions to a tag including subscriber data', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a tag from a subscriber', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const tagSubscriberFields = [ + { + displayName: 'Tag ID', + name: 'tagId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + required: true, + displayOptions: { + show: { + resource: [ + 'tagSubscriber', + ], + operation: [ + 'add', + 'getAll', + 'delete', + ], + }, + }, + default: '', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'tagSubscriber', + ], + operation: [ + 'add', + 'delete', + ], + }, + }, + default: '', + description: 'Subscriber email address.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'tagSubscriber', + ], + operation: [ + 'add', + ], + }, + }, + options: [ + { + displayName: 'Custom Fields', + name: 'fields', + placeholder: 'Add Custom Field', + description: 'Object of key/value pairs for custom fields (the custom field must exist before you can use it here).', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'field', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field Key', + name: 'key', + type: 'string', + default: '', + placeholder: 'last_name', + description: `The field's key.`, + }, + { + displayName: 'Field Value', + name: 'value', + type: 'string', + default: '', + placeholder: 'Doe', + description: 'Value of the field.', + }, + ], + }, + ], + }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: 'Subscriber first name.', + }, + ], + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'tagSubscriber', + ], + }, + }, + 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: [ + 'tagSubscriber', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'tagSubscriber', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Subscriber State', + name: 'subscriberState', + type: 'options', + options: [ + { + name: 'Active', + value: 'active', + }, + { + name: 'Cancelled', + value: 'cancelled', + }, + ], + default: 'active', + }, + ], + description: 'Receive only active subscribers or cancelled subscribers.', + }, +] as INodeProperties[]; From 833001a9e46055fa3a805a390cb90c68b56c5e1a Mon Sep 17 00:00:00 2001 From: ricardo Date: Thu, 6 Aug 2020 13:33:43 -0400 Subject: [PATCH 08/39] :zap: Improvements to ConvertKit-Trigger --- .../ConvertKit/ConvertKitTrigger.node.ts | 274 ++++++++++++++++-- .../nodes/ConvertKit/GenericFunctions.ts | 13 +- .../nodes/ConvertKit/SequenceDescription.ts | 6 +- 3 files changed, 255 insertions(+), 38 deletions(-) diff --git a/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts b/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts index 5b1c6c22745..cb5bdbeb2ce 100644 --- a/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts +++ b/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts @@ -8,12 +8,17 @@ import { INodeTypeDescription, INodeType, IWebhookResponseData, + ILoadOptionsFunctions, + INodePropertyOptions, } from 'n8n-workflow'; import { convertKitApiRequest, } from './GenericFunctions'; +import { + snakeCase, +} from 'change-case'; export class ConvertKitTrigger implements INodeType { description: INodeTypeDescription = { @@ -34,7 +39,7 @@ export class ConvertKitTrigger implements INodeType { { name: 'convertKitApi', required: true, - } + }, ], webhooks: [ { @@ -50,21 +55,86 @@ export class ConvertKitTrigger implements INodeType { name: 'event', type: 'options', required: true, - default: 'subscriberActivated', + default: '', description: 'The events that can trigger the webhook and whether they are enabled.', options: [ { - name: 'Subscriber Activated', - value: 'subscriberActivated', - description: 'Whether the webhook is triggered when a subscriber is activated.', + name: 'Form Subscribe', + value: 'formSubscribe', }, { - name: 'Link Clicked', - value: 'linkClicked', - description: 'Whether the webhook is triggered when a link is clicked.', + name: 'Link Click', + value: 'linkClick', + }, + { + name: 'Product Purchase', + value: 'productPurchase', + }, + { + name: 'Purchase Created', + value: 'purchaseCreate', + }, + { + name: 'Sequence Complete', + value: 'courseComplete', + }, + { + name: 'Sequence Subscribe', + value: 'courseSubscribe', + }, + { + name: 'Subscriber Activated', + value: 'subscriberActivate', + }, + { + name: 'Subscriber Unsubscribe', + value: 'subscriberUnsubscribe', + }, + { + name: 'Tag Add', + value: 'tagAdd', + }, + { + name: 'Tag Remove', + value: 'tagRemove', }, ], }, + { + displayName: 'Form ID', + name: 'formId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getForms', + }, + required: true, + default: '', + displayOptions: { + show: { + event: [ + 'formSubscribe', + ], + }, + }, + }, + { + displayName: 'Sequence ID', + name: 'courseId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getSequences', + }, + required: true, + default: '', + displayOptions: { + show: { + event: [ + 'courseSubscribe', + 'courseComplete', + ], + }, + }, + }, { displayName: 'Initiating Link', name: 'link', @@ -75,7 +145,39 @@ export class ConvertKitTrigger implements INodeType { displayOptions: { show: { event: [ - 'linkClicked', + 'linkClick', + ], + }, + }, + }, + { + displayName: 'Product ID', + name: 'productId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + event: [ + 'productPurchase', + ], + }, + }, + }, + { + displayName: 'Tag ID', + name: 'tagId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + required: true, + default: '', + displayOptions: { + show: { + event: [ + 'tagAdd', + 'tagRemove', ], }, }, @@ -83,73 +185,181 @@ export class ConvertKitTrigger implements INodeType { ], }; + 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 convertKitApiRequest.call(this, 'GET', '/tags'); + + for (const tag of tags) { + + const tagName = tag.name; + + const tagId = tag.id; + + returnData.push({ + name: tagName, + value: tagId, + }); + } + + return returnData; + }, + // Get all the forms to display them to user so that he can + // select them easily + async getForms(this: ILoadOptionsFunctions): Promise { + + const returnData: INodePropertyOptions[] = []; + + const { forms } = await convertKitApiRequest.call(this, 'GET', '/forms'); + + for (const form of forms) { + + const formName = form.name; + + const formId = form.id; + + returnData.push({ + name: formName, + value: formId, + }); + } + + return returnData; + }, + + // Get all the sequences to display them to user so that he can + // select them easily + async getSequences(this: ILoadOptionsFunctions): Promise { + + const returnData: INodePropertyOptions[] = []; + + const { courses } = await convertKitApiRequest.call(this, 'GET', '/sequences'); + + for (const course of courses) { + + const courseName = course.name; + + const courseId = course.id; + + returnData.push({ + name: courseName, + value: courseId, + }); + } + + return returnData; + }, + } + }; + // @ts-ignore (because of request) webhookMethods = { default: { async checkExists(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + // THe API does not have an endpoint to list all webhooks + if(webhookData.webhookId) { return true; } + return false; }, async create(this: IHookFunctions): Promise { - let webhook; + const webhookUrl = this.getNodeWebhookUrl('default'); - const event = this.getNodeParameter('event', 0); + + let event = this.getNodeParameter('event', 0) as string; + const endpoint = '/automations/hooks'; - const qs: IDataObject = {}; + if (event === 'purchaseCreate') { - try { - qs.target_url = webhookUrl; + event = `purchase.${snakeCase(event)}`; - if(event === 'subscriberActivated') { - qs.event = { - name: 'subscriber.subscriber_activate', - }; - } else if(event === 'linkClicked') { - const link = this.getNodeParameter('link', 0) as string; - qs.event = { - name: 'subscriber.link_click', - initiator_value: link, - }; - } - webhook = await convertKitApiRequest.call(this, 'POST', endpoint, {}, qs); - } catch (error) { - throw error; + } else { + + event = `subscriber.${snakeCase(event)}`; } + const body: IDataObject = { + target_url: webhookUrl as string, + event: { + name: event + }, + }; + + if (event === 'subscriber.form_subscribe') { + //@ts-ignore + body.event['form_id'] = this.getNodeParameter('formId', 0); + } + + if (event === 'subscriber.course_subscribe' || event === 'subscriber.course_complete') { + //@ts-ignore + body.event['sequence_id'] = this.getNodeParameter('courseId', 0); + } + + if (event === 'subscriber.link_click') { + //@ts-ignore + body.event['initiator_value'] = this.getNodeParameter('link', 0); + } + + if (event === 'subscriber.product_purchase') { + //@ts-ignore + body.event['product_id'] = this.getNodeParameter('productId', 0); + } + + if (event === 'subscriber.tag_add' || event === 'subscriber.tag_remove"') { + //@ts-ignore + body.event['tag_id'] = this.getNodeParameter('tagId', 0); + } + + const webhook = await convertKitApiRequest.call(this, 'POST', endpoint, body); + if (webhook.rule.id === undefined) { return false; } const webhookData = this.getWorkflowStaticData('node'); + webhookData.webhookId = webhook.rule.id as string; - webhookData.events = event; + return true; }, async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId !== undefined) { + const endpoint = `/automations/hooks/${webhookData.webhookId}`; + try { - await convertKitApiRequest.call(this, 'DELETE', endpoint, {}, {}); + + await convertKitApiRequest.call(this, 'DELETE', endpoint); + } catch (error) { + return false; } + delete webhookData.webhookId; - delete webhookData.events; } + return true; }, }, }; - async webhook(this: IWebhookFunctions): Promise { const returnData: IDataObject[] = []; returnData.push(this.getBodyData()); diff --git a/packages/nodes-base/nodes/ConvertKit/GenericFunctions.ts b/packages/nodes-base/nodes/ConvertKit/GenericFunctions.ts index 4844045e35b..81c8e43b327 100644 --- a/packages/nodes-base/nodes/ConvertKit/GenericFunctions.ts +++ b/packages/nodes-base/nodes/ConvertKit/GenericFunctions.ts @@ -39,12 +39,19 @@ export async function convertKitApiRequest(this: IExecuteFunctions | IExecuteSin delete options.body; } - console.log(options); + // it's a webhook so include the api secret on the body + if ((options.uri as string).includes('/automations/hooks')) { + options.body['api_secret'] = credentials.apiSecret; + } else { + qs.api_secret = credentials.apiSecret; + } + + if (Object.keys(options.qs).length === 0) { + delete options.qs; + } try { - qs.api_secret = credentials.apiSecret; - return await this.helpers.request!(options); } catch (error) { diff --git a/packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts b/packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts index 493b713cdac..5f1186cd55c 100644 --- a/packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts +++ b/packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts @@ -18,17 +18,17 @@ export const sequenceOperations = [ { name: 'Add Subscriber', value: 'addSubscriber', - description: 'Add a subscriber.', + description: 'Add a subscriber', }, { name: 'Get All', value: 'getAll', - description: 'Get all sequences.', + description: 'Get all sequences', }, { name: 'Get Subscriptions', value: 'getSubscriptions', - description: 'List subscriptions to a sequence including subscriber data.', + description: 'Get all subscriptions to a sequence including subscriber data', }, ], default: 'addSubscriber', From b3a85871068c0df1c143eefbe4cd78a88d70a116 Mon Sep 17 00:00:00 2001 From: ricardo Date: Mon, 10 Aug 2020 09:41:10 -0400 Subject: [PATCH 09/39] :zap: Improvements --- .../credentials/ConvertKitApi.credentials.ts | 2 +- .../ConvertKit/ConvertKitTrigger.node.ts | 4 ++- .../nodes/ConvertKit/FormDescription.ts | 36 +++++++++---------- .../nodes/ConvertKit/SequenceDescription.ts | 36 +++++++++---------- 4 files changed, 40 insertions(+), 38 deletions(-) diff --git a/packages/nodes-base/credentials/ConvertKitApi.credentials.ts b/packages/nodes-base/credentials/ConvertKitApi.credentials.ts index 1685c11a244..d7e86975556 100644 --- a/packages/nodes-base/credentials/ConvertKitApi.credentials.ts +++ b/packages/nodes-base/credentials/ConvertKitApi.credentials.ts @@ -6,7 +6,7 @@ import { export class ConvertKitApi implements ICredentialType { name = 'convertKitApi'; - displayName = 'ConvertKit Api'; + displayName = 'ConvertKit API'; properties = [ { displayName: 'API Secret', diff --git a/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts b/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts index cb5bdbeb2ce..0332facf72f 100644 --- a/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts +++ b/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts @@ -297,6 +297,8 @@ export class ConvertKitTrigger implements INodeType { }, }; + console.log(event); + if (event === 'subscriber.form_subscribe') { //@ts-ignore body.event['form_id'] = this.getNodeParameter('formId', 0); @@ -317,7 +319,7 @@ export class ConvertKitTrigger implements INodeType { body.event['product_id'] = this.getNodeParameter('productId', 0); } - if (event === 'subscriber.tag_add' || event === 'subscriber.tag_remove"') { + if (event === 'subscriber.tag_add' || event === 'subscriber.tag_remove') { //@ts-ignore body.event['tag_id'] = this.getNodeParameter('tagId', 0); } diff --git a/packages/nodes-base/nodes/ConvertKit/FormDescription.ts b/packages/nodes-base/nodes/ConvertKit/FormDescription.ts index f6e1a6f3e5e..a8496805bc5 100644 --- a/packages/nodes-base/nodes/ConvertKit/FormDescription.ts +++ b/packages/nodes-base/nodes/ConvertKit/FormDescription.ts @@ -37,24 +37,6 @@ export const formOperations = [ ] as INodeProperties[]; export const formFields = [ - { - displayName: 'Email', - name: 'email', - type: 'string', - required: true, - displayOptions: { - show: { - resource: [ - 'form', - ], - operation: [ - 'addSubscriber', - ], - }, - }, - default: '', - description: `The subscriber's email address.`, - }, { displayName: 'Form ID', name: 'id', @@ -77,6 +59,24 @@ export const formFields = [ default: '', description: 'Form ID.', }, + { + displayName: 'Email', + name: 'email', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'form', + ], + operation: [ + 'addSubscriber', + ], + }, + }, + default: '', + description: `The subscriber's email address.`, + }, { displayName: 'Additional Fields', name: 'additionalFields', diff --git a/packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts b/packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts index 5f1186cd55c..42437cc155c 100644 --- a/packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts +++ b/packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts @@ -37,24 +37,6 @@ export const sequenceOperations = [ ] as INodeProperties[]; export const sequenceFields = [ - { - displayName: 'Email', - name: 'email', - type: 'string', - required: true, - displayOptions: { - show: { - resource: [ - 'sequence', - ], - operation: [ - 'addSubscriber', - ], - }, - }, - default: '', - description: `The subscriber's email address.`, - }, { displayName: 'Sequence ID', name: 'id', @@ -77,6 +59,24 @@ export const sequenceFields = [ default: '', description: 'Sequence ID.', }, + { + displayName: 'Email', + name: 'email', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'sequence', + ], + operation: [ + 'addSubscriber', + ], + }, + }, + default: '', + description: `The subscriber's email address.`, + }, { displayName: 'Return All', name: 'returnAll', From 4568eb21cc0a43f90725a397a549de95e65bdff9 Mon Sep 17 00:00:00 2001 From: ricardo Date: Mon, 10 Aug 2020 09:43:21 -0400 Subject: [PATCH 10/39] :zap: small fix --- packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts b/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts index 0332facf72f..580bab7d565 100644 --- a/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts +++ b/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts @@ -297,8 +297,6 @@ export class ConvertKitTrigger implements INodeType { }, }; - console.log(event); - if (event === 'subscriber.form_subscribe') { //@ts-ignore body.event['form_id'] = this.getNodeParameter('formId', 0); From 327abd943f3e41e1d074d3ba33dce9fe2c5e237e Mon Sep 17 00:00:00 2001 From: ricardo Date: Fri, 21 Aug 2020 18:04:12 -0400 Subject: [PATCH 11/39] :zap: Add search operation to contact resource --- .../nodes/Hubspot/ContactDescription.ts | 263 ++++++++++++++++++ .../nodes-base/nodes/Hubspot/Hubspot.node.ts | 47 ++++ 2 files changed, 310 insertions(+) diff --git a/packages/nodes-base/nodes/Hubspot/ContactDescription.ts b/packages/nodes-base/nodes/Hubspot/ContactDescription.ts index 60d2be5d706..015f3d30cf6 100644 --- a/packages/nodes-base/nodes/Hubspot/ContactDescription.ts +++ b/packages/nodes-base/nodes/Hubspot/ContactDescription.ts @@ -40,6 +40,11 @@ export const contactOperations = [ value: 'getRecentlyCreatedUpdated', description: 'Get recently created/updated contacts', }, + { + name: 'Search', + value: 'search', + description: 'Search contacts', + }, ], default: 'upsert', description: 'The operation to perform.', @@ -834,4 +839,262 @@ export const contactFields = [ }, ], }, + +//*-------------------------------------------------------------------------- */ +/* contact:search */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'search', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'search', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 250, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Sort By', + name: 'sorts', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getContactProperties', + }, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'search', + ], + }, + }, + default: [], + }, + { + displayName: 'Direction', + name: 'direction', + type: 'options', + options: [ + { + name: 'ASC', + value: 'ASCENDING', + }, + { + name: 'DESC', + value: 'DESCENDING', + }, + ], + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'search', + ], + }, + }, + default: 'DESCENDING', + description: 'Defines the direction in which search results are ordered. Default value is DESC.', + }, + { + displayName: 'Filter Groups', + name: 'filterGroupsUi', + type: 'fixedCollection', + default: '', + placeholder: 'Add Filter Group', + typeOptions: { + multipleValues: true, + }, + required: false, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'search', + ], + }, + }, + options: [ + { + name: 'filterGroupsValues', + displayName: 'Filter Group', + values: [ + { + displayName: 'Filters', + name: 'filtersUi', + type: 'fixedCollection', + default: '', + placeholder: 'Add Filter', + typeOptions: { + multipleValues: true, + }, + required: false, + options: [ + { + name: 'filterValues', + displayName: 'Filter', + values: [ + { + displayName: 'Property Name', + name: 'propertyName', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getContactProperties', + }, + default: '', + }, + { + displayName: 'Operator', + name: 'operator', + type: 'options', + options: [ + { + name: 'Equal', + value: 'EQ', + }, + { + name: 'Not Equal', + value: 'NEQ', + }, + { + name: 'Less Than', + value: 'LT', + }, + { + name: 'Less Than Or Equal', + value: 'LTE', + }, + { + name: 'Greater Than', + value: 'GT', + }, + { + name: 'Greater Than Or Equal', + value: 'GTE', + }, + { + name: 'Is Known', + value: 'HAS_PROPERTY', + }, + { + name: 'Is Unknown', + value: 'NOT_HAS_PROPERTY', + }, + { + name: 'Contains Exactly', + value: 'CONSTAIN_TOKEN', + }, + { + name: `Doesn't Contain Exactly`, + value: 'NOT_CONSTAIN_TOKEN', + }, + ], + default: 'EQ', + }, + { + displayName: 'Value', + name: 'value', + displayOptions: { + hide: { + operator: [ + 'HAS_PROPERTY', + 'NOT_HAS_PROPERTY', + ], + }, + }, + type: 'string', + default: '', + }, + ], + } + ], + description: 'Use filters to limit the results to only CRM objects with matching property values. More info here', + }, + ], + } + ], + description: `When multiple filters are provided within a filterGroup, they will be combined using a logical AND operator.
+ When multiple filterGroups are provided, they will be combined using a logical OR operator.
+ The system supports a maximum of three filterGroups with up to three filters each.
+ More info here` + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'search', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'properties', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getContactProperties', + }, + default: [ + 'firstname', + 'lastname', + 'email', + ], + description: `Used to include specific company properties in the results.
+ By default, the results will only include company ID and will not include the values for any properties for your companys.
+ Including this parameter will include the data for the specified property in the results.
+ You can include this parameter multiple times to request multiple properties separed by ,.`, + }, + { + displayName: 'Query', + name: 'query', + type: 'string', + default: '', + description: 'Perform a text search against all property values for an object type', + }, + ], + }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts index 458a974837d..c6cb238903c 100644 --- a/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts +++ b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts @@ -1166,6 +1166,53 @@ export class Hubspot implements INodeType { const endpoint = `/contacts/v1/contact/vid/${contactId}`; responseData = await hubspotApiRequest.call(this, 'DELETE', endpoint); } + //https://developers.hubspot.com/docs/api/crm/search + if (operation === 'search') { + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + const filtersGroupsUi = this.getNodeParameter('filterGroupsUi', i) as IDataObject; + const sorts = this.getNodeParameter('sorts', i) as string; + const direction = this.getNodeParameter('direction', i) as string; + + const body: IDataObject = { + sorts: [ + { + propertyName: sorts, + direction, + }, + ], + }; + + if (filtersGroupsUi) { + const filterGroupValues = (filtersGroupsUi as IDataObject).filterGroupsValues as IDataObject[]; + if (filterGroupValues) { + body.filterGroups = []; + for (const filterGroupValue of filterGroupValues) { + if (filterGroupValue.filtersUi) { + const filterValues = (filterGroupValue.filtersUi as IDataObject).filterValues as IDataObject[]; + if (filterValues) { + //@ts-ignore + body.filterGroups.push({ filters: filterValues }); + } + } + } + } + } + + Object.assign(body, additionalFields); + + const endpoint = '/crm/v3/objects/contacts/search'; + + if (returnAll) { + + responseData = await hubspotApiRequestAllItems.call(this, 'results', 'POST', endpoint, body, qs); + + } else { + qs.count = this.getNodeParameter('limit', 0) as number; + responseData = await hubspotApiRequest.call(this, 'POST', endpoint, body, qs); + responseData = responseData.results; + } + } } //https://developers.hubspot.com/docs/methods/companies/companies-overview if (resource === 'company') { From f5c8fbe9533dcb2127b4aa2752ef562bac14487a Mon Sep 17 00:00:00 2001 From: Tanay Pant Date: Mon, 24 Aug 2020 10:50:37 +0200 Subject: [PATCH 12/39] :hammer: Fix pronunciation guide --- README.md | 4 ++-- docker/images/n8n/README.md | 4 ++-- packages/cli/README.md | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0896adac6eb..0150479ff05 100644 --- a/README.md +++ b/README.md @@ -63,9 +63,9 @@ check out our job posts: -## What does n8n mean and how do you pronounce it +## What does n8n mean and how do you pronounce it? -**Short answer:** It means "nodemation" +**Short answer:** It means "nodemation" and it is pronounced as n-eight-n. **Long answer:** I get that question quite often (more often than I expected) so I decided it is probably best to answer it here. While looking for a diff --git a/docker/images/n8n/README.md b/docker/images/n8n/README.md index f3c662d5744..ab8c4b56ed3 100644 --- a/docker/images/n8n/README.md +++ b/docker/images/n8n/README.md @@ -260,9 +260,9 @@ docker build --build-arg N8N_VERSION=0.18.1 -t n8nio/n8n:0.18.1 . ``` -## What does n8n mean and how do you pronounce it +## What does n8n mean and how do you pronounce it? -**Short answer:** It means "nodemation" +**Short answer:** It means "nodemation" and it is pronounced as n-eight-n. **Long answer:** I get that question quite often (more often than I expected) so I decided it is probably best to answer it here. While looking for a diff --git a/packages/cli/README.md b/packages/cli/README.md index b67c5cc524d..b7cbd5417db 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -60,9 +60,9 @@ If you are interested in a hosted version of n8n on our infrastructure please co -## What does n8n mean and how do you pronounce it +## What does n8n mean and how do you pronounce it? -**Short answer:** It means "nodemation" +**Short answer:** It means "nodemation" and it is pronounced as n-eight-n. **Long answer:** I get that question quite often (more often than I expected) so I decided it is probably best to answer it here. While looking for a From 4c075db26c1cfe774ac36cb764505db20b6ce1bb Mon Sep 17 00:00:00 2001 From: ricardo Date: Mon, 24 Aug 2020 20:55:57 -0400 Subject: [PATCH 13/39] :zap: Add card comment operation --- .../nodes/Airtable/Airtable.node.ts | 2 +- .../nodes/Trello/AttachmentDescription.ts | 4 +- .../nodes/Trello/BoardDescription.ts | 4 +- .../nodes/Trello/CardCommentDescription.ts | 177 +++++++++++ .../nodes/Trello/CardDescription.ts | 4 +- .../nodes/Trello/ChecklistDescription.ts | 4 +- .../nodes/Trello/GenericFunctions.ts | 8 +- .../nodes/Trello/LabelDescription.ts | 4 +- .../nodes/Trello/ListDescription.ts | 295 ++++++++++-------- .../nodes-base/nodes/Trello/Trello.node.ts | 108 ++++++- 10 files changed, 463 insertions(+), 147 deletions(-) create mode 100644 packages/nodes-base/nodes/Trello/CardCommentDescription.ts diff --git a/packages/nodes-base/nodes/Airtable/Airtable.node.ts b/packages/nodes-base/nodes/Airtable/Airtable.node.ts index 25790c01e9a..acdee495e47 100644 --- a/packages/nodes-base/nodes/Airtable/Airtable.node.ts +++ b/packages/nodes-base/nodes/Airtable/Airtable.node.ts @@ -367,7 +367,7 @@ export class Airtable implements INodeType { const operation = this.getNodeParameter('operation', 0) as string; const application = this.getNodeParameter('application', 0) as string; - const table = this.getNodeParameter('table', 0) as string; + const table = encodeURI(this.getNodeParameter('table', 0) as string); let returnAll = false; let endpoint = ''; diff --git a/packages/nodes-base/nodes/Trello/AttachmentDescription.ts b/packages/nodes-base/nodes/Trello/AttachmentDescription.ts index 4306b2ed118..9b473a9a656 100644 --- a/packages/nodes-base/nodes/Trello/AttachmentDescription.ts +++ b/packages/nodes-base/nodes/Trello/AttachmentDescription.ts @@ -1,4 +1,6 @@ -import { INodeProperties } from "n8n-workflow"; +import { + INodeProperties, +} from "n8n-workflow"; export const attachmentOperations = [ // ---------------------------------- diff --git a/packages/nodes-base/nodes/Trello/BoardDescription.ts b/packages/nodes-base/nodes/Trello/BoardDescription.ts index b089d689cef..d9b0eead48c 100644 --- a/packages/nodes-base/nodes/Trello/BoardDescription.ts +++ b/packages/nodes-base/nodes/Trello/BoardDescription.ts @@ -1,4 +1,6 @@ -import { INodeProperties } from "n8n-workflow"; +import { + INodeProperties, +} from "n8n-workflow"; export const boardOperations = [ // ---------------------------------- diff --git a/packages/nodes-base/nodes/Trello/CardCommentDescription.ts b/packages/nodes-base/nodes/Trello/CardCommentDescription.ts new file mode 100644 index 00000000000..1ce873f16ea --- /dev/null +++ b/packages/nodes-base/nodes/Trello/CardCommentDescription.ts @@ -0,0 +1,177 @@ +import { + INodeProperties, +} from "n8n-workflow"; + +export const cardCommentOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'cardComment', + ], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + description: 'Add a comment to a card', + }, + { + name: 'Revove', + value: 'remove', + description: 'Remove a comment from a card', + }, + { + name: 'Update', + value: 'update', + description: 'Update a comment in a card', + }, + ], + default: 'add', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const cardCommentFields = [ + // ---------------------------------- + // cardComment:add + // ---------------------------------- + { + displayName: 'Card ID', + name: 'cardId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'add', + ], + resource: [ + 'cardComment', + ], + }, + }, + description: 'The id of the card', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'add', + ], + resource: [ + 'cardComment', + ], + }, + }, + description: 'Text of the comment', + }, + + // ---------------------------------- + // cardComment:remove + // ---------------------------------- + { + displayName: 'Card ID', + name: 'cardId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'remove', + ], + resource: [ + 'cardComment', + ], + }, + }, + description: 'The ID of the card.', + }, + { + displayName: 'Comment ID', + name: 'commentId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'remove', + ], + resource: [ + 'cardComment', + ], + }, + }, + description: 'The ID of the comment to delete.', + }, + + // ---------------------------------- + // cardComment:update + // ---------------------------------- + { + displayName: 'Card ID', + name: 'cardId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'cardComment', + ], + }, + }, + description: 'The ID of the card to update.', + }, + { + displayName: 'Comment ID', + name: 'commentId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'cardComment', + ], + }, + }, + description: 'The ID of the comment to delete.', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'cardComment', + ], + }, + }, + description: 'Text of the comment', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Trello/CardDescription.ts b/packages/nodes-base/nodes/Trello/CardDescription.ts index 6b5158e6d15..0352e3e2d03 100644 --- a/packages/nodes-base/nodes/Trello/CardDescription.ts +++ b/packages/nodes-base/nodes/Trello/CardDescription.ts @@ -1,4 +1,6 @@ -import { INodeProperties } from "n8n-workflow"; +import { + INodeProperties, +} from "n8n-workflow"; export const cardOperations = [ // ---------------------------------- diff --git a/packages/nodes-base/nodes/Trello/ChecklistDescription.ts b/packages/nodes-base/nodes/Trello/ChecklistDescription.ts index be11ba6f178..a8ecb03e6db 100644 --- a/packages/nodes-base/nodes/Trello/ChecklistDescription.ts +++ b/packages/nodes-base/nodes/Trello/ChecklistDescription.ts @@ -1,4 +1,6 @@ -import { INodeProperties } from "n8n-workflow"; +import { + INodeProperties, +} from 'n8n-workflow'; export const checklistOperations = [ // ---------------------------------- diff --git a/packages/nodes-base/nodes/Trello/GenericFunctions.ts b/packages/nodes-base/nodes/Trello/GenericFunctions.ts index 831d3f3b430..b7c65c242bf 100644 --- a/packages/nodes-base/nodes/Trello/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Trello/GenericFunctions.ts @@ -4,9 +4,13 @@ import { ILoadOptionsFunctions, } from 'n8n-core'; -import { OptionsWithUri } from 'request'; -import { IDataObject } from 'n8n-workflow'; +import { + OptionsWithUri, +} from 'request'; +import { + IDataObject, +} from 'n8n-workflow'; /** * Make an API request to Trello diff --git a/packages/nodes-base/nodes/Trello/LabelDescription.ts b/packages/nodes-base/nodes/Trello/LabelDescription.ts index 2b938ae5de7..3aaa9180988 100644 --- a/packages/nodes-base/nodes/Trello/LabelDescription.ts +++ b/packages/nodes-base/nodes/Trello/LabelDescription.ts @@ -1,4 +1,6 @@ -import { INodeProperties } from "n8n-workflow"; +import { + INodeProperties, +} from 'n8n-workflow'; export const labelOperations = [ // ---------------------------------- diff --git a/packages/nodes-base/nodes/Trello/ListDescription.ts b/packages/nodes-base/nodes/Trello/ListDescription.ts index b6b45656321..105f841144e 100644 --- a/packages/nodes-base/nodes/Trello/ListDescription.ts +++ b/packages/nodes-base/nodes/Trello/ListDescription.ts @@ -1,42 +1,46 @@ -import { INodeProperties } from "n8n-workflow"; +import { + INodeProperties, +} from 'n8n-workflow'; export const listOperations = [ // ---------------------------------- // list // ---------------------------------- { - displayName: "Operation", - name: "operation", - type: "options", + displayName: 'Operation', + name: 'operation', + type: 'options', displayOptions: { show: { - resource: ["list"] - } + resource: [ + 'list', + ], + }, }, options: [ { - name: "Archive", - value: "archive", - description: "Archive/Unarchive a list" + name: 'Archive', + value: 'archive', + description: 'Archive/Unarchive a list' }, { - name: "Create", - value: "create", - description: "Create a new list" + name: 'Create', + value: 'create', + description: 'Create a new list' }, { - name: "Get", - value: "get", - description: "Get the data of a list" + name: 'Get', + value: 'get', + description: 'Get the data of a list' }, { - name: "Update", - value: "update", - description: "Update a list" + name: 'Update', + value: 'update', + description: 'Update a list' } ], - default: "create", - description: "The operation to perform." + default: 'create', + description: 'The operation to perform.' } ] as INodeProperties[]; @@ -45,92 +49,112 @@ export const listFields = [ // list:archive // ---------------------------------- { - displayName: "List ID", - name: "id", - type: "string", - default: "", + displayName: 'List ID', + name: 'id', + type: 'string', + default: '', required: true, displayOptions: { show: { - operation: ["archive"], - resource: ["list"] - } + operation: [ + 'archive', + ], + resource: [ + 'list', + ], + }, }, - description: "The ID of the list to archive or unarchive." + description: 'The ID of the list to archive or unarchive.' }, { - displayName: "Archive", - name: "archive", - type: "boolean", + displayName: 'Archive', + name: 'archive', + type: 'boolean', default: false, displayOptions: { show: { - operation: ["archive"], - resource: ["list"] - } + operation: [ + 'archive', + ], + resource: [ + 'list', + ], + }, }, - description: "If the list should be archived or unarchived." + description: 'If the list should be archived or unarchived.' }, // ---------------------------------- // list:create // ---------------------------------- { - displayName: "Board ID", - name: "idBoard", - type: "string", - default: "", + displayName: 'Board ID', + name: 'idBoard', + type: 'string', + default: '', required: true, displayOptions: { show: { - operation: ["create"], - resource: ["list"] - } + operation: [ + 'create', + ], + resource: [ + 'list', + ], + }, }, - description: "The ID of the board the list should be created in" + description: 'The ID of the board the list should be created in' }, { - displayName: "Name", - name: "name", - type: "string", - default: "", - placeholder: "My list", + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + placeholder: 'My list', required: true, displayOptions: { show: { - operation: ["create"], - resource: ["list"] - } + operation: [ + 'create', + ], + resource: [ + 'list', + ], + }, }, - description: "The name of the list" + description: 'The name of the list' }, { - displayName: "Additional Fields", - name: "additionalFields", - type: "collection", - placeholder: "Add Field", + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', displayOptions: { show: { - operation: ["create"], - resource: ["list"] - } + operation: [ + 'create', + ], + resource: [ + 'list', + ], + }, }, default: {}, options: [ { - displayName: "List Source", - name: "idListSource", - type: "string", - default: "", - description: "ID of the list to copy into the new list." + displayName: 'List Source', + name: 'idListSource', + type: 'string', + default: '', + description: 'ID of the list to copy into the new list.' }, { - displayName: "Position", - name: "pos", - type: "string", - default: "bottom", + displayName: 'Position', + name: 'pos', + type: 'string', + default: 'bottom', description: - "The position of the new list. top, bottom, or a positive float." + 'The position of the new list. top, bottom, or a positive float.' } ] }, @@ -139,39 +163,46 @@ export const listFields = [ // list:get // ---------------------------------- { - displayName: "List ID", - name: "id", - type: "string", - default: "", + displayName: 'List ID', + name: 'id', + type: 'string', + default: '', required: true, displayOptions: { show: { - operation: ["get"], - resource: ["list"] - } + operation: [ + 'get', + ], + resource: [ + 'list', + ], + }, }, - description: "The ID of the list to get." + description: 'The ID of the list to get.' }, { - displayName: "Additional Fields", - name: "additionalFields", - type: "collection", - placeholder: "Add Field", + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', displayOptions: { show: { - operation: ["get"], - resource: ["list"] - } + operation: [ + 'get', + ], + resource: [ + 'list', + ], + }, }, default: {}, options: [ { - displayName: "Fields", - name: "fields", - type: "string", - default: "all", - description: - 'Fields to return. Either "all" or a comma-separated list of fields.' + displayName: 'Fields', + name: 'fields', + type: 'string', + default: 'all', + description: 'Fields to return. Either "all" or a comma-separated list of fields.' } ] }, @@ -180,67 +211,75 @@ export const listFields = [ // list:update // ---------------------------------- { - displayName: "List ID", - name: "id", - type: "string", - default: "", + displayName: 'List ID', + name: 'id', + type: 'string', + default: '', required: true, displayOptions: { show: { - operation: ["update"], - resource: ["list"] - } + operation: [ + 'update', + ], + resource: [ + 'list', + ], + }, }, - description: "The ID of the list to update." + description: 'The ID of the list to update.' }, { - displayName: "Update Fields", - name: "updateFields", - type: "collection", - placeholder: "Add Field", + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', displayOptions: { show: { - operation: ["update"], - resource: ["list"] - } + operation: [ + 'update', + ], + resource: [ + 'list', + ], + }, }, default: {}, options: [ { - displayName: "Board ID", - name: "idBoard", - type: "string", - default: "", - description: "ID of a board the list should be moved to." + displayName: 'Board ID', + name: 'idBoard', + type: 'string', + default: '', + description: 'ID of a board the list should be moved to.' }, { - displayName: "Closed", - name: "closed", - type: "boolean", + displayName: 'Closed', + name: 'closed', + type: 'boolean', default: false, - description: "Whether the list is closed." + description: 'Whether the list is closed.' }, { - displayName: "Name", - name: "name", - type: "string", - default: "", - description: "New name of the list" + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'New name of the list' }, { - displayName: "Position", - name: "pos", - type: "string", - default: "bottom", + displayName: 'Position', + name: 'pos', + type: 'string', + default: 'bottom', description: - "The position of the list. top, bottom, or a positive float." + 'The position of the list. top, bottom, or a positive float.' }, { - displayName: "Subscribed", - name: "subscribed", - type: "boolean", + displayName: 'Subscribed', + name: 'subscribed', + type: 'boolean', default: false, - description: "Whether the acting user is subscribed to the list." + description: 'Whether the acting user is subscribed to the list.' } ] } diff --git a/packages/nodes-base/nodes/Trello/Trello.node.ts b/packages/nodes-base/nodes/Trello/Trello.node.ts index 527c94bfb16..66ba233ddb3 100644 --- a/packages/nodes-base/nodes/Trello/Trello.node.ts +++ b/packages/nodes-base/nodes/Trello/Trello.node.ts @@ -1,4 +1,7 @@ -import { IExecuteFunctions } from "n8n-core"; +import { + IExecuteFunctions, +} from 'n8n-core'; + import { IDataObject, INodeTypeDescription, @@ -6,13 +9,44 @@ import { INodeType, } from 'n8n-workflow'; -import { apiRequest } from "./GenericFunctions"; -import { attachmentOperations, attachmentFields } from './AttachmentDescription'; -import { boardOperations, boardFields } from './BoardDescription'; -import { cardOperations, cardFields } from './CardDescription'; -import { checklistOperations, checklistFields } from './ChecklistDescription'; -import { labelOperations, labelFields } from './LabelDescription'; -import { listOperations, listFields } from './ListDescription'; +import { + apiRequest, +} from './GenericFunctions'; + +import { + attachmentOperations, + attachmentFields, +} from './AttachmentDescription'; + +import { + boardOperations, + boardFields, +} from './BoardDescription'; + +import { + cardOperations, + cardFields, +} from './CardDescription'; + +import { + cardCommentOperations, + cardCommentFields, +} from './CardCommentDescription'; + +import { + checklistOperations, + checklistFields, +} from './ChecklistDescription'; + +import { + labelOperations, + labelFields, +} from './LabelDescription'; + +import { + listOperations, + listFields, +} from './ListDescription'; export class Trello implements INodeType { description: INodeTypeDescription = { @@ -33,7 +67,7 @@ export class Trello implements INodeType { { name: 'trelloApi', required: true, - } + }, ], properties: [ { @@ -53,6 +87,10 @@ export class Trello implements INodeType { name: 'Card', value: 'card', }, + { + name: 'Card Comment', + value: 'cardComment', + }, { name: 'Checklist', value: 'checklist', @@ -76,6 +114,7 @@ export class Trello implements INodeType { ...attachmentOperations, ...boardOperations, ...cardOperations, + ...cardCommentOperations, ...checklistOperations, ...labelOperations, ...listOperations, @@ -86,15 +125,14 @@ export class Trello implements INodeType { ...attachmentFields, ...boardFields, ...cardFields, + ...cardCommentFields, ...checklistFields, ...labelFields, ...listFields ], - }; - async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); const returnData: IDataObject[] = []; @@ -236,6 +274,54 @@ export class Trello implements INodeType { throw new Error(`The operation "${operation}" is not known!`); } + } else if (resource === 'cardComment') { + + if (operation === 'add') { + // ---------------------------------- + // add + // ---------------------------------- + + const cardId = this.getNodeParameter('cardId', i) as string; + + qs.text = this.getNodeParameter('text', i) as string; + + requestMethod = 'POST'; + + endpoint = `cards/${cardId}/actions/comments`; + + + } else if (operation === 'remove') { + // ---------------------------------- + // delete + // ---------------------------------- + + requestMethod = 'DELETE'; + + const cardId = this.getNodeParameter('cardId', i) as string; + + const commentId = this.getNodeParameter('commentId', i) as string; + + endpoint = `/cards/${cardId}/actions/${commentId}/comments`; + + } else if (operation === 'update') { + // ---------------------------------- + // update + // ---------------------------------- + + requestMethod = 'PUT'; + + const cardId = this.getNodeParameter('cardId', i) as string; + + const commentId = this.getNodeParameter('commentId', i) as string; + + qs.text = this.getNodeParameter('text', i) as string; + + endpoint = `cards/${cardId}/actions/${commentId}/comments`; + + } else { + throw new Error(`The operation "${operation}" is not known!`); + } + } else if (resource === 'list') { if (operation === 'archive') { From 98edb353d4ac7dd0914a40968f8caaae982f7e6f Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Tue, 25 Aug 2020 08:27:26 +0200 Subject: [PATCH 14/39] :zap: Minor improvements to Trello-Node --- .../nodes/Trello/AttachmentDescription.ts | 2 +- .../nodes/Trello/BoardDescription.ts | 2 +- .../nodes/Trello/CardCommentDescription.ts | 28 +++++++++---------- .../nodes/Trello/CardDescription.ts | 2 +- .../nodes-base/nodes/Trello/Trello.node.ts | 6 ++-- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/nodes-base/nodes/Trello/AttachmentDescription.ts b/packages/nodes-base/nodes/Trello/AttachmentDescription.ts index 9b473a9a656..1fa0bc58def 100644 --- a/packages/nodes-base/nodes/Trello/AttachmentDescription.ts +++ b/packages/nodes-base/nodes/Trello/AttachmentDescription.ts @@ -1,6 +1,6 @@ import { INodeProperties, -} from "n8n-workflow"; +} from 'n8n-workflow'; export const attachmentOperations = [ // ---------------------------------- diff --git a/packages/nodes-base/nodes/Trello/BoardDescription.ts b/packages/nodes-base/nodes/Trello/BoardDescription.ts index d9b0eead48c..9e75de40ce5 100644 --- a/packages/nodes-base/nodes/Trello/BoardDescription.ts +++ b/packages/nodes-base/nodes/Trello/BoardDescription.ts @@ -1,6 +1,6 @@ import { INodeProperties, -} from "n8n-workflow"; +} from 'n8n-workflow'; export const boardOperations = [ // ---------------------------------- diff --git a/packages/nodes-base/nodes/Trello/CardCommentDescription.ts b/packages/nodes-base/nodes/Trello/CardCommentDescription.ts index 1ce873f16ea..6c4f38fcca5 100644 --- a/packages/nodes-base/nodes/Trello/CardCommentDescription.ts +++ b/packages/nodes-base/nodes/Trello/CardCommentDescription.ts @@ -1,6 +1,6 @@ import { INodeProperties, -} from "n8n-workflow"; +} from 'n8n-workflow'; export const cardCommentOperations = [ { @@ -16,29 +16,29 @@ export const cardCommentOperations = [ }, options: [ { - name: 'Add', - value: 'add', - description: 'Add a comment to a card', + name: 'Create', + value: 'create', + description: 'Create a comment on a card', }, { - name: 'Revove', - value: 'remove', - description: 'Remove a comment from a card', + name: 'Delete', + value: 'delete', + description: 'Delete a comment from a card', }, { name: 'Update', value: 'update', - description: 'Update a comment in a card', + description: 'Update a comment on a card', }, ], - default: 'add', + default: 'create', description: 'The operation to perform.', }, ] as INodeProperties[]; export const cardCommentFields = [ // ---------------------------------- - // cardComment:add + // cardComment:create // ---------------------------------- { displayName: 'Card ID', @@ -49,7 +49,7 @@ export const cardCommentFields = [ displayOptions: { show: { operation: [ - 'add', + 'create', ], resource: [ 'cardComment', @@ -67,7 +67,7 @@ export const cardCommentFields = [ displayOptions: { show: { operation: [ - 'add', + 'create', ], resource: [ 'cardComment', @@ -89,7 +89,7 @@ export const cardCommentFields = [ displayOptions: { show: { operation: [ - 'remove', + 'delete', ], resource: [ 'cardComment', @@ -107,7 +107,7 @@ export const cardCommentFields = [ displayOptions: { show: { operation: [ - 'remove', + 'delete', ], resource: [ 'cardComment', diff --git a/packages/nodes-base/nodes/Trello/CardDescription.ts b/packages/nodes-base/nodes/Trello/CardDescription.ts index 0352e3e2d03..9a9661023e5 100644 --- a/packages/nodes-base/nodes/Trello/CardDescription.ts +++ b/packages/nodes-base/nodes/Trello/CardDescription.ts @@ -1,6 +1,6 @@ import { INodeProperties, -} from "n8n-workflow"; +} from 'n8n-workflow'; export const cardOperations = [ // ---------------------------------- diff --git a/packages/nodes-base/nodes/Trello/Trello.node.ts b/packages/nodes-base/nodes/Trello/Trello.node.ts index 66ba233ddb3..c39b51a6c15 100644 --- a/packages/nodes-base/nodes/Trello/Trello.node.ts +++ b/packages/nodes-base/nodes/Trello/Trello.node.ts @@ -276,9 +276,9 @@ export class Trello implements INodeType { } else if (resource === 'cardComment') { - if (operation === 'add') { + if (operation === 'create') { // ---------------------------------- - // add + // create // ---------------------------------- const cardId = this.getNodeParameter('cardId', i) as string; @@ -290,7 +290,7 @@ export class Trello implements INodeType { endpoint = `cards/${cardId}/actions/comments`; - } else if (operation === 'remove') { + } else if (operation === 'delete') { // ---------------------------------- // delete // ---------------------------------- From ad67d020c062b0a6cc42b8d4ba0b6c7c0d0f4751 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Tue, 25 Aug 2020 09:52:00 +0200 Subject: [PATCH 15/39] :zap: Small improvements to Hubspot-Node --- .../nodes/Hubspot/ContactDescription.ts | 72 +++++++------------ .../nodes-base/nodes/Hubspot/Hubspot.node.ts | 6 +- 2 files changed, 29 insertions(+), 49 deletions(-) diff --git a/packages/nodes-base/nodes/Hubspot/ContactDescription.ts b/packages/nodes-base/nodes/Hubspot/ContactDescription.ts index 015f3d30cf6..7cd76705978 100644 --- a/packages/nodes-base/nodes/Hubspot/ContactDescription.ts +++ b/packages/nodes-base/nodes/Hubspot/ContactDescription.ts @@ -884,52 +884,6 @@ export const contactFields = [ default: 100, description: 'How many results to return.', }, - { - displayName: 'Sort By', - name: 'sorts', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getContactProperties', - }, - displayOptions: { - show: { - resource: [ - 'contact', - ], - operation: [ - 'search', - ], - }, - }, - default: [], - }, - { - displayName: 'Direction', - name: 'direction', - type: 'options', - options: [ - { - name: 'ASC', - value: 'ASCENDING', - }, - { - name: 'DESC', - value: 'DESCENDING', - }, - ], - displayOptions: { - show: { - resource: [ - 'contact', - ], - operation: [ - 'search', - ], - }, - }, - default: 'DESCENDING', - description: 'Defines the direction in which search results are ordered. Default value is DESC.', - }, { displayName: 'Filter Groups', name: 'filterGroupsUi', @@ -1071,6 +1025,23 @@ export const contactFields = [ }, }, options: [ + { + displayName: 'Direction', + name: 'direction', + type: 'options', + options: [ + { + name: 'ASC', + value: 'ASCENDING', + }, + { + name: 'DESC', + value: 'DESCENDING', + }, + ], + default: 'DESCENDING', + description: 'Defines the direction in which search results are ordered. Default value is DESC.', + }, { displayName: 'Fields', name: 'properties', @@ -1095,6 +1066,15 @@ export const contactFields = [ default: '', description: 'Perform a text search against all property values for an object type', }, + { + displayName: 'Sort By', + name: 'sortBy', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getContactProperties', + }, + default: 'createdate', + }, ], }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts index c6cb238903c..cf53fdd3b49 100644 --- a/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts +++ b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts @@ -1171,13 +1171,13 @@ export class Hubspot implements INodeType { const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; const returnAll = this.getNodeParameter('returnAll', 0) as boolean; const filtersGroupsUi = this.getNodeParameter('filterGroupsUi', i) as IDataObject; - const sorts = this.getNodeParameter('sorts', i) as string; - const direction = this.getNodeParameter('direction', i) as string; + const sortBy = additionalFields.sortBy || 'createdate'; + const direction = additionalFields.direction || 'DESCENDING'; const body: IDataObject = { sorts: [ { - propertyName: sorts, + propertyName: sortBy, direction, }, ], From 72102faed5e29aa96fb26217f54d329bf3d6117f Mon Sep 17 00:00:00 2001 From: Rupenieks <32895755+Rupenieks@users.noreply.github.com> Date: Tue, 25 Aug 2020 10:50:39 +0200 Subject: [PATCH 16/39] :sparkles: Add Paddle Integration (#726) * :construction: Resource descriptions * :construction: Node logic / Genericfunctions setup * :construction: Tests / changes * :construction: Changes - Added loadOptions to Payments / Coupon properties for easier item selection - Added exemptions for how data is returned due to inconsistent data return object from API - Other small fixes in main node * :construction: Simplified HTTPS error response * :construction: Added RAW Data options * :fire: Removed order resource - Cannot fetch order without a checkout ID, which can only be obtained via a custom implementation which involves a callback function when a user goes through their checkout process. * :zap: Improvement to Paddle-Node * :zap: Improvements * :zap: Added all currencies, discount grouped properties to coupon update Co-authored-by: ricardo --- .../credentials/PaddleApi.credentials.ts | 23 + .../nodes/ActiveCampaign/DealDescription.ts | 2 +- .../nodes/Paddle/CouponDescription.ts | 878 ++++++++++++++++++ .../nodes/Paddle/GenericFunctions.ts | 76 ++ .../nodes/Paddle/OrderDescription.ts | 52 ++ .../nodes-base/nodes/Paddle/Paddle.node.ts | 517 +++++++++++ .../nodes/Paddle/PaymentDescription.ts | 248 +++++ .../nodes/Paddle/PlanDescription.ts | 98 ++ .../nodes/Paddle/ProductDescription.ts | 71 ++ .../nodes/Paddle/UserDescription.ts | 176 ++++ packages/nodes-base/nodes/Paddle/paddle.png | Bin 0 -> 3076 bytes packages/nodes-base/package.json | 2 + 12 files changed, 2142 insertions(+), 1 deletion(-) create mode 100644 packages/nodes-base/credentials/PaddleApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Paddle/CouponDescription.ts create mode 100644 packages/nodes-base/nodes/Paddle/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Paddle/OrderDescription.ts create mode 100644 packages/nodes-base/nodes/Paddle/Paddle.node.ts create mode 100644 packages/nodes-base/nodes/Paddle/PaymentDescription.ts create mode 100644 packages/nodes-base/nodes/Paddle/PlanDescription.ts create mode 100644 packages/nodes-base/nodes/Paddle/ProductDescription.ts create mode 100644 packages/nodes-base/nodes/Paddle/UserDescription.ts create mode 100644 packages/nodes-base/nodes/Paddle/paddle.png diff --git a/packages/nodes-base/credentials/PaddleApi.credentials.ts b/packages/nodes-base/credentials/PaddleApi.credentials.ts new file mode 100644 index 00000000000..143a24b3b86 --- /dev/null +++ b/packages/nodes-base/credentials/PaddleApi.credentials.ts @@ -0,0 +1,23 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class PaddleApi implements ICredentialType { + name = 'paddleApi'; + displayName = 'Paddle API'; + properties = [ + { + displayName: 'Vendor Auth Code', + name: 'vendorAuthCode', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Vendor ID', + name: 'vendorId', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/ActiveCampaign/DealDescription.ts b/packages/nodes-base/nodes/ActiveCampaign/DealDescription.ts index 940157e5f01..9d61c43395f 100644 --- a/packages/nodes-base/nodes/ActiveCampaign/DealDescription.ts +++ b/packages/nodes-base/nodes/ActiveCampaign/DealDescription.ts @@ -526,4 +526,4 @@ export const dealFields = [ description: 'The content of the deal note', }, -] as INodeProperties[]; \ No newline at end of file +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Paddle/CouponDescription.ts b/packages/nodes-base/nodes/Paddle/CouponDescription.ts new file mode 100644 index 00000000000..69ffc4e6e2b --- /dev/null +++ b/packages/nodes-base/nodes/Paddle/CouponDescription.ts @@ -0,0 +1,878 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const couponOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'coupon', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a coupon.', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all coupons.', + }, + { + name: 'Update', + value: 'update', + description: 'Update a coupon.', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const couponFields = [ +/* -------------------------------------------------------------------------- */ +/* coupon:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Coupon Type', + name: 'couponType', + type: 'options', + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + `create` + ], + jsonParameters: [ + false + ] + }, + }, + default: 'checkout', + description: 'Either product (valid for specified products or subscription plans) or checkout (valid for any checkout).', + options: [ + { + name: 'Checkout', + value: 'checkout' + }, + { + name: 'Product', + value: 'product' + }, + ] + }, + { + displayName: 'Product IDs', + name: 'productIds', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getProducts', + }, + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + `create` + ], + couponType: [ + 'product', + ], + jsonParameters: [ + false + ] + }, + }, + default: '', + description: 'Comma-separated list of product IDs. Required if coupon_type is product.', + required: true, + }, + { + displayName: 'Discount Type', + name: 'discountType', + type: 'options', + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + `create` + ], + jsonParameters: [ + false + ] + }, + }, + default: 'flat', + description: 'Either flat or percentage.', + options: [ + { + name: 'Flat', + value: 'flat' + }, + { + name: 'Percentage', + value: 'percentage' + }, + ] + }, + { + displayName: 'Discount Amount Currency', + name: 'discountAmount', + type: 'number', + default: '', + description: 'Discount amount in currency.', + typeOptions: { + minValue: 1 + }, + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + `create` + ], + discountType: [ + 'flat', + ], + jsonParameters: [ + false + ] + }, + }, + }, + { + displayName: 'Discount Amount %', + name: 'discountAmount', + type: 'number', + default: '', + description: 'Discount amount in percentage.', + typeOptions: { + minValue: 1, + maxValue: 100 + }, + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + `create` + ], + discountType: [ + 'percentage', + ], + jsonParameters: [ + false + ] + }, + }, + }, + { + displayName: 'Currency', + name: 'currency', + type: 'options', + default: 'EUR', + description: 'The currency must match the balance currency specified in your account.', + options: [ + { + name: 'ARS', + value: 'ARS' + }, + { + name: 'AUD', + value: 'AUD' + }, + { + name: 'BRL', + value: 'BRL' + }, + { + name: 'GBP', + value: 'GBP' + }, + { + name: 'CAD', + value: 'CAD' + }, + { + name: 'CNY', + value: 'CNY' + }, + { + name: 'CZK', + value: 'CZK' + }, + { + name: 'DKK', + value: 'DKK' + }, + { + name: 'EUR', + value: 'EUR' + }, + { + name: 'HKD', + value: 'HKD' + }, + { + name: 'HUF', + value: 'HUF' + }, + { + name: 'INR', + value: 'INR' + }, + { + name: 'JPY', + value: 'JPY' + }, + { + name: 'MXN', + value: 'MXN' + }, + { + name: 'TWD', + value: 'TWD' + }, + { + name: 'NZD', + value: 'NZD' + }, + { + name: 'NOK', + value: 'NOK' + }, + { + name: 'PLN', + value: 'PLN' + }, + { + name: 'RUB', + value: 'RUB' + }, + { + name: 'SGD', + value: 'SGD' + }, + { + name: 'ZAR', + value: 'ZAR' + }, + { + name: 'KRW', + value: 'KRW' + }, + { + name: 'SEK', + value: 'SEK' + }, + { + name: 'CHF', + value: 'CHF' + }, + { + name: 'THB', + value: 'THB' + }, + { + name: 'CHF', + value: 'CHF' + }, + { + name: 'USD', + value: 'USD' + }, + ], + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + `create` + ], + discountType: [ + 'flat', + ], + jsonParameters: [ + false + ] + }, + }, + }, + { + displayName: 'JSON Parameters', + name: 'jsonParameters', + type: 'boolean', + default: false, + description: '', + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: ' Additional Fields', + name: 'additionalFieldsJson', + type: 'json', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + 'create', + ], + jsonParameters: [ + true, + ], + }, + }, + description: `Attributes in JSON form.`, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + 'create', + ], + jsonParameters: [ + false + ] + }, + }, + default: {}, + options: [ + { + displayName: 'Allowed Uses', + name: 'allowedUses', + type: 'number', + default: 1, + description: 'Number of times a coupon can be used in a checkout. This will be set to 999,999 by default, if not specified.', + }, + { + displayName: 'Coupon Code', + name: 'couponCode', + type: 'string', + default: '', + description: 'Will be randomly generated if not specified.', + }, + { + displayName: 'Coupon Prefix', + name: 'couponPrefix', + type: 'string', + default: '', + description: 'Prefix for generated codes. Not valid if coupon_code is specified.', + }, + { + displayName: 'Expires', + name: 'expires', + type: 'dateTime', + default: '', + description: 'The coupon will expire on the date at 00:00:00 UTC.', + }, + { + displayName: 'Group', + name: 'group', + type: 'string', + typeOptions: { + minValue: 1, + maxValue: 50 + }, + default: '', + description: 'The name of the coupon group this coupon should be assigned to.', + }, + { + displayName: 'Recurring', + name: 'recurring', + type: 'boolean', + default: false, + description: 'If the coupon is used on subscription products, this indicates whether the discount should apply to recurring payments after the initial purchase.', + }, + { + displayName: 'Number of Coupons', + name: 'numberOfCoupons', + type: 'number', + default: 1, + description: 'Number of coupons to generate. Not valid if coupon_code is specified.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'Description of the coupon. This will be displayed in the Seller Dashboard.', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* coupon:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Product ID', + name: 'productId', + type: 'string', + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + `getAll` + ] + }, + }, + default: '', + required: true, + description: 'The specific product/subscription ID.', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'coupon', + ], + }, + }, + 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: [ + 'coupon', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, +/* -------------------------------------------------------------------------- */ +/* coupon:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Update by', + name: 'updateBy', + type: 'options', + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + `update` + ], + jsonParameters: [ + false, + ], + }, + }, + default: 'couponCode', + description: 'Either flat or percentage.', + options: [ + { + name: 'Coupon Code', + value: 'couponCode' + }, + { + name: 'Group', + value: 'group' + }, + ] + }, + { + displayName: 'Coupon Code', + name: 'couponCode', + type: 'string', + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + 'update' + ], + updateBy: [ + 'couponCode' + ], + jsonParameters: [ + false, + ], + }, + }, + default: '', + description: 'Identify the coupon to update', + }, + { + displayName: 'Group', + name: 'group', + type: 'string', + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + 'update' + ], + updateBy: [ + 'group' + ], + jsonParameters: [ + false, + ], + }, + }, + default: '', + description: 'The name of the group of coupons you want to update.', + }, + { + displayName: 'JSON Parameters', + name: 'jsonParameters', + type: 'boolean', + default: false, + description: '', + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: ' Additional Fields', + name: 'additionalFieldsJson', + type: 'json', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + 'update', + ], + jsonParameters: [ + true, + ], + }, + }, + description: `Attributes in JSON form.`, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + 'update', + ], + jsonParameters: [ + false + ] + }, + }, + default: {}, + options: [ + { + displayName: 'Allowed Uses', + name: 'allowedUses', + type: 'number', + default: 1, + description: 'Number of times a coupon can be used in a checkout. This will be set to 999,999 by default, if not specified.', + }, + { + displayName: 'Discount', + name: 'discount', + type: 'fixedCollection', + default: 'discountProperties', + options: [ + { + displayName: 'Discount Properties', + name: 'discountProperties', + values: [ + { + displayName: 'Currency', + name: 'currency', + type: 'options', + default: 'EUR', + description: 'The currency must match the balance currency specified in your account.', + displayOptions: { + show: { + discountType: [ + 'flat', + ], + }, + }, + options: [ + { + name: 'ARS', + value: 'ARS' + }, + { + name: 'AUD', + value: 'AUD' + }, + { + name: 'BRL', + value: 'BRL' + }, + { + name: 'GBP', + value: 'GBP' + }, + { + name: 'CAD', + value: 'CAD' + }, + { + name: 'CNY', + value: 'CNY' + }, + { + name: 'CZK', + value: 'CZK' + }, + { + name: 'DKK', + value: 'DKK' + }, + { + name: 'EUR', + value: 'EUR' + }, + { + name: 'HKD', + value: 'HKD' + }, + { + name: 'HUF', + value: 'HUF' + }, + { + name: 'INR', + value: 'INR' + }, + { + name: 'JPY', + value: 'JPY' + }, + { + name: 'MXN', + value: 'MXN' + }, + { + name: 'TWD', + value: 'TWD' + }, + { + name: 'NZD', + value: 'NZD' + }, + { + name: 'NOK', + value: 'NOK' + }, + { + name: 'PLN', + value: 'PLN' + }, + { + name: 'RUB', + value: 'RUB' + }, + { + name: 'SGD', + value: 'SGD' + }, + { + name: 'ZAR', + value: 'ZAR' + }, + { + name: 'KRW', + value: 'KRW' + }, + { + name: 'SEK', + value: 'SEK' + }, + { + name: 'CHF', + value: 'CHF' + }, + { + name: 'THB', + value: 'THB' + }, + { + name: 'CHF', + value: 'CHF' + }, + { + name: 'USD', + value: 'USD' + }, + ], + }, + { + displayName: 'Discount Type', + name: 'discountType', + type: 'options', + default: 'flat', + description: 'Either flat or percentage.', + options: [ + { + name: 'Flat', + value: 'flat' + }, + { + name: 'Percentage', + value: 'percentage' + }, + ] + }, + { + displayName: 'Discount Amount Currency', + name: 'discountAmount', + type: 'number', + default: '', + description: 'Discount amount.', + displayOptions: { + show: { + discountType: [ + 'flat', + ], + }, + }, + typeOptions: { + minValue: 0 + }, + }, + { + displayName: 'Discount Amount Percentage', + name: 'discountAmount', + type: 'number', + default: '', + description: 'Discount amount.', + displayOptions: { + show: { + discountType: [ + 'percentage', + ], + }, + }, + typeOptions: { + minValue: 0, + maxValue: 100 + }, + }, + ], + }, + ], + }, + { + displayName: 'Expires', + name: 'expires', + type: 'dateTime', + default: '', + description: 'The coupon will expire on the date at 00:00:00 UTC.', + }, + { + displayName: 'New Coupon Code', + name: 'newCouponCode', + type: 'string', + default: '', + description: 'New code to rename the coupon to.', + }, + { + displayName: 'New Group Name', + name: 'newGroup', + type: 'string', + typeOptions: { + minValue: 1, + maxValue: 50 + }, + default: '', + description: 'New group name to move coupon to.', + }, + { + displayName: 'Product IDs', + name: 'productIds', + type: 'string', + default: '', + description: 'Comma-separated list of products e.g. 499531,1234,123546. If blank then remove associated products.', + }, + { + displayName: 'Recurring', + name: 'recurring', + type: 'boolean', + default: false, + description: 'If the coupon is used on subscription products, this indicates whether the discount should apply to recurring payments after the initial purchase.', + }, + ], + }, + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Paddle/GenericFunctions.ts b/packages/nodes-base/nodes/Paddle/GenericFunctions.ts new file mode 100644 index 00000000000..caecdddeee3 --- /dev/null +++ b/packages/nodes-base/nodes/Paddle/GenericFunctions.ts @@ -0,0 +1,76 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IExecuteSingleFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function paddleApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions, endpoint: string, method: string, body: any = {}, query?: IDataObject, uri?: string): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('paddleApi'); + + if (credentials === undefined) { + throw new Error('Could not retrieve credentials!'); + } + + const options : OptionsWithUri = { + method, + headers: { + 'content-type': 'application/json' + }, + uri: `https://vendors.paddle.com/api${endpoint}` , + body, + json: true + }; + + body['vendor_id'] = credentials.vendorId; + body['vendor_auth_code'] = credentials.vendorAuthCode; + try { + const response = await this.helpers.request!(options); + + if (!response.success) { + throw new Error(`Code: ${response.error.code}. Message: ${response.error.message}`); + } + + return response; + } catch (error) { + throw new Error(`ERROR: Code: ${error.code}. Message: ${error.message}`); + } +} + +export async function paddleApiRequestAllItems(this: IHookFunctions | IExecuteFunctions, propertyName: string, endpoint: string, method: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + body.results_per_page = 200; + body.page = 1; + + do { + responseData = await paddleApiRequest.call(this, endpoint, method, body, query); + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData[propertyName].length !== 0 + ); + + return returnData; +} + +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/Paddle/OrderDescription.ts b/packages/nodes-base/nodes/Paddle/OrderDescription.ts new file mode 100644 index 00000000000..367082a4b30 --- /dev/null +++ b/packages/nodes-base/nodes/Paddle/OrderDescription.ts @@ -0,0 +1,52 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const orderOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'order', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get an order', + } + ], + default: 'get', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const orderFields = [ + +/* -------------------------------------------------------------------------- */ +/* order:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Checkout ID', + name: 'checkoutId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'order', + ], + operation: [ + 'get', + ], + }, + }, + description: 'The identifier of the buyer’s checkout.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Paddle/Paddle.node.ts b/packages/nodes-base/nodes/Paddle/Paddle.node.ts new file mode 100644 index 00000000000..4179762307b --- /dev/null +++ b/packages/nodes-base/nodes/Paddle/Paddle.node.ts @@ -0,0 +1,517 @@ +import { + IExecuteFunctions +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, + ILoadOptionsFunctions, + INodePropertyOptions +} from 'n8n-workflow'; + +import { + couponFields, + couponOperations, +} from './CouponDescription'; + +import { + paddleApiRequest, + paddleApiRequestAllItems, + validateJSON +} from './GenericFunctions'; + +import { + paymentFields, + paymentOperations, +} from './PaymentDescription'; + +import { + planFields, + planOperations, +} from './PlanDescription'; + +import { + productFields, + productOperations, +} from './ProductDescription'; + +import { + userFields, + userOperations, +} from './UserDescription'; + +// import { +// orderOperations, +// orderFields, +// } from './OrderDescription'; + +import * as moment from 'moment'; + +export class Paddle implements INodeType { + description: INodeTypeDescription = { + displayName: 'Paddle', + name: 'paddle', + icon: 'file:paddle.png', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Paddle API', + defaults: { + name: 'Paddle', + color: '#45567c', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'paddleApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Coupon', + value: 'coupon', + }, + { + name: 'Payment', + value: 'payment', + }, + { + name: 'Plan', + value: 'plan', + }, + { + name: 'Product', + value: 'product', + }, + // { + // name: 'Order', + // value: 'order', + // }, + { + name: 'User', + value: 'user', + }, + ], + default: 'coupon', + description: 'Resource to consume.', + }, + // COUPON + ...couponOperations, + ...couponFields, + // PAYMENT + ...paymentOperations, + ...paymentFields, + // PLAN + ...planOperations, + ...planFields, + // PRODUCT + ...productOperations, + ...productFields, + // ORDER + // ...orderOperations, + // ...orderFields, + // USER + ...userOperations, + ...userFields + ], + }; + + methods = { + loadOptions: { + /* -------------------------------------------------------------------------- */ + /* PAYMENT */ + /* -------------------------------------------------------------------------- */ + + // Get all payment so they can be selected in payment rescheduling + async getPayments(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const endpoint = '/2.0/subscription/payments'; + const paymentResponse = await paddleApiRequest.call(this, endpoint, 'POST', {}); + + // Alert user if there's no payments present to be loaded into payments property + if (paymentResponse.response === undefined || paymentResponse.response.length === 0) { + throw Error('No payments on account.'); + } + + for (const payment of paymentResponse.response) { + const id = payment.id; + returnData.push({ + name: id, + value: id, + }); + } + return returnData; + }, + + /* -------------------------------------------------------------------------- */ + /* PRODUCTS */ + /* -------------------------------------------------------------------------- */ + + // Get all Products so they can be selected in coupon creation when assigning products + async getProducts(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const endpoint = '/2.0/product/get_products'; + const products = await paddleApiRequest.call(this, endpoint, 'POST', {}); + + // Alert user if there's no products present to be loaded into payments property + if ( products.length === 0) { + throw Error('No products on account.'); + } + + for (const product of products) { + const name = product.name; + const id = product.id; + returnData.push({ + name, + value: id, + }); + } + return returnData; + }, + } + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + let responseData; + const body: IDataObject = {}; + 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 === 'coupon') { + if (operation === 'create') { + 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 discountType = this.getNodeParameter('discountType', i) as string; + const couponType = this.getNodeParameter('couponType', i) as string; + const discountAmount = this.getNodeParameter('discountAmount', i) as number; + + if (couponType === 'product') { + body.product_ids = (this.getNodeParameter('productIds', i) as string[]).join(); + } + + if (discountType === 'flat') { + body.currency = this.getNodeParameter('currency', i) as string; + } + + body.coupon_type = couponType; + body.discount_type = discountType; + body.discount_amount = discountAmount; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (additionalFields.allowedUses) { + body.allowed_uses = additionalFields.allowedUses as number; + } + if (additionalFields.couponCode) { + body.coupon_code = additionalFields.couponCode as string; + } + if (additionalFields.couponPrefix) { + body.coupon_prefix = additionalFields.couponPrefix as string; + } + if (additionalFields.expires) { + body.expires = moment(additionalFields.expires as Date).format('YYYY-MM-DD') as string; + } + if (additionalFields.group) { + body.group = additionalFields.group as string; + } + if (additionalFields.recurring) { + body.recurring = 1; + } else { + body.recurring = 0; + } + if (additionalFields.numberOfCoupons) { + body.num_coupons = additionalFields.numberOfCoupons as number; + } + if (additionalFields.description) { + body.description = additionalFields.description as string; + } + + const endpoint = '/2.1/product/create_coupon'; + + responseData = await paddleApiRequest.call(this, endpoint, 'POST', body); + responseData = responseData.response.coupon_codes; + } + } + + if (operation === 'getAll') { + const productId = this.getNodeParameter('productId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const endpoint = '/2.0/product/list_coupons'; + + body.product_id = productId as string; + + responseData = await paddleApiRequest.call(this, endpoint, 'POST', body); + + if (returnAll) { + responseData = responseData.response; + } else { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.response.splice(0, limit); + } + } + + if (operation === 'update') { + + 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 updateBy = this.getNodeParameter('updateBy', i) as string; + + if (updateBy === 'group') { + body.group = this.getNodeParameter('group', i) as string; + } else { + body.coupon_code = this.getNodeParameter('couponCode', i) as string; + } + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (additionalFields.allowedUses) { + body.allowed_uses = additionalFields.allowedUses as number; + } + if (additionalFields.currency) { + body.currency = additionalFields.currency as string; + } + if (additionalFields.newCouponCode) { + body.new_coupon_code = additionalFields.newCouponCode as string; + } + if (additionalFields.expires) { + body.expires = moment(additionalFields.expires as Date).format('YYYY-MM-DD') as string; + } + if (additionalFields.newGroup) { + body.new_group = additionalFields.newGroup as string; + } + if (additionalFields.recurring === true) { + body.recurring = 1; + } else if (additionalFields.recurring === false) { + body.recurring = 0; + } + if (additionalFields.productIds) { + body.product_ids = additionalFields.productIds as number; + } + if (additionalFields.discountAmount) { + body.discount_amount = additionalFields.discountAmount as number; + } + if (additionalFields.discount) { + //@ts-ignore + if (additionalFields.discount.discountProperties.discountType === 'percentage') { + // @ts-ignore + body.discount_amount = additionalFields.discount.discountProperties.discountAmount as number; + } else { + //@ts-ignore + body.currency = additionalFields.discount.discountProperties.currency as string; + //@ts-ignore + body.discount_amount = additionalFields.discount.discountProperties.discountAmount as number; + } + } + } + + const endpoint = '/2.1/product/update_coupon'; + + responseData = await paddleApiRequest.call(this, endpoint, 'POST', body); + responseData = responseData.response; + } + } + if (resource === 'payment') { + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + 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.subscriptionId) { + body.subscription_id = additionalFields.subscriptionId as number; + } + if (additionalFields.plan) { + body.plan = additionalFields.plan as string; + } + if (additionalFields.state) { + body.state = additionalFields.state as string; + } + if (additionalFields.isPaid) { + body.is_paid = 1; + } else { + body.is_paid = 0; + } + if (additionalFields.from) { + body.from = moment(additionalFields.from as Date).format('YYYY-MM-DD') as string; + } + if (additionalFields.to) { + body.to = moment(additionalFields.to as Date).format('YYYY-MM-DD') as string; + } + if (additionalFields.isOneOffCharge) { + body.is_one_off_charge = additionalFields.isOneOffCharge as boolean; + } + } + const endpoint = '/2.0/subscription/payments'; + + responseData = await paddleApiRequest.call(this, endpoint, 'POST', body); + + if (returnAll) { + responseData = responseData.response; + } else { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.response.splice(0, limit); + } + } + if (operation === 'reschedule') { + const paymentId = this.getNodeParameter('paymentId', i) as number; + const date = this.getNodeParameter('date', i) as Date; + + body.payment_id = paymentId; + body.date = body.to = moment(date as Date).format('YYYY-MM-DD') as string; + + const endpoint = '/2.0/subscription/payments_reschedule'; + + responseData = await paddleApiRequest.call(this, endpoint, 'POST', body); + } + } + if (resource === 'plan') { + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const endpoint = '/2.0/subscription/plans'; + + responseData = await paddleApiRequest.call(this, endpoint, 'POST', body); + + if (returnAll) { + responseData = responseData.response; + } else { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.response.splice(0, limit); + } + } + if (operation === 'get') { + const planId = this.getNodeParameter('planId', i) as string; + + body.plan = planId; + + const endpoint = '/2.0/subscription/plans'; + + responseData = await paddleApiRequest.call(this, endpoint, 'POST', body); + responseData = responseData.response; + } + } + if (resource === 'product') { + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const endpoint = '/2.0/product/get_products'; + + responseData = await paddleApiRequest.call(this, endpoint, 'POST', body); + + if (returnAll) { + responseData = responseData.response.products; + } else { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.response.products.splice(0, limit); + } + } + } + if (resource === 'order') { + if (operation === 'get') { + const endpoint = '/1.0/order'; + const checkoutId = this.getNodeParameter('checkoutId', i) as string; + + body.checkout_id = checkoutId; + + responseData = await paddleApiRequest.call(this, endpoint, 'GET', body); + } + } + if (resource === 'user') { + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + 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.state) { + body.state = additionalFields.state as string; + } + if (additionalFields.planId) { + body.plan_id = additionalFields.planId as string; + } + if (additionalFields.subscriptionId) { + body.subscription_id = additionalFields.subscriptionId as string; + } + } + + const endpoint = '/2.0/subscription/users'; + + if (returnAll) { + responseData = await paddleApiRequestAllItems.call(this, 'response', endpoint, 'POST', body); + } else { + body.results_per_page = this.getNodeParameter('limit', i) as number; + responseData = await paddleApiRequest.call(this, endpoint, 'POST', body); + responseData = responseData.response; + } + } + } + + 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/Paddle/PaymentDescription.ts b/packages/nodes-base/nodes/Paddle/PaymentDescription.ts new file mode 100644 index 00000000000..2d0ddf7894c --- /dev/null +++ b/packages/nodes-base/nodes/Paddle/PaymentDescription.ts @@ -0,0 +1,248 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const paymentOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'payment', + ], + }, + }, + options: [ + { + name: 'Get All', + value: 'getAll', + description: 'Get all payment.', + }, + { + name: 'Reschedule', + value: 'reschedule', + description: 'Reschedule payment.', + } + ], + default: 'getAll', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const paymentFields = [ +/* -------------------------------------------------------------------------- */ +/* payment:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'payment', + ], + }, + }, + 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: [ + 'payment', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'JSON Parameters', + name: 'jsonParameters', + type: 'boolean', + default: false, + description: '', + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: ' Additional Fields', + name: 'additionalFieldsJson', + type: 'json', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'getAll', + ], + jsonParameters: [ + true, + ], + }, + }, + description: `Attributes in JSON form.`, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'getAll', + ], + jsonParameters: [ + false + ] + }, + }, + default: {}, + options: [ + { + displayName: 'Date From', + name: 'from', + type: 'dateTime', + default: '', + description: 'payment starting from date.', + }, + { + displayName: 'Date To', + name: 'to', + type: 'dateTime', + default: '', + description: 'payment up until date.', + }, + { + displayName: 'Is Paid', + name: 'isPaid', + type: 'boolean', + default: false, + description: 'payment is paid.', + }, + { + displayName: 'Plan ID', + name: 'plan', + type: 'string', + default: '', + description: 'Filter: The product/plan ID (single or comma-separated values).', + }, + { + displayName: 'Subscription ID', + name: 'subscriptionId', + type: 'number', + default: '', + description: 'A specific user subscription ID.', + }, + { + displayName: 'State', + name: 'state', + type: 'options', + default: 'active', + description: 'Filter: The user subscription status. Returns all active, past_due, trialing and paused subscription plans if not specified.', + options: [ + { + name: 'Active', + value: 'active' + }, + { + name: 'Past Due', + value: 'past_due' + }, + { + name: 'Paused', + value: 'paused' + }, + { + name: 'Trialing', + value: 'trialing' + }, + ] + }, + { + displayName: 'One off charge', + name: 'isOneOffCharge', + type: 'boolean', + default: false, + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* payment:reschedule */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Payment ID', + name: 'paymentId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getpayment', + }, + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'reschedule', + ], + }, + }, + description: 'The upcoming subscription payment ID.', + }, + { + displayName: 'Date', + name: 'date', + type: 'dateTime', + default: '', + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'reschedule', + ], + }, + }, + description: 'Date you want to move the payment to.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Paddle/PlanDescription.ts b/packages/nodes-base/nodes/Paddle/PlanDescription.ts new file mode 100644 index 00000000000..912692ca943 --- /dev/null +++ b/packages/nodes-base/nodes/Paddle/PlanDescription.ts @@ -0,0 +1,98 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const planOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'plan', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get a plan.', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all plans.', + } + ], + default: 'get', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const planFields = [ + +/* -------------------------------------------------------------------------- */ +/* plan:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Plan ID', + name: 'planId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'plan', + ], + operation: [ + 'get', + ], + }, + }, + description: 'Filter: The subscription plan ID.', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'plan', + ], + }, + }, + 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: [ + 'plan', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Paddle/ProductDescription.ts b/packages/nodes-base/nodes/Paddle/ProductDescription.ts new file mode 100644 index 00000000000..9a627a86e0c --- /dev/null +++ b/packages/nodes-base/nodes/Paddle/ProductDescription.ts @@ -0,0 +1,71 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const productOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'product', + ], + }, + }, + options: [ + { + name: 'Get All', + value: 'getAll', + description: 'Get all products.', + } + ], + default: 'getAll', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const productFields = [ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'product', + ], + }, + }, + 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: [ + 'product', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Paddle/UserDescription.ts b/packages/nodes-base/nodes/Paddle/UserDescription.ts new file mode 100644 index 00000000000..d45cc2ddb19 --- /dev/null +++ b/packages/nodes-base/nodes/Paddle/UserDescription.ts @@ -0,0 +1,176 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const userOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'user', + ], + }, + }, + options: [ + { + name: 'Get All', + value: 'getAll', + description: 'Get all users', + } + ], + default: 'getAll', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const userFields = [ +/* -------------------------------------------------------------------------- */ +/* user:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 1, + required: true, + typeOptions: { + minValue: 1, + maxValue: 200 + }, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false + ] + }, + }, + description: 'Number of subscription records to return per page.', + }, + { + displayName: 'JSON Parameters', + name: 'jsonParameters', + type: 'boolean', + default: false, + description: '', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: ' Additional Fields', + name: 'additionalFieldsJson', + type: 'json', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + jsonParameters: [ + true, + ], + }, + }, + description: `Attributes in JSON form.`, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + jsonParameters: [ + false + ] + }, + }, + default: {}, + options: [ + { + displayName: 'Plan ID', + name: 'planId', + type: 'string', + default: '', + description: 'Filter: The subscription plan ID.', + }, + { + displayName: 'Subscription ID', + name: 'subscriptionId', + type: 'string', + default: '', + description: 'A specific user subscription ID.', + }, + { + displayName: 'State', + name: 'state', + type: 'options', + default: 'active', + description: 'Filter: The user subscription status. Returns all active, past_due, trialing and paused subscription plans if not specified.', + options: [ + { + name: 'Active', + value: 'active' + }, + { + name: 'Past Due', + value: 'past_due' + }, + { + name: 'Paused', + value: 'paused' + }, + { + name: 'Trialing', + value: 'trialing' + }, + ] + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Paddle/paddle.png b/packages/nodes-base/nodes/Paddle/paddle.png new file mode 100644 index 0000000000000000000000000000000000000000..80426ca92c3439e320c00be5eb12476c4957f7ab GIT binary patch literal 3076 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 XufyuO^DY|czS9HQA$}(ot={+t`pL%m literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 1ae8d4cee24..691cca25e20 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -120,6 +120,7 @@ "dist/credentials/OAuth1Api.credentials.js", "dist/credentials/OAuth2Api.credentials.js", "dist/credentials/OpenWeatherMapApi.credentials.js", + "dist/credentials/PaddleApi.credentials.js", "dist/credentials/PagerDutyApi.credentials.js", "dist/credentials/PagerDutyOAuth2Api.credentials.js", "dist/credentials/PayPalApi.credentials.js", @@ -287,6 +288,7 @@ "dist/nodes/NextCloud/NextCloud.node.js", "dist/nodes/NoOp.node.js", "dist/nodes/OpenWeatherMap.node.js", + "dist/nodes/Paddle/Paddle.node.js", "dist/nodes/PagerDuty/PagerDuty.node.js", "dist/nodes/PayPal/PayPal.node.js", "dist/nodes/PayPal/PayPalTrigger.node.js", From 5def03855b7e142b1129d86829432365661a852a Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Tue, 25 Aug 2020 10:51:16 +0200 Subject: [PATCH 17/39] :zap: Minor improvements to Paddle-Node --- .../nodes/Paddle/CouponDescription.ts | 174 +++++++++--------- .../nodes/Paddle/GenericFunctions.ts | 4 +- .../nodes/Paddle/OrderDescription.ts | 6 +- .../nodes-base/nodes/Paddle/Paddle.node.ts | 6 +- .../nodes/Paddle/PaymentDescription.ts | 14 +- .../nodes/Paddle/PlanDescription.ts | 6 +- .../nodes/Paddle/UserDescription.ts | 10 +- 7 files changed, 106 insertions(+), 114 deletions(-) diff --git a/packages/nodes-base/nodes/Paddle/CouponDescription.ts b/packages/nodes-base/nodes/Paddle/CouponDescription.ts index 69ffc4e6e2b..179ec825b39 100644 --- a/packages/nodes-base/nodes/Paddle/CouponDescription.ts +++ b/packages/nodes-base/nodes/Paddle/CouponDescription.ts @@ -37,9 +37,9 @@ export const couponOperations = [ ] as INodeProperties[]; export const couponFields = [ -/* -------------------------------------------------------------------------- */ -/* coupon:create */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* coupon:create */ + /* -------------------------------------------------------------------------- */ { displayName: 'Coupon Type', name: 'couponType', @@ -199,14 +199,14 @@ export const couponFields = [ name: 'BRL', value: 'BRL' }, - { - name: 'GBP', - value: 'GBP' - }, { name: 'CAD', value: 'CAD' }, + { + name: 'CHF', + value: 'CHF' + }, { name: 'CNY', value: 'CNY' @@ -223,6 +223,10 @@ export const couponFields = [ name: 'EUR', value: 'EUR' }, + { + name: 'GBP', + value: 'GBP' + }, { name: 'HKD', value: 'HKD' @@ -239,22 +243,22 @@ export const couponFields = [ name: 'JPY', value: 'JPY' }, + { + name: 'KRW', + value: 'KRW' + }, { name: 'MXN', value: 'MXN' }, { - name: 'TWD', - value: 'TWD' + name: 'NOK', + value: 'NOK' }, { name: 'NZD', value: 'NZD' }, - { - name: 'NOK', - value: 'NOK' - }, { name: 'PLN', value: 'PLN' @@ -263,38 +267,30 @@ export const couponFields = [ name: 'RUB', value: 'RUB' }, - { - name: 'SGD', - value: 'SGD' - }, - { - name: 'ZAR', - value: 'ZAR' - }, - { - name: 'KRW', - value: 'KRW' - }, { name: 'SEK', value: 'SEK' }, { - name: 'CHF', - value: 'CHF' + name: 'SGD', + value: 'SGD' }, { name: 'THB', value: 'THB' }, { - name: 'CHF', - value: 'CHF' + name: 'TWD', + value: 'TWD' }, { name: 'USD', value: 'USD' }, + { + name: 'ZAR', + value: 'ZAR' + }, ], displayOptions: { show: { @@ -394,6 +390,13 @@ export const couponFields = [ default: '', description: 'Prefix for generated codes. Not valid if coupon_code is specified.', }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'Description of the coupon. This will be displayed in the Seller Dashboard.', + }, { displayName: 'Expires', name: 'expires', @@ -412,13 +415,6 @@ export const couponFields = [ default: '', description: 'The name of the coupon group this coupon should be assigned to.', }, - { - displayName: 'Recurring', - name: 'recurring', - type: 'boolean', - default: false, - description: 'If the coupon is used on subscription products, this indicates whether the discount should apply to recurring payments after the initial purchase.', - }, { displayName: 'Number of Coupons', name: 'numberOfCoupons', @@ -427,17 +423,17 @@ export const couponFields = [ description: 'Number of coupons to generate. Not valid if coupon_code is specified.', }, { - displayName: 'Description', - name: 'description', - type: 'string', - default: '', - description: 'Description of the coupon. This will be displayed in the Seller Dashboard.', + displayName: 'Recurring', + name: 'recurring', + type: 'boolean', + default: false, + description: 'If the coupon is used on subscription products, this indicates whether the discount should apply to recurring payments after the initial purchase.', }, ], }, -/* -------------------------------------------------------------------------- */ -/* coupon:getAll */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* coupon:getAll */ + /* -------------------------------------------------------------------------- */ { displayName: 'Product ID', name: 'productId', @@ -497,9 +493,9 @@ export const couponFields = [ default: 100, description: 'How many results to return.', }, -/* -------------------------------------------------------------------------- */ -/* coupon:update */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* coupon:update */ + /* -------------------------------------------------------------------------- */ { displayName: 'Update by', name: 'updateBy', @@ -594,7 +590,7 @@ export const couponFields = [ }, }, { - displayName: ' Additional Fields', + displayName: 'Additional Fields', name: 'additionalFieldsJson', type: 'json', typeOptions: { @@ -679,14 +675,14 @@ export const couponFields = [ name: 'BRL', value: 'BRL' }, - { - name: 'GBP', - value: 'GBP' - }, { name: 'CAD', value: 'CAD' }, + { + name: 'CHF', + value: 'CHF' + }, { name: 'CNY', value: 'CNY' @@ -703,6 +699,10 @@ export const couponFields = [ name: 'EUR', value: 'EUR' }, + { + name: 'GBP', + value: 'GBP' + }, { name: 'HKD', value: 'HKD' @@ -719,22 +719,22 @@ export const couponFields = [ name: 'JPY', value: 'JPY' }, + { + name: 'KRW', + value: 'KRW' + }, { name: 'MXN', value: 'MXN' }, { - name: 'TWD', - value: 'TWD' + name: 'NOK', + value: 'NOK' }, { name: 'NZD', value: 'NZD' }, - { - name: 'NOK', - value: 'NOK' - }, { name: 'PLN', value: 'PLN' @@ -743,57 +743,32 @@ export const couponFields = [ name: 'RUB', value: 'RUB' }, - { - name: 'SGD', - value: 'SGD' - }, - { - name: 'ZAR', - value: 'ZAR' - }, - { - name: 'KRW', - value: 'KRW' - }, { name: 'SEK', value: 'SEK' }, { - name: 'CHF', - value: 'CHF' + name: 'SGD', + value: 'SGD' }, { name: 'THB', value: 'THB' }, { - name: 'CHF', - value: 'CHF' + name: 'TWD', + value: 'TWD' }, { name: 'USD', value: 'USD' }, + { + name: 'ZAR', + value: 'ZAR' + }, ], }, - { - displayName: 'Discount Type', - name: 'discountType', - type: 'options', - default: 'flat', - description: 'Either flat or percentage.', - options: [ - { - name: 'Flat', - value: 'flat' - }, - { - name: 'Percentage', - value: 'percentage' - }, - ] - }, { displayName: 'Discount Amount Currency', name: 'discountAmount', @@ -829,6 +804,23 @@ export const couponFields = [ maxValue: 100 }, }, + { + displayName: 'Discount Type', + name: 'discountType', + type: 'options', + default: 'flat', + description: 'Either flat or percentage.', + options: [ + { + name: 'Flat', + value: 'flat' + }, + { + name: 'Percentage', + value: 'percentage' + }, + ] + }, ], }, ], diff --git a/packages/nodes-base/nodes/Paddle/GenericFunctions.ts b/packages/nodes-base/nodes/Paddle/GenericFunctions.ts index caecdddeee3..243dc56ff6d 100644 --- a/packages/nodes-base/nodes/Paddle/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Paddle/GenericFunctions.ts @@ -21,12 +21,12 @@ export async function paddleApiRequest(this: IHookFunctions | IExecuteFunctions throw new Error('Could not retrieve credentials!'); } - const options : OptionsWithUri = { + const options: OptionsWithUri = { method, headers: { 'content-type': 'application/json' }, - uri: `https://vendors.paddle.com/api${endpoint}` , + uri: `https://vendors.paddle.com/api${endpoint}`, body, json: true }; diff --git a/packages/nodes-base/nodes/Paddle/OrderDescription.ts b/packages/nodes-base/nodes/Paddle/OrderDescription.ts index 367082a4b30..d4afd8f7cf8 100644 --- a/packages/nodes-base/nodes/Paddle/OrderDescription.ts +++ b/packages/nodes-base/nodes/Paddle/OrderDescription.ts @@ -28,9 +28,9 @@ export const orderOperations = [ export const orderFields = [ -/* -------------------------------------------------------------------------- */ -/* order:get */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* order:get */ + /* -------------------------------------------------------------------------- */ { displayName: 'Checkout ID', name: 'checkoutId', diff --git a/packages/nodes-base/nodes/Paddle/Paddle.node.ts b/packages/nodes-base/nodes/Paddle/Paddle.node.ts index 4179762307b..94e1246f490 100644 --- a/packages/nodes-base/nodes/Paddle/Paddle.node.ts +++ b/packages/nodes-base/nodes/Paddle/Paddle.node.ts @@ -4,11 +4,11 @@ import { import { IDataObject, + ILoadOptionsFunctions, INodeExecutionData, + INodePropertyOptions, INodeType, INodeTypeDescription, - ILoadOptionsFunctions, - INodePropertyOptions } from 'n8n-workflow'; import { @@ -163,7 +163,7 @@ export class Paddle implements INodeType { const products = await paddleApiRequest.call(this, endpoint, 'POST', {}); // Alert user if there's no products present to be loaded into payments property - if ( products.length === 0) { + if (products.length === 0) { throw Error('No products on account.'); } diff --git a/packages/nodes-base/nodes/Paddle/PaymentDescription.ts b/packages/nodes-base/nodes/Paddle/PaymentDescription.ts index 2d0ddf7894c..b2020d4f56d 100644 --- a/packages/nodes-base/nodes/Paddle/PaymentDescription.ts +++ b/packages/nodes-base/nodes/Paddle/PaymentDescription.ts @@ -32,9 +32,9 @@ export const paymentOperations = [ ] as INodeProperties[]; export const paymentFields = [ -/* -------------------------------------------------------------------------- */ -/* payment:getAll */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* payment:getAll */ + /* -------------------------------------------------------------------------- */ { displayName: 'Return All', name: 'returnAll', @@ -94,7 +94,7 @@ export const paymentFields = [ }, }, { - displayName: ' Additional Fields', + displayName: 'Additional Fields', name: 'additionalFieldsJson', type: 'json', typeOptions: { @@ -204,9 +204,9 @@ export const paymentFields = [ }, ], }, -/* -------------------------------------------------------------------------- */ -/* payment:reschedule */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* payment:reschedule */ + /* -------------------------------------------------------------------------- */ { displayName: 'Payment ID', name: 'paymentId', diff --git a/packages/nodes-base/nodes/Paddle/PlanDescription.ts b/packages/nodes-base/nodes/Paddle/PlanDescription.ts index 912692ca943..152db9a7ff2 100644 --- a/packages/nodes-base/nodes/Paddle/PlanDescription.ts +++ b/packages/nodes-base/nodes/Paddle/PlanDescription.ts @@ -33,9 +33,9 @@ export const planOperations = [ export const planFields = [ -/* -------------------------------------------------------------------------- */ -/* plan:get */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* plan:get */ + /* -------------------------------------------------------------------------- */ { displayName: 'Plan ID', name: 'planId', diff --git a/packages/nodes-base/nodes/Paddle/UserDescription.ts b/packages/nodes-base/nodes/Paddle/UserDescription.ts index d45cc2ddb19..50b1a92959c 100644 --- a/packages/nodes-base/nodes/Paddle/UserDescription.ts +++ b/packages/nodes-base/nodes/Paddle/UserDescription.ts @@ -27,9 +27,9 @@ export const userOperations = [ ] as INodeProperties[]; export const userFields = [ -/* -------------------------------------------------------------------------- */ -/* user:getAll */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* user:getAll */ + /* -------------------------------------------------------------------------- */ { displayName: 'Return All', name: 'returnAll', @@ -51,7 +51,7 @@ export const userFields = [ displayName: 'Limit', name: 'limit', type: 'number', - default: 1, + default: 100, required: true, typeOptions: { minValue: 1, @@ -90,7 +90,7 @@ export const userFields = [ }, }, { - displayName: ' Additional Fields', + displayName: 'Additional Fields', name: 'additionalFieldsJson', type: 'json', typeOptions: { From 2c9e589c726358adfc0ac88d59719f74629fe479 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Tue, 25 Aug 2020 11:22:14 +0200 Subject: [PATCH 18/39] :zap: Minor improvements to Contentful-Node --- .../credentials/ContentfulApi.credentials.ts | 8 +++----- .../nodes-base/nodes/Contentful/Contentful.node.ts | 4 ++-- .../nodes-base/nodes/Contentful/EntryDescription.ts | 5 +---- .../nodes-base/nodes/Contentful/LocaleDescription.ts | 4 ++-- .../nodes/Contentful/SearchParameterDescription.ts | 10 +++++----- .../nodes-base/nodes/Contentful/SpaceDescription.ts | 2 +- 6 files changed, 14 insertions(+), 19 deletions(-) diff --git a/packages/nodes-base/credentials/ContentfulApi.credentials.ts b/packages/nodes-base/credentials/ContentfulApi.credentials.ts index d08b97e647d..8ce49fb147c 100644 --- a/packages/nodes-base/credentials/ContentfulApi.credentials.ts +++ b/packages/nodes-base/credentials/ContentfulApi.credentials.ts @@ -14,23 +14,21 @@ export class ContentfulApi implements ICredentialType { type: 'string' as NodePropertyTypes, default: '', required: true, - description: 'The id for the Contentful space.' + description: 'The id for the Contentful space.', }, { displayName: 'Content Delivery API Access token', name: 'ContentDeliveryaccessToken', type: 'string' as NodePropertyTypes, default: '', - required: true, - description: 'Access token that has access to the space' + description: 'Access token that has access to the space. Can be left empty if only Delivery API should be used.', }, { displayName: 'Content Preview API Access token', name: 'ContentPreviewaccessToken', type: 'string' as NodePropertyTypes, default: '', - required: true, - description: 'Access token that has access to the space' + description: 'Access token that has access to the space. Can be left empty if only Preview API should be used.', }, ]; } diff --git a/packages/nodes-base/nodes/Contentful/Contentful.node.ts b/packages/nodes-base/nodes/Contentful/Contentful.node.ts index 820e8b07a90..7edd8804901 100644 --- a/packages/nodes-base/nodes/Contentful/Contentful.node.ts +++ b/packages/nodes-base/nodes/Contentful/Contentful.node.ts @@ -46,7 +46,7 @@ export class Contentful implements INodeType { displayName: 'Source', name: 'source', type: 'options', - default: 'Delivery API', + default: 'deliveryApi', description: 'Pick where your data comes from, delivery or preview API', options: [ { @@ -71,7 +71,7 @@ export class Contentful implements INodeType { LocaleDescription.resource, SpaceDescription.resource, ], - default: '', + default: 'entry', description: 'The resource to operate on.' }, diff --git a/packages/nodes-base/nodes/Contentful/EntryDescription.ts b/packages/nodes-base/nodes/Contentful/EntryDescription.ts index c22e3ba0d66..24fbfbed267 100644 --- a/packages/nodes-base/nodes/Contentful/EntryDescription.ts +++ b/packages/nodes-base/nodes/Contentful/EntryDescription.ts @@ -2,9 +2,6 @@ import { INodeProperties, INodePropertyOptions, } from 'n8n-workflow'; -import { type } from 'os'; -import { notEqual } from 'assert'; -import { exists } from 'fs'; export const resource = { name: 'Entry', @@ -55,7 +52,7 @@ export const fields = [ }, }, default: 'master', - description: 'The id for the Contentful environment (e.g. master, staging, etc.). Depending on your plan, you might not have environments. In that case use "master".' + description: 'The id for the Contentful environment (e.g. master, staging, etc.). Depending on your plan, you might not have environments. In that case use "master".', }, { displayName: 'Return All', diff --git a/packages/nodes-base/nodes/Contentful/LocaleDescription.ts b/packages/nodes-base/nodes/Contentful/LocaleDescription.ts index 45f2872ad22..5f06237af8d 100644 --- a/packages/nodes-base/nodes/Contentful/LocaleDescription.ts +++ b/packages/nodes-base/nodes/Contentful/LocaleDescription.ts @@ -4,8 +4,8 @@ import { } from 'n8n-workflow'; export const resource = { - name: 'Locale', - value: 'locale', + name: 'Locale', + value: 'locale', } as INodePropertyOptions; export const operations = [ diff --git a/packages/nodes-base/nodes/Contentful/SearchParameterDescription.ts b/packages/nodes-base/nodes/Contentful/SearchParameterDescription.ts index 7557cbb7552..9e89c340e36 100644 --- a/packages/nodes-base/nodes/Contentful/SearchParameterDescription.ts +++ b/packages/nodes-base/nodes/Contentful/SearchParameterDescription.ts @@ -8,7 +8,7 @@ export const fields = [ placeholder: 'Add parameter', type: 'fixedCollection', typeOptions: { - multipleValues: true + multipleValues: true, }, default: {}, options: [ @@ -29,9 +29,9 @@ export const fields = [ type: 'string', default: '', description: 'Value of the search parameter to set.' - } - ] - } - ] + }, + ], + }, + ], } ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Contentful/SpaceDescription.ts b/packages/nodes-base/nodes/Contentful/SpaceDescription.ts index 035f3c6e573..41c77ac6dee 100644 --- a/packages/nodes-base/nodes/Contentful/SpaceDescription.ts +++ b/packages/nodes-base/nodes/Contentful/SpaceDescription.ts @@ -15,7 +15,7 @@ export const operations = [ displayOptions: { show: { resource: [ - resource.value + resource.value, ], }, }, From 1975252805b5dbe7bab3ce2fc2ce64ff1fe2f38a Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Tue, 25 Aug 2020 17:02:01 +0200 Subject: [PATCH 19/39] :zap: Add allowUnauthorizedCerts to Postgres-Node --- .../nodes-base/credentials/Postgres.credentials.ts | 14 ++++++++++++++ .../nodes-base/nodes/Postgres/Postgres.node.ts | 13 ++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/nodes-base/credentials/Postgres.credentials.ts b/packages/nodes-base/credentials/Postgres.credentials.ts index 03f0dd80b80..d3a8e90e519 100644 --- a/packages/nodes-base/credentials/Postgres.credentials.ts +++ b/packages/nodes-base/credentials/Postgres.credentials.ts @@ -36,10 +36,24 @@ export class Postgres implements ICredentialType { }, default: '', }, + { + displayName: 'Ignore SSL Issues', + name: 'allowUnauthorizedCerts', + type: 'boolean' as NodePropertyTypes, + default: false, + description: 'Connect even if SSL certificate validation is not possible.', + }, { displayName: 'SSL', name: 'ssl', type: 'options' as NodePropertyTypes, + displayOptions: { + show: { + allowUnauthorizedCerts: [ + false, + ], + }, + }, options: [ { name: 'disable', diff --git a/packages/nodes-base/nodes/Postgres/Postgres.node.ts b/packages/nodes-base/nodes/Postgres/Postgres.node.ts index f92234eb06b..7c706a0fc2e 100644 --- a/packages/nodes-base/nodes/Postgres/Postgres.node.ts +++ b/packages/nodes-base/nodes/Postgres/Postgres.node.ts @@ -189,16 +189,23 @@ export class Postgres implements INodeType { const pgp = pgPromise(); - const config = { + const config: IDataObject = { host: credentials.host as string, port: credentials.port as number, database: credentials.database as string, user: credentials.user as string, password: credentials.password as string, - ssl: !['disable', undefined].includes(credentials.ssl as string | undefined), - sslmode: (credentials.ssl as string) || 'disable', }; + if (credentials.allowUnauthorizedCerts === true) { + config.ssl = { + rejectUnauthorized: false, + }; + } else { + config.ssl = !['disable', undefined].includes(credentials.ssl as string | undefined); + config.sslmode = (credentials.ssl as string) || 'disable'; + } + const db = pgp(config); let returnItems = []; From 730463aa10556170d973fd55c920c82e76a9cba5 Mon Sep 17 00:00:00 2001 From: Rupenieks <32895755+Rupenieks@users.noreply.github.com> Date: Tue, 25 Aug 2020 17:57:17 +0200 Subject: [PATCH 20/39] :zap: Added simple, resolve and RAW email formats. Inline attachments now retrieved if format is resolved. (#884) --- .../nodes-base/nodes/EmailReadImap.node.ts | 210 ++++++++++++++---- 1 file changed, 169 insertions(+), 41 deletions(-) diff --git a/packages/nodes-base/nodes/EmailReadImap.node.ts b/packages/nodes-base/nodes/EmailReadImap.node.ts index aa976970c48..0703d23a1d5 100644 --- a/packages/nodes-base/nodes/EmailReadImap.node.ts +++ b/packages/nodes-base/nodes/EmailReadImap.node.ts @@ -6,9 +6,16 @@ import { INodeType, INodeTypeDescription, ITriggerResponse, + IBinaryKeyData, } from 'n8n-workflow'; import { connect as imapConnect, ImapSimple, ImapSimpleOptions, getParts, Message } from 'imap-simple'; +import { + simpleParser, + Source as ParserSource, +} from 'mailparser'; + +import * as lodash from 'lodash'; export class EmailReadImap implements INodeType { description: INodeTypeDescription = { @@ -59,8 +66,39 @@ export class EmailReadImap implements INodeType { name: 'downloadAttachments', type: 'boolean', default: false, + displayOptions: { + show: { + format: [ + 'simple' + ], + }, + }, description: 'If attachments of emails should be downloaded.
Only set if needed as it increases processing.', }, + { + displayName: 'Format', + name: 'format', + type: 'options', + options: [ + { + name: 'RAW', + value: 'raw', + description: 'Returns the full email message data with body content in the raw field as a base64url encoded string; the payload field is not used.' + }, + { + name: 'Resolved', + value: 'resolved', + description: 'Returns the full email with all data resolved and attachments saved as binary data.', + }, + { + name: 'Simple', + value: 'simple', + description: 'Returns the full email; do not use if you wish to gather inline attachments.', + }, + ], + default: 'simple', + description: 'The format to return the message in', + }, { displayName: 'Property Prefix Name', name: 'dataPropertyAttachmentsPrefixName', @@ -68,6 +106,23 @@ export class EmailReadImap implements INodeType { default: 'attachment_', displayOptions: { show: { + format: [ + 'resolved' + ], + }, + }, + description: 'Prefix for name of the binary property to which to
write the attachments. An index starting with 0 will be added.
So if name is "attachment_" the first attachment is saved to "attachment_0"', + }, + { + displayName: 'Property Prefix Name', + name: 'dataPropertyAttachmentsPrefixName', + type: 'string', + default: 'attachment_', + displayOptions: { + show: { + format: [ + 'simple' + ], downloadAttachments: [ true ], @@ -105,7 +160,6 @@ export class EmailReadImap implements INodeType { const mailbox = this.getNodeParameter('mailbox') as string; const postProcessAction = this.getNodeParameter('postProcessAction') as string; - const downloadAttachments = this.getNodeParameter('downloadAttachments') as boolean; const options = this.getNodeParameter('options', {}) as IDataObject; @@ -156,16 +210,26 @@ export class EmailReadImap implements INodeType { // Returns all the new unseen messages const getNewEmails = async (connection: ImapSimple): Promise => { - + const format = this.getNodeParameter('format', 0) as string; const searchCriteria = [ 'UNSEEN' ]; - const fetchOptions = { - bodies: ['HEADER', 'TEXT'], - markSeen: postProcessAction === 'read', - struct: true, - }; + let fetchOptions = {}; + + if (format === 'simple' || format === 'raw') { + fetchOptions = { + bodies: ['TEXT', 'HEADER'], + markSeen: postProcessAction === 'read', + struct: true, + }; + } else if (format === 'resolved') { + fetchOptions = { + bodies: [''], + markSeen: postProcessAction === 'read', + struct: true, + }; + } const results = await connection.search(searchCriteria, fetchOptions); @@ -174,10 +238,7 @@ export class EmailReadImap implements INodeType { let attachments: IBinaryData[]; let propertyName: string; - let dataPropertyAttachmentsPrefixName = ''; - if (downloadAttachments === true) { - dataPropertyAttachmentsPrefixName = this.getNodeParameter('dataPropertyAttachmentsPrefixName') as string; - } + // All properties get by default moved to metadata except the ones // which are defined here which get set on the top level. @@ -188,45 +249,83 @@ export class EmailReadImap implements INodeType { 'subject', 'to', ]; - for (const message of results) { - const parts = getParts(message.attributes.struct!); + if (format === 'resolved') { + const dataPropertyAttachmentsPrefixName = this.getNodeParameter('dataPropertyAttachmentsPrefixName') as string; - newEmail = { - json: { - textHtml: await getText(parts, message, 'html'), - textPlain: await getText(parts, message, 'plain'), - metadata: {} as IDataObject, + for (const message of results) { + const part = lodash.find(message.parts, {which: ''}); + + if (part === undefined) { + throw new Error('Email part could not be parsed.'); } - }; + const parsedEmail = await parseRawEmail.call(this, part.body, dataPropertyAttachmentsPrefixName); - messageHeader = message.parts.filter((part) => { - return part.which === 'HEADER'; - }); - - messageBody = messageHeader[0].body; - for (propertyName of Object.keys(messageBody)) { - if (messageBody[propertyName].length) { - if (topLevelProperties.includes(propertyName)) { - newEmail.json[propertyName] = messageBody[propertyName][0]; - } else { - (newEmail.json.metadata as IDataObject)[propertyName] = messageBody[propertyName][0]; - } - } + newEmails.push(parsedEmail); } + } else if (format === 'simple') { + const downloadAttachments = this.getNodeParameter('downloadAttachments') as boolean; + let dataPropertyAttachmentsPrefixName = ''; if (downloadAttachments === true) { - // Get attachments and add them if any get found - attachments = await getAttachment(connection, parts, message); - if (attachments.length) { - newEmail.binary = {}; - for (let i = 0; i < attachments.length; i++) { - newEmail.binary[`${dataPropertyAttachmentsPrefixName}${i}`] = attachments[i]; - } - } + dataPropertyAttachmentsPrefixName = this.getNodeParameter('dataPropertyAttachmentsPrefixName') as string; } - newEmails.push(newEmail); + for (const message of results) { + const parts = getParts(message.attributes.struct!); + + newEmail = { + json: { + textHtml: await getText(parts, message, 'html'), + textPlain: await getText(parts, message, 'plain'), + metadata: {} as IDataObject, + } + }; + + messageHeader = message.parts.filter((part) => { + return part.which === 'HEADER'; + }); + + messageBody = messageHeader[0].body; + for (propertyName of Object.keys(messageBody)) { + if (messageBody[propertyName].length) { + if (topLevelProperties.includes(propertyName)) { + newEmail.json[propertyName] = messageBody[propertyName][0]; + } else { + (newEmail.json.metadata as IDataObject)[propertyName] = messageBody[propertyName][0]; + } + } + } + + if (downloadAttachments === true) { + // Get attachments and add them if any get found + attachments = await getAttachment(connection, parts, message); + if (attachments.length) { + newEmail.binary = {}; + for (let i = 0; i < attachments.length; i++) { + newEmail.binary[`${dataPropertyAttachmentsPrefixName}${i}`] = attachments[i]; + } + } + } + + newEmails.push(newEmail); + } + } else if (format === 'raw') { + for (const message of results) { + const part = lodash.find(message.parts, {which: 'TEXT'}); + + if (part === undefined) { + throw new Error('Email part could not be parsed.'); + } + // Return base64 string + newEmail = { + json: { + raw: part.body + } + }; + + newEmails.push(newEmail); + } } return newEmails; @@ -277,3 +376,32 @@ export class EmailReadImap implements INodeType { } } + +export async function parseRawEmail(this: ITriggerFunctions, messageEncoded: ParserSource, dataPropertyNameDownload: string): Promise { + const responseData = await simpleParser(messageEncoded); + const headers: IDataObject = {}; + for (const header of responseData.headerLines) { + headers[header.key] = header.line; + } + + // @ts-ignore + responseData.headers = headers; + // @ts-ignore + responseData.headerLines = undefined; + + const binaryData: IBinaryKeyData = {}; + if (responseData.attachments) { + + for (let i = 0; i < responseData.attachments.length; i++) { + const attachment = responseData.attachments[i]; + binaryData[`${dataPropertyNameDownload}${i}`] = await this.helpers.prepareBinaryData(attachment.content, attachment.filename, attachment.contentType); + } + // @ts-ignore + responseData.attachments = undefined; + } + + return { + json: responseData as unknown as IDataObject, + binary: Object.keys(binaryData).length ? binaryData : undefined, + } as INodeExecutionData; +} From 1bf57b3c4f37474d00f67adaf774b24b84bf7d94 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Tue, 25 Aug 2020 17:57:45 +0200 Subject: [PATCH 21/39] :zap: Minor improvements to EmailReadImap-Node --- .../nodes-base/nodes/EmailReadImap.node.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/nodes-base/nodes/EmailReadImap.node.ts b/packages/nodes-base/nodes/EmailReadImap.node.ts index 0703d23a1d5..920c0d41fc7 100644 --- a/packages/nodes-base/nodes/EmailReadImap.node.ts +++ b/packages/nodes-base/nodes/EmailReadImap.node.ts @@ -1,15 +1,15 @@ import { ITriggerFunctions } from 'n8n-core'; import { IBinaryData, + IBinaryKeyData, IDataObject, INodeExecutionData, INodeType, INodeTypeDescription, ITriggerResponse, - IBinaryKeyData, } from 'n8n-workflow'; -import { connect as imapConnect, ImapSimple, ImapSimpleOptions, getParts, Message } from 'imap-simple'; +import { connect as imapConnect, ImapSimple, ImapSimpleOptions, getParts, Message } from 'imap-simple'; import { simpleParser, Source as ParserSource, @@ -51,11 +51,11 @@ export class EmailReadImap implements INodeType { options: [ { name: 'Mark as read', - value: 'read' + value: 'read', }, { name: 'Nothing', - value: 'nothing' + value: 'nothing', }, ], default: 'read', @@ -69,7 +69,7 @@ export class EmailReadImap implements INodeType { displayOptions: { show: { format: [ - 'simple' + 'simple', ], }, }, @@ -107,7 +107,7 @@ export class EmailReadImap implements INodeType { displayOptions: { show: { format: [ - 'resolved' + 'resolved', ], }, }, @@ -121,10 +121,10 @@ export class EmailReadImap implements INodeType { displayOptions: { show: { format: [ - 'simple' + 'simple', ], downloadAttachments: [ - true + true, ], }, }, @@ -254,7 +254,7 @@ export class EmailReadImap implements INodeType { const dataPropertyAttachmentsPrefixName = this.getNodeParameter('dataPropertyAttachmentsPrefixName') as string; for (const message of results) { - const part = lodash.find(message.parts, {which: ''}); + const part = lodash.find(message.parts, { which: '' }); if (part === undefined) { throw new Error('Email part could not be parsed.'); @@ -312,7 +312,7 @@ export class EmailReadImap implements INodeType { } } else if (format === 'raw') { for (const message of results) { - const part = lodash.find(message.parts, {which: 'TEXT'}); + const part = lodash.find(message.parts, { which: 'TEXT' }); if (part === undefined) { throw new Error('Email part could not be parsed.'); From abdda858eba6b317eb173c7fcbc194550fb94fbf Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Tue, 25 Aug 2020 12:56:19 -0400 Subject: [PATCH 22/39] :bug: Fix issue with OAuth2 request (#874) --- packages/nodes-base/nodes/HttpRequest.node.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/HttpRequest.node.ts b/packages/nodes-base/nodes/HttpRequest.node.ts index 41dedd25a1c..ca84937cb89 100644 --- a/packages/nodes-base/nodes/HttpRequest.node.ts +++ b/packages/nodes-base/nodes/HttpRequest.node.ts @@ -821,7 +821,7 @@ export class HttpRequest implements INodeType { } else if (oAuth2Api !== undefined) { //@ts-ignore - response = await this.helpers.requestOAuth2.call(this, 'oAuth2Api', requestOptions, 'Bearer'); + response = await this.helpers.requestOAuth2.call(this, 'oAuth2Api', requestOptions, { tokenType: 'Bearer' }); } else { response = await this.helpers.request(requestOptions); } From 44f7b7a9c297daeaed45de0f8a04b5709bd66743 Mon Sep 17 00:00:00 2001 From: Rupenieks <32895755+Rupenieks@users.noreply.github.com> Date: Tue, 25 Aug 2020 20:37:24 +0200 Subject: [PATCH 23/39] :sparkles: Dynamic title based on workflow execution (#865) * :white_check_mark: Added title changes based on workflow execution * :zap: Title changes on workflow open, reset on workflow delete, fixed not showing when page refreshed * :zap: Title icons --- .../editor-ui/src/components/MainHeader.vue | 4 ++- .../editor-ui/src/components/MainSidebar.vue | 5 +++- .../editor-ui/src/components/WorkflowOpen.vue | 3 +++ .../src/components/mixins/pushConnection.ts | 7 ++++-- .../src/components/mixins/titleChange.ts | 25 +++++++++++++++++++ .../src/components/mixins/workflowRun.ts | 13 +++++++--- packages/editor-ui/src/main.ts | 3 +++ packages/editor-ui/src/views/NodeView.vue | 4 +++ 8 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 packages/editor-ui/src/components/mixins/titleChange.ts diff --git a/packages/editor-ui/src/components/MainHeader.vue b/packages/editor-ui/src/components/MainHeader.vue index 1a40ce261d1..ae4aac9c498 100644 --- a/packages/editor-ui/src/components/MainHeader.vue +++ b/packages/editor-ui/src/components/MainHeader.vue @@ -89,6 +89,7 @@ import { workflowHelpers } from '@/components/mixins/workflowHelpers'; import { saveAs } from 'file-saver'; import mixins from 'vue-typed-mixins'; +import titleChange from './mixins/titleChange'; export default mixins( genericHelpers, @@ -96,6 +97,7 @@ export default mixins( restApi, showMessage, workflowHelpers, + titleChange ) .extend({ name: 'MainHeader', @@ -155,6 +157,7 @@ export default mixins( }, methods: { async openWorkflow (workflowId: string) { + titleChange.set(workflowId, 'IDLE'); // Change to other workflow this.$router.push({ name: 'NodeViewExisting', @@ -162,7 +165,6 @@ export default mixins( }); }, }, - async mounted () { // Initialize the push connection this.pushConnect(); diff --git a/packages/editor-ui/src/components/MainSidebar.vue b/packages/editor-ui/src/components/MainSidebar.vue index baea830b011..85455c1f37e 100644 --- a/packages/editor-ui/src/components/MainSidebar.vue +++ b/packages/editor-ui/src/components/MainSidebar.vue @@ -186,6 +186,7 @@ import { workflowRun } from '@/components/mixins/workflowRun'; import { saveAs } from 'file-saver'; import mixins from 'vue-typed-mixins'; +import titleChange from './mixins/titleChange'; export default mixins( genericHelpers, @@ -194,6 +195,7 @@ export default mixins( workflowHelpers, workflowRun, workflowSave, + titleChange ) .extend({ name: 'MainHeader', @@ -417,7 +419,8 @@ export default mixins( this.$showError(error, 'Problem deleting the workflow', 'There was a problem deleting the workflow:'); return; } - + // Reset tab title since workflow is deleted. + titleChange.reset(); this.$showMessage({ title: 'Workflow got deleted', message: `The workflow "${this.workflowName}" got deleted!`, diff --git a/packages/editor-ui/src/components/WorkflowOpen.vue b/packages/editor-ui/src/components/WorkflowOpen.vue index 9592ecb4d55..9383363b801 100644 --- a/packages/editor-ui/src/components/WorkflowOpen.vue +++ b/packages/editor-ui/src/components/WorkflowOpen.vue @@ -37,11 +37,13 @@ import { showMessage } from '@/components/mixins/showMessage'; import { IWorkflowShortResponse } from '@/Interface'; import mixins from 'vue-typed-mixins'; +import titleChange from './mixins/titleChange'; export default mixins( genericHelpers, restApi, showMessage, + titleChange ).extend({ name: 'WorkflowOpen', props: [ @@ -89,6 +91,7 @@ export default mixins( }, openWorkflow (data: IWorkflowShortResponse, column: any) { // tslint:disable-line:no-any if (column.label !== 'Active') { + titleChange.set(data.name, 'IDLE'); this.$emit('openWorkflow', data.id); } }, diff --git a/packages/editor-ui/src/components/mixins/pushConnection.ts b/packages/editor-ui/src/components/mixins/pushConnection.ts index d9e77d929c5..bfc73182047 100644 --- a/packages/editor-ui/src/components/mixins/pushConnection.ts +++ b/packages/editor-ui/src/components/mixins/pushConnection.ts @@ -12,10 +12,12 @@ import { nodeHelpers } from '@/components/mixins/nodeHelpers'; import { showMessage } from '@/components/mixins/showMessage'; import mixins from 'vue-typed-mixins'; +import titleChange from './titleChange'; export const pushConnection = mixins( nodeHelpers, showMessage, + titleChange ) .extend({ data () { @@ -147,7 +149,7 @@ export const pushConnection = mixins( */ pushMessageReceived (event: Event, isRetry?: boolean): boolean { const retryAttempts = 5; - + const workflow = this.getWorkflow(); let receivedData: IPushData; try { // @ts-ignore @@ -207,7 +209,7 @@ export const pushConnection = mixins( if (runDataExecuted.data.resultData.error && runDataExecuted.data.resultData.error.message) { errorMessage = `There was a problem executing the workflow:
"${runDataExecuted.data.resultData.error.message}"`; } - + titleChange.set(workflow.name, 'ERROR'); this.$showMessage({ title: 'Problem executing workflow', message: errorMessage, @@ -215,6 +217,7 @@ export const pushConnection = mixins( }); } else { // Workflow did execute without a problem + titleChange.set(workflow.name, 'IDLE'); this.$showMessage({ title: 'Workflow got executed', message: 'Workflow did get executed successfully!', diff --git a/packages/editor-ui/src/components/mixins/titleChange.ts b/packages/editor-ui/src/components/mixins/titleChange.ts new file mode 100644 index 00000000000..eb931b24499 --- /dev/null +++ b/packages/editor-ui/src/components/mixins/titleChange.ts @@ -0,0 +1,25 @@ +type Status = 'EXECUTING' | 'IDLE' | 'ERROR'; + +export default { + /** + * Change title of n8n tab + * @param workflow Name of workflow + * @param status Status of workflow + */ + set (workflow : string, status : Status) { + if (status === 'EXECUTING') { + window.document.title = `n8n - 🔄 ${workflow}}`; + } + else if (status === 'IDLE') { + window.document.title = `n8n - ▶️ ${workflow}`; + } + else { + window.document.title = `n8n - ⚠️ ${workflow}`; + } + + }, + + reset () { + document.title = `n8n - Workflow Automation`; + } +}; \ No newline at end of file diff --git a/packages/editor-ui/src/components/mixins/workflowRun.ts b/packages/editor-ui/src/components/mixins/workflowRun.ts index a32814f5bb1..5b064a308c2 100644 --- a/packages/editor-ui/src/components/mixins/workflowRun.ts +++ b/packages/editor-ui/src/components/mixins/workflowRun.ts @@ -14,10 +14,13 @@ import { restApi } from '@/components/mixins/restApi'; import { workflowHelpers } from '@/components/mixins/workflowHelpers'; import mixins from 'vue-typed-mixins'; +import titleChange from './titleChange'; +import { title } from 'process'; export const workflowRun = mixins( restApi, workflowHelpers, + titleChange ).extend({ methods: { // Starts to executes a workflow on server. @@ -27,6 +30,7 @@ export const workflowRun = mixins( // because then it can not receive the data as it executes. throw new Error('No active connection to server. It is maybe down.'); } + const workflow = this.getWorkflow(); this.$store.commit('addActiveAction', 'workflowRunning'); @@ -55,7 +59,8 @@ export const workflowRun = mixins( } const workflow = this.getWorkflow(); - + titleChange.set(workflow.name, 'EXECUTING'); + try { // Check first if the workflow has any issues before execute it const issuesExist = this.$store.getters.nodesIssuesExist; @@ -78,6 +83,7 @@ export const workflowRun = mixins( type: 'error', duration: 0, }); + titleChange.set(workflow.name, 'ERROR'); return; } } @@ -164,9 +170,10 @@ export const workflowRun = mixins( }, }; this.$store.commit('setWorkflowExecutionData', executionData); - - return await this.runWorkflowApi(startRunData); + + return await this.runWorkflowApi(startRunData); } catch (error) { + titleChange.set(workflow.name, 'ERROR'); this.$showError(error, 'Problem running workflow', 'There was a problem running the workflow:'); return undefined; } diff --git a/packages/editor-ui/src/main.ts b/packages/editor-ui/src/main.ts index 37784f7e898..0caf2905cb3 100644 --- a/packages/editor-ui/src/main.ts +++ b/packages/editor-ui/src/main.ts @@ -15,6 +15,8 @@ import './n8n-theme.scss'; import App from '@/App.vue'; import router from './router'; +import titleChange from './components/mixins/titleChange'; + import { library } from '@fortawesome/fontawesome-svg-core'; import { faAngleDoubleLeft, @@ -92,6 +94,7 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; import { store } from './store'; Vue.use(ElementUI, { locale }); +Vue.mixin(titleChange); library.add(faAngleDoubleLeft); library.add(faAngleDown); diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 10b7896856f..b16d4933ac1 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -157,6 +157,7 @@ import { IWorkflowDataUpdate, XYPositon, } from '../Interface'; +import titleChange from '../components/mixins/titleChange'; export default mixins( copyPaste, @@ -167,6 +168,7 @@ export default mixins( showMessage, workflowHelpers, workflowRun, + titleChange ) .extend({ name: 'NodeView', @@ -1324,6 +1326,8 @@ export default mixins( } if (workflowId !== null) { + let workflow = await this.restApi().getWorkflow(workflowId); + titleChange.set(workflow.name, 'IDLE'); // Open existing workflow await this.openWorkflow(workflowId); } else { From b78145f2ec6d1a7cb41bf6916ae0e73da3b5a332 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Tue, 25 Aug 2020 20:38:09 +0200 Subject: [PATCH 24/39] :zap: Fix issues with Dynamic title --- packages/editor-ui/src/Interface.ts | 2 + .../editor-ui/src/components/MainHeader.vue | 6 +-- .../editor-ui/src/components/MainSidebar.vue | 6 +-- .../editor-ui/src/components/WorkflowOpen.vue | 6 +-- .../src/components/mixins/pushConnection.ts | 15 ++++-- .../src/components/mixins/titleChange.ts | 52 +++++++++++-------- .../src/components/mixins/workflowRun.ts | 15 +++--- packages/editor-ui/src/main.ts | 3 -- packages/editor-ui/src/views/NodeView.vue | 9 ++-- 9 files changed, 62 insertions(+), 52 deletions(-) diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 881e938b601..2f3973b8485 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -417,3 +417,5 @@ export interface ITimeoutHMS { minutes: number; seconds: number; } + +export type WorkflowTitleStatus = 'EXECUTING' | 'IDLE' | 'ERROR'; diff --git a/packages/editor-ui/src/components/MainHeader.vue b/packages/editor-ui/src/components/MainHeader.vue index ae4aac9c498..a6d5710156f 100644 --- a/packages/editor-ui/src/components/MainHeader.vue +++ b/packages/editor-ui/src/components/MainHeader.vue @@ -84,20 +84,20 @@ import { genericHelpers } from '@/components/mixins/genericHelpers'; import { pushConnection } from '@/components/mixins/pushConnection'; import { restApi } from '@/components/mixins/restApi'; import { showMessage } from '@/components/mixins/showMessage'; +import { titleChange } from '@/components/mixins/titleChange'; import { workflowHelpers } from '@/components/mixins/workflowHelpers'; import { saveAs } from 'file-saver'; import mixins from 'vue-typed-mixins'; -import titleChange from './mixins/titleChange'; export default mixins( genericHelpers, pushConnection, restApi, showMessage, + titleChange, workflowHelpers, - titleChange ) .extend({ name: 'MainHeader', @@ -157,7 +157,7 @@ export default mixins( }, methods: { async openWorkflow (workflowId: string) { - titleChange.set(workflowId, 'IDLE'); + this.$titleSet(this.workflowName, 'IDLE'); // Change to other workflow this.$router.push({ name: 'NodeViewExisting', diff --git a/packages/editor-ui/src/components/MainSidebar.vue b/packages/editor-ui/src/components/MainSidebar.vue index 85455c1f37e..a54b5305886 100644 --- a/packages/editor-ui/src/components/MainSidebar.vue +++ b/packages/editor-ui/src/components/MainSidebar.vue @@ -179,6 +179,7 @@ import WorkflowSettings from '@/components/WorkflowSettings.vue'; import { genericHelpers } from '@/components/mixins/genericHelpers'; import { restApi } from '@/components/mixins/restApi'; import { showMessage } from '@/components/mixins/showMessage'; +import { titleChange } from '@/components/mixins/titleChange'; import { workflowHelpers } from '@/components/mixins/workflowHelpers'; import { workflowSave } from '@/components/mixins/workflowSave'; import { workflowRun } from '@/components/mixins/workflowRun'; @@ -186,16 +187,15 @@ import { workflowRun } from '@/components/mixins/workflowRun'; import { saveAs } from 'file-saver'; import mixins from 'vue-typed-mixins'; -import titleChange from './mixins/titleChange'; export default mixins( genericHelpers, restApi, showMessage, + titleChange, workflowHelpers, workflowRun, workflowSave, - titleChange ) .extend({ name: 'MainHeader', @@ -420,7 +420,7 @@ export default mixins( return; } // Reset tab title since workflow is deleted. - titleChange.reset(); + this.$titleReset(); this.$showMessage({ title: 'Workflow got deleted', message: `The workflow "${this.workflowName}" got deleted!`, diff --git a/packages/editor-ui/src/components/WorkflowOpen.vue b/packages/editor-ui/src/components/WorkflowOpen.vue index 9383363b801..abedea5b4c0 100644 --- a/packages/editor-ui/src/components/WorkflowOpen.vue +++ b/packages/editor-ui/src/components/WorkflowOpen.vue @@ -34,16 +34,16 @@ import WorkflowActivator from '@/components/WorkflowActivator.vue'; import { restApi } from '@/components/mixins/restApi'; import { genericHelpers } from '@/components/mixins/genericHelpers'; import { showMessage } from '@/components/mixins/showMessage'; +import { titleChange } from '@/components/mixins/titleChange'; import { IWorkflowShortResponse } from '@/Interface'; import mixins from 'vue-typed-mixins'; -import titleChange from './mixins/titleChange'; export default mixins( genericHelpers, restApi, showMessage, - titleChange + titleChange, ).extend({ name: 'WorkflowOpen', props: [ @@ -91,7 +91,7 @@ export default mixins( }, openWorkflow (data: IWorkflowShortResponse, column: any) { // tslint:disable-line:no-any if (column.label !== 'Active') { - titleChange.set(data.name, 'IDLE'); + this.$titleSet(data.name, 'IDLE'); this.$emit('openWorkflow', data.id); } }, diff --git a/packages/editor-ui/src/components/mixins/pushConnection.ts b/packages/editor-ui/src/components/mixins/pushConnection.ts index bfc73182047..ebda8f837ae 100644 --- a/packages/editor-ui/src/components/mixins/pushConnection.ts +++ b/packages/editor-ui/src/components/mixins/pushConnection.ts @@ -10,14 +10,14 @@ import { import { nodeHelpers } from '@/components/mixins/nodeHelpers'; import { showMessage } from '@/components/mixins/showMessage'; +import { titleChange } from '@/components/mixins/titleChange'; import mixins from 'vue-typed-mixins'; -import titleChange from './titleChange'; export const pushConnection = mixins( nodeHelpers, showMessage, - titleChange + titleChange, ) .extend({ data () { @@ -149,7 +149,6 @@ export const pushConnection = mixins( */ pushMessageReceived (event: Event, isRetry?: boolean): boolean { const retryAttempts = 5; - const workflow = this.getWorkflow(); let receivedData: IPushData; try { // @ts-ignore @@ -203,13 +202,19 @@ export const pushConnection = mixins( const runDataExecuted = pushData.data; + console.log('..pushData..'); + console.log(pushData); + + + // @ts-ignore + const workflow = this.getWorkflow(); if (runDataExecuted.finished !== true) { // There was a problem with executing the workflow let errorMessage = 'There was a problem executing the workflow!'; if (runDataExecuted.data.resultData.error && runDataExecuted.data.resultData.error.message) { errorMessage = `There was a problem executing the workflow:
"${runDataExecuted.data.resultData.error.message}"`; } - titleChange.set(workflow.name, 'ERROR'); + this.$titleSet(workflow.name, 'ERROR'); this.$showMessage({ title: 'Problem executing workflow', message: errorMessage, @@ -217,7 +222,7 @@ export const pushConnection = mixins( }); } else { // Workflow did execute without a problem - titleChange.set(workflow.name, 'IDLE'); + this.$titleSet(workflow.name, 'IDLE'); this.$showMessage({ title: 'Workflow got executed', message: 'Workflow did get executed successfully!', diff --git a/packages/editor-ui/src/components/mixins/titleChange.ts b/packages/editor-ui/src/components/mixins/titleChange.ts index eb931b24499..0f7a0bd788e 100644 --- a/packages/editor-ui/src/components/mixins/titleChange.ts +++ b/packages/editor-ui/src/components/mixins/titleChange.ts @@ -1,25 +1,31 @@ -type Status = 'EXECUTING' | 'IDLE' | 'ERROR'; +import Vue from 'vue'; -export default { - /** - * Change title of n8n tab - * @param workflow Name of workflow - * @param status Status of workflow - */ - set (workflow : string, status : Status) { - if (status === 'EXECUTING') { - window.document.title = `n8n - 🔄 ${workflow}}`; - } - else if (status === 'IDLE') { - window.document.title = `n8n - ▶️ ${workflow}`; - } - else { - window.document.title = `n8n - ⚠️ ${workflow}`; - } - - }, +import { + WorkflowTitleStatus, +} from '../../Interface'; - reset () { - document.title = `n8n - Workflow Automation`; - } -}; \ No newline at end of file +export const titleChange = Vue.extend({ + methods: { + /** + * Change title of n8n tab + * + * @param {string} workflow Name of workflow + * @param {WorkflowTitleStatus} status Status of workflow + */ + $titleSet(workflow: string, status: WorkflowTitleStatus) { + let icon = '⚠️'; + if (status === 'EXECUTING') { + icon = '🔄'; + } else if (status === 'IDLE') { + icon = '▶️'; + } + + window.document.title = `n8n - ${icon} ${workflow}`; + }, + + $titleReset() { + document.title = `n8n - Workflow Automation`; + }, + + }, +}); diff --git a/packages/editor-ui/src/components/mixins/workflowRun.ts b/packages/editor-ui/src/components/mixins/workflowRun.ts index 5b064a308c2..08ac66a110b 100644 --- a/packages/editor-ui/src/components/mixins/workflowRun.ts +++ b/packages/editor-ui/src/components/mixins/workflowRun.ts @@ -14,13 +14,12 @@ import { restApi } from '@/components/mixins/restApi'; import { workflowHelpers } from '@/components/mixins/workflowHelpers'; import mixins from 'vue-typed-mixins'; -import titleChange from './titleChange'; -import { title } from 'process'; +import { titleChange } from './titleChange'; export const workflowRun = mixins( restApi, workflowHelpers, - titleChange + titleChange, ).extend({ methods: { // Starts to executes a workflow on server. @@ -59,8 +58,8 @@ export const workflowRun = mixins( } const workflow = this.getWorkflow(); - titleChange.set(workflow.name, 'EXECUTING'); - + this.$titleSet(workflow.name as string, 'EXECUTING'); + try { // Check first if the workflow has any issues before execute it const issuesExist = this.$store.getters.nodesIssuesExist; @@ -83,7 +82,7 @@ export const workflowRun = mixins( type: 'error', duration: 0, }); - titleChange.set(workflow.name, 'ERROR'); + this.$titleSet(workflow.name as string, 'ERROR'); return; } } @@ -170,10 +169,10 @@ export const workflowRun = mixins( }, }; this.$store.commit('setWorkflowExecutionData', executionData); - + return await this.runWorkflowApi(startRunData); } catch (error) { - titleChange.set(workflow.name, 'ERROR'); + this.$titleSet(workflow.name as string, 'ERROR'); this.$showError(error, 'Problem running workflow', 'There was a problem running the workflow:'); return undefined; } diff --git a/packages/editor-ui/src/main.ts b/packages/editor-ui/src/main.ts index 0caf2905cb3..37784f7e898 100644 --- a/packages/editor-ui/src/main.ts +++ b/packages/editor-ui/src/main.ts @@ -15,8 +15,6 @@ import './n8n-theme.scss'; import App from '@/App.vue'; import router from './router'; -import titleChange from './components/mixins/titleChange'; - import { library } from '@fortawesome/fontawesome-svg-core'; import { faAngleDoubleLeft, @@ -94,7 +92,6 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; import { store } from './store'; Vue.use(ElementUI, { locale }); -Vue.mixin(titleChange); library.add(faAngleDoubleLeft); library.add(faAngleDown); diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index b16d4933ac1..dd4c7565578 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -115,6 +115,8 @@ import { mouseSelect } from '@/components/mixins/mouseSelect'; import { moveNodeWorkflow } from '@/components/mixins/moveNodeWorkflow'; import { restApi } from '@/components/mixins/restApi'; import { showMessage } from '@/components/mixins/showMessage'; +import { titleChange } from '@/components/mixins/titleChange'; + import { workflowHelpers } from '@/components/mixins/workflowHelpers'; import { workflowRun } from '@/components/mixins/workflowRun'; @@ -157,7 +159,6 @@ import { IWorkflowDataUpdate, XYPositon, } from '../Interface'; -import titleChange from '../components/mixins/titleChange'; export default mixins( copyPaste, @@ -166,9 +167,9 @@ export default mixins( moveNodeWorkflow, restApi, showMessage, + titleChange, workflowHelpers, workflowRun, - titleChange ) .extend({ name: 'NodeView', @@ -1326,8 +1327,8 @@ export default mixins( } if (workflowId !== null) { - let workflow = await this.restApi().getWorkflow(workflowId); - titleChange.set(workflow.name, 'IDLE'); + const workflow = await this.restApi().getWorkflow(workflowId); + this.$titleSet(workflow.name, 'IDLE'); // Open existing workflow await this.openWorkflow(workflowId); } else { From e3e39c4f627f8942d7e87780c0985a3802d0ae8e Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Tue, 25 Aug 2020 15:50:13 -0400 Subject: [PATCH 25/39] :zap: Reuse trigger and target on Zendesk-Trigger (#767) * :zap: Reuse trigger and target on Zendesk-Trigger * :bug: Fix bug --- .../nodes/Zendesk/GenericFunctions.ts | 2 + .../nodes/Zendesk/ZendeskTrigger.node.ts | 93 ++++++++++++++++--- 2 files changed, 83 insertions(+), 12 deletions(-) diff --git a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts index 271c10259f3..5b4757abc88 100644 --- a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts @@ -43,6 +43,8 @@ export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions options.uri = `https://${credentials.subdomain}.zendesk.com/api/v2${resource}.json`; options.headers!['Authorization'] = `Basic ${base64Key}`; + //console.log(options); + return await this.helpers.request!(options); } else { const credentials = this.getCredentials('zendeskOAuth2Api'); diff --git a/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts b/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts index 421d9e4b32f..c8d1a8b3779 100644 --- a/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts +++ b/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts @@ -375,14 +375,14 @@ export class ZendeskTrigger implements INodeType { displayName: 'All', values: [ ...conditionFields, - ] + ], }, { name: 'any', displayName: 'Any', values: [ ...conditionFields, - ] + ], }, ], }, @@ -435,17 +435,74 @@ export class ZendeskTrigger implements INodeType { webhookMethods = { default: { async checkExists(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default') as string; const webhookData = this.getWorkflowStaticData('node'); - if (webhookData.webhookId === undefined) { + const conditions = this.getNodeParameter('conditions') as IDataObject; + const conditionsAll = conditions.all as [IDataObject]; + + let endpoint = ''; + const aux: IDataObject = {}; + const resultAll = [], resultAny = []; + + if (conditionsAll) { + for (const conditionAll of conditionsAll) { + aux.field = conditionAll.field; + aux.operator = conditionAll.operation; + if (conditionAll.operation !== 'changed' + && conditionAll.operation !== 'not_changed') { + aux.value = conditionAll.value; + } else { + aux.value = null; + } + resultAll.push(aux); + } + } + + const conditionsAny = conditions.any as [IDataObject]; + if (conditionsAny) { + for (const conditionAny of conditionsAny) { + aux.field = conditionAny.field; + aux.operator = conditionAny.operation; + if (conditionAny.operation !== 'changed' + && conditionAny.operation !== 'not_changed') { + aux.value = conditionAny.value; + } else { + aux.value = null; + } + resultAny.push(aux); + } + } + + // check if there is a target already created + endpoint = `/targets`; + const targets = await zendeskApiRequestAllItems.call(this, 'targets', 'GET', endpoint); + for (const target of targets) { + if (target.target_url === webhookUrl) { + webhookData.targetId = target.id.toString(); + break; + } + } + + // no target was found + if (webhookData.targetId === undefined) { return false; } - const endpoint = `/triggers/${webhookData.webhookId}`; - try { - await zendeskApiRequest.call(this, 'GET', endpoint); - } catch (e) { - return false; + + endpoint = `/triggers/active`; + const triggers = await zendeskApiRequestAllItems.call(this, 'triggers', 'GET', endpoint); + for (const trigger of triggers) { + const toDeleteTriggers = []; + // this trigger belong to the current target + if (trigger.actions[0].value[0].toString() === webhookData.targetId?.toString()) { + toDeleteTriggers.push(trigger.id); + } + // delete all trigger attach to this target; + if (toDeleteTriggers.length !== 0) { + await zendeskApiRequest.call(this, 'DELETE', '/triggers/destroy_many', {}, { ids: toDeleteTriggers.join(',') } ); + } } - return true; + + return false; }, async create(this: IHookFunctions): Promise { const webhookUrl = this.getNodeWebhookUrl('default') as string; @@ -509,8 +566,8 @@ export class ZendeskTrigger implements INodeType { { field: 'notification_target', value: [], - } - ] + }, + ], }, }; const bodyTarget: IDataObject = { @@ -523,9 +580,21 @@ export class ZendeskTrigger implements INodeType { content_type: 'application/json', }, }; - const { target } = await zendeskApiRequest.call(this, 'POST', '/targets', bodyTarget); + let target: IDataObject = {}; + + // if target id exists but trigger does not then reuse the target + // and create the trigger else create both + if (webhookData.targetId !== undefined) { + target.id = webhookData.targetId; + } else { + target = await zendeskApiRequest.call(this, 'POST', '/targets', bodyTarget); + target = target.target as IDataObject; + } + // @ts-ignore bodyTrigger.trigger.actions[0].value = [target.id, JSON.stringify(message)]; + + //@ts-ignore const { trigger } = await zendeskApiRequest.call(this, 'POST', '/triggers', bodyTrigger); webhookData.webhookId = trigger.id; webhookData.targetId = target.id; From 426f0ae7844484d4b0188d3bd90265f2f4956a20 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Tue, 25 Aug 2020 23:16:38 +0200 Subject: [PATCH 26/39] :zap: Minor improvements to ConvertKit-Node --- .../nodes/ConvertKit/ConvertKit.node.ts | 16 ++++++++-------- .../nodes/ConvertKit/ConvertKitTrigger.node.ts | 12 +++--------- .../nodes/ConvertKit/GenericFunctions.ts | 6 +++--- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/packages/nodes-base/nodes/ConvertKit/ConvertKit.node.ts b/packages/nodes-base/nodes/ConvertKit/ConvertKit.node.ts index 8c9870639ea..7933379ad64 100644 --- a/packages/nodes-base/nodes/ConvertKit/ConvertKit.node.ts +++ b/packages/nodes-base/nodes/ConvertKit/ConvertKit.node.ts @@ -6,9 +6,9 @@ import { import { IDataObject, INodeExecutionData, + INodePropertyOptions, INodeTypeDescription, INodeType, - INodePropertyOptions, } from 'n8n-workflow'; import { @@ -16,28 +16,28 @@ import { } from './GenericFunctions'; import { - customFieldOperations, customFieldFields, + customFieldOperations, } from './CustomFieldDescription'; import { - formOperations, formFields, + formOperations, } from './FormDescription'; import { - sequenceOperations, sequenceFields, + sequenceOperations, } from './SequenceDescription'; import { - tagOperations, tagFields, + tagOperations, } from './TagDescription'; import { - tagSubscriberOperations, tagSubscriberFields, + tagSubscriberOperations, } from './TagSubscriberDescription'; export class ConvertKit implements INodeType { @@ -88,7 +88,7 @@ export class ConvertKit implements INodeType { value: 'tagSubscriber', }, ], - default: 'customField', + default: 'form', description: 'The resource to operate on.' }, //-------------------- @@ -470,7 +470,7 @@ export class ConvertKit implements INodeType { const email = this.getNodeParameter('email', i) as string; - responseData= await convertKitApiRequest.call(this, 'POST', `/tags/${tagId}>/unsubscribe`, { email }); + responseData = await convertKitApiRequest.call(this, 'POST', `/tags/${tagId}>/unsubscribe`, { email }); } } diff --git a/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts b/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts index 580bab7d565..84fa78c93f4 100644 --- a/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts +++ b/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts @@ -5,11 +5,11 @@ import { import { IDataObject, + ILoadOptionsFunctions, + INodePropertyOptions, INodeTypeDescription, INodeType, IWebhookResponseData, - ILoadOptionsFunctions, - INodePropertyOptions, } from 'n8n-workflow'; import { @@ -266,7 +266,7 @@ export class ConvertKitTrigger implements INodeType { // THe API does not have an endpoint to list all webhooks - if(webhookData.webhookId) { + if (webhookData.webhookId) { return true; } @@ -282,11 +282,8 @@ export class ConvertKitTrigger implements INodeType { const endpoint = '/automations/hooks'; if (event === 'purchaseCreate') { - event = `purchase.${snakeCase(event)}`; - } else { - event = `subscriber.${snakeCase(event)}`; } @@ -344,11 +341,8 @@ export class ConvertKitTrigger implements INodeType { const endpoint = `/automations/hooks/${webhookData.webhookId}`; try { - await convertKitApiRequest.call(this, 'DELETE', endpoint); - } catch (error) { - return false; } diff --git a/packages/nodes-base/nodes/ConvertKit/GenericFunctions.ts b/packages/nodes-base/nodes/ConvertKit/GenericFunctions.ts index 81c8e43b327..2f034f6892e 100644 --- a/packages/nodes-base/nodes/ConvertKit/GenericFunctions.ts +++ b/packages/nodes-base/nodes/ConvertKit/GenericFunctions.ts @@ -4,8 +4,8 @@ import { import { IExecuteFunctions, - ILoadOptionsFunctions, IExecuteSingleFunctions, + ILoadOptionsFunctions, } from 'n8n-core'; import { @@ -16,7 +16,7 @@ import { export async function convertKitApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IHookFunctions, method: string, endpoint: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any - const credentials = this.getCredentials('convertKitApi'); + const credentials = this.getCredentials('convertKitApi'); if (credentials === undefined) { throw new Error('No credentials got returned!'); @@ -29,7 +29,7 @@ export async function convertKitApiRequest(this: IExecuteFunctions | IExecuteSin method, qs, body, - uri: uri ||`https://api.convertkit.com/v3${endpoint}`, + uri: uri || `https://api.convertkit.com/v3${endpoint}`, json: true, }; From c3277df25bf053948e64079b7f79d97c569fa8aa Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Wed, 26 Aug 2020 03:09:07 -0400 Subject: [PATCH 27/39] :zap: Todoist node enhancement (#823) * Todoist node enhancement * done * :zap: Improvements * :lipstick: Remove comment * :lipstick: remove unnecessary line * :books: Add breaking change message * :zap: Remove unnecessary line Co-authored-by: lukigarazus --- packages/cli/BREAKING-CHANGES.md | 20 + .../TodoistOAuth2Api.credentials.ts | 46 ++ .../nodes/Todoist/GenericFunctions.ts | 75 ++- .../nodes-base/nodes/Todoist/Todoist.node.ts | 430 ++++++++++++------ packages/nodes-base/nodes/Todoist/todoist.png | Bin 4124 -> 5648 bytes packages/nodes-base/package.json | 1 + 6 files changed, 395 insertions(+), 177 deletions(-) create mode 100644 packages/nodes-base/credentials/TodoistOAuth2Api.credentials.ts diff --git a/packages/cli/BREAKING-CHANGES.md b/packages/cli/BREAKING-CHANGES.md index 68be5ce947e..c61a00fc5a7 100644 --- a/packages/cli/BREAKING-CHANGES.md +++ b/packages/cli/BREAKING-CHANGES.md @@ -2,6 +2,26 @@ This list shows all the versions which include breaking changes and how to upgrade. +## 0.80.0 + +### What changed? + +We have renamed the operations on the Todoist Node to keep consistency with the codebase. Also, deleted the operations close_match and delete_match as these operations can be accomplished using the operations getAll, close, and delete. + +### When is action necessary? + +When one of the following operations is used. + +- close_by +- close_match +- delete_id +- delete_match + +### How to upgrade: + +After upgrading open all workflows, which contain the Todoist Node, set the corresponding operation, and then save the workflow. + +If the operations close_match or delete_match are used, recreate them using the operations getAll, delete and close. ## 0.69.0 diff --git a/packages/nodes-base/credentials/TodoistOAuth2Api.credentials.ts b/packages/nodes-base/credentials/TodoistOAuth2Api.credentials.ts new file mode 100644 index 00000000000..9341e69c657 --- /dev/null +++ b/packages/nodes-base/credentials/TodoistOAuth2Api.credentials.ts @@ -0,0 +1,46 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class TodoistOAuth2Api implements ICredentialType { + name = 'todoistOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Todoist OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://todoist.com/oauth/authorize', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://todoist.com/oauth/access_token', + required: true, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: 'data:read_write,data:delete', + }, + { + 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/Todoist/GenericFunctions.ts b/packages/nodes-base/nodes/Todoist/GenericFunctions.ts index f4fdfad5e1b..d8d7767680a 100644 --- a/packages/nodes-base/nodes/Todoist/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Todoist/GenericFunctions.ts @@ -1,66 +1,37 @@ -import { OptionsWithUri } from 'request'; +import { + OptionsWithUri, +} from 'request'; import { IExecuteFunctions, IHookFunctions, ILoadOptionsFunctions, - IExecuteSingleFunctions } from 'n8n-core'; -import * as _ from 'lodash'; - -export const filterAndExecuteForEachTask = async function( - this: IExecuteSingleFunctions, - taskCallback: (t: any) => any -) { - const expression = this.getNodeParameter('expression') as string; - const projectId = this.getNodeParameter('project') as number; - // Enable regular expressions - const reg = new RegExp(expression); - const tasks = await todoistApiRequest.call(this, '/tasks', 'GET'); - const filteredTasks = tasks.filter( - // Make sure that project will match no matter what the type is. If project was not selected match all projects - (el: any) => (!projectId || el.project_id) && el.content.match(reg) - ); - return { - affectedTasks: ( - await Promise.all(filteredTasks.map((t: any) => taskCallback(t))) - ) - // This makes it more clear and informative. We pass the ID as a convention and content to give the user confirmation that his/her expression works as expected - .map( - (el, i) => - el || { id: filteredTasks[i].id, content: filteredTasks[i].content } - ) - }; -}; +import { + IDataObject, +} from 'n8n-workflow'; export async function todoistApiRequest( this: | IHookFunctions | IExecuteFunctions - | IExecuteSingleFunctions | ILoadOptionsFunctions, - resource: string, method: string, + resource: string, body: any = {}, - headers?: object -): Promise { - // tslint:disable-line:no-any - const credentials = this.getCredentials('todoistApi'); - - if (credentials === undefined) { - throw new Error('No credentials got returned!'); - } - - const headerWithAuthentication = Object.assign({}, headers, { Authorization: `Bearer ${credentials.apiKey}` }); + qs: IDataObject = {}, +): Promise { // tslint:disable-line:no-any + const authentication = this.getNodeParameter('authentication', 0, 'apiKey'); const endpoint = 'api.todoist.com/rest/v1'; const options: OptionsWithUri = { - headers: headerWithAuthentication, + headers: {}, method, + qs, uri: `https://${endpoint}${resource}`, - json: true + json: true, }; if (Object.keys(body).length !== 0) { @@ -68,13 +39,25 @@ export async function todoistApiRequest( } try { - return this.helpers.request!(options); + if (authentication === 'apiKey') { + const credentials = this.getCredentials('todoistApi') as IDataObject; + + //@ts-ignore + options.headers['Authorization'] = `Bearer ${credentials.apiKey}`; + + return this.helpers.request!(options); + } else { + //@ts-ignore + return await this.helpers.requestOAuth2.call(this, 'todoistOAuth2Api', options); + } + } catch (error) { - const errorMessage = error.response.body.message || error.response.body.Message; + const errorMessage = error.response.body; if (errorMessage !== undefined) { - throw errorMessage; + throw new Error(errorMessage); } - throw error.response.body; + + throw errorMessage; } } diff --git a/packages/nodes-base/nodes/Todoist/Todoist.node.ts b/packages/nodes-base/nodes/Todoist/Todoist.node.ts index fc70424c9f2..66a8c1120a5 100644 --- a/packages/nodes-base/nodes/Todoist/Todoist.node.ts +++ b/packages/nodes-base/nodes/Todoist/Todoist.node.ts @@ -1,6 +1,7 @@ -import { - IExecuteSingleFunctions, +import { + IExecuteFunctions, } from 'n8n-core'; + import { IDataObject, INodeTypeDescription, @@ -9,12 +10,11 @@ import { ILoadOptionsFunctions, INodePropertyOptions, } from 'n8n-workflow'; + import { todoistApiRequest, - filterAndExecuteForEachTask, } from './GenericFunctions'; - interface IBodyCreateTask { content: string; project_id?: number; @@ -48,9 +48,44 @@ export class Todoist implements INodeType { { name: 'todoistApi', required: true, - } + displayOptions: { + show: { + authentication: [ + 'apiKey', + ], + }, + }, + }, + { + name: 'todoistOAuth2Api', + required: true, + displayOptions: { + show: { + authentication: [ + 'oAuth2', + ], + }, + }, + }, ], properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'API Key', + value: 'apiKey', + }, + { + name: 'OAuth2', + value: 'oAuth2', + }, + ], + default: 'apiKey', + description: 'The resource to operate on.', + }, { displayName: 'Resource', name: 'resource', @@ -85,24 +120,29 @@ export class Todoist implements INodeType { description: 'Create a new task', }, { - name: 'Close by ID', - value: 'close_id', - description: 'Close a task by passing an ID', + name: 'Close', + value: 'close', + description: 'Close a task', }, { - name: 'Close matching', - value: 'close_match', - description: 'Close a task by passing a regular expression', + name: 'Delete', + value: 'delete', + description: 'Delete a task', }, { - name: 'Delete by ID', - value: 'delete_id', - description: 'Delete a task by passing an ID', + name: 'Get', + value: 'get', + description: 'Get a task', }, { - name: 'Delete matching', - value: 'delete_match', - description: 'Delete a task by passing a regular expression', + name: 'Get All', + value: 'getAll', + description: 'Get all tasks', + }, + { + name: 'Reopen', + value: 'reopen', + description: 'Reopen a task', }, ], default: 'create', @@ -122,9 +162,7 @@ export class Todoist implements INodeType { ], operation: [ 'create', - 'close_match', - 'delete_match', - ] + ], }, }, default: '', @@ -144,7 +182,7 @@ export class Todoist implements INodeType { ], operation: [ 'create', - ] + ], }, }, default: [], @@ -165,7 +203,7 @@ export class Todoist implements INodeType { ], operation: [ 'create', - ] + ], }, }, default: '', @@ -173,32 +211,27 @@ export class Todoist implements INodeType { description: 'Task content', }, { - displayName: 'ID', - name: 'id', + displayName: 'Task ID', + name: 'taskId', type: 'string', default: '', required: true, - typeOptions: { rows: 1 }, displayOptions: { - show: { resource: ['task'], operation: ['close_id', 'delete_id'] } + show: { + resource: [ + 'task', + ], + operation: [ + 'delete', + 'close', + 'get', + 'reopen', + ], + }, }, }, { - displayName: 'Expression to match', - name: 'expression', - type: 'string', - default: '', - required: true, - typeOptions: { rows: 1 }, - displayOptions: { - show: { - resource: ['task'], - operation: ['close_match', 'delete_match'] - } - } - }, - { - displayName: 'Options', + displayName: 'Additional Fields', name: 'options', type: 'collection', placeholder: 'Add Option', @@ -210,22 +243,10 @@ export class Todoist implements INodeType { ], operation: [ 'create', - ] + ], }, }, options: [ - { - displayName: 'Priority', - name: 'priority', - type: 'number', - typeOptions: { - numberStepSize: 1, - maxValue: 4, - minValue: 1, - }, - default: 1, - description: 'Task priority from 1 (normal) to 4 (urgent).', - }, { displayName: 'Due Date Time', name: 'dueDateTime', @@ -240,24 +261,131 @@ export class Todoist implements INodeType { default: '', description: 'Human defined task due date (ex.: “next Monday”, “Tomorrow”). Value is set using local (not UTC) time.', }, - ] - } - ] + { + displayName: 'Priority', + name: 'priority', + type: 'number', + typeOptions: { + numberStepSize: 1, + maxValue: 4, + minValue: 1, + }, + default: 1, + description: 'Task priority from 1 (normal) to 4 (urgent).', + }, + ], + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'task', + ], + }, + }, + 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: [ + 'task', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Filter', + name: 'filter', + type: 'string', + default: '', + description: 'Filter by any supported filter.', + }, + { + displayName: 'IDs', + name: 'ids', + type: 'string', + default: '', + description: 'A list of the task IDs to retrieve, this should be a comma separated list.', + }, + { + displayName: 'Label ID', + name: 'labelId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLabels', + }, + default: {}, + description: 'Filter tasks by label.', + }, + { + displayName: 'Lang', + name: 'lang', + type: 'string', + default: '', + description: 'IETF language tag defining what language filter is written in, if differs from default English', + }, + { + displayName: 'Project ID', + name: 'projectId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getProjects', + }, + default: '', + description: 'Filter tasks by project id.', + }, + ], + }, + ], }; - methods = { loadOptions: { // Get all the available projects to display them to user so that he can // select them easily async getProjects(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - let projects; - try { - projects = await todoistApiRequest.call(this, '/projects', 'GET'); - } catch (err) { - throw new Error(`Todoist Error: ${err}`); - } + const projects = await todoistApiRequest.call(this, 'GET', '/projects'); for (const project of projects) { const projectName = project.name; const projectId = project.id; @@ -275,12 +403,8 @@ export class Todoist implements INodeType { // select them easily async getLabels(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - let labels; - try { - labels = await todoistApiRequest.call(this, '/labels', 'GET'); - } catch (err) { - throw new Error(`Todoist Error: ${err}`); - } + const labels = await todoistApiRequest.call(this, 'GET', '/labels'); + for (const label of labels) { const labelName = label.name; const labelId = label.id; @@ -296,67 +420,111 @@ export class Todoist implements INodeType { } }; - async executeSingle(this: IExecuteSingleFunctions): Promise { - const resource = this.getNodeParameter('resource') as string; - const operation = this.getNodeParameter('operation') as string; - try { - return { - json: { result: await OPERATIONS[resource]?.[operation]?.bind(this)() } - }; - } catch (err) { - return { json: { error: `Todoist Error: ${err.message}` } }; + 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; + for (let i = 0; i < length; i++) { + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + if (resource === 'task') { + if (operation === 'create') { + //https://developer.todoist.com/rest/v1/#create-a-new-task + const content = this.getNodeParameter('content', i) as string; + const projectId = this.getNodeParameter('project', i) as number; + const labels = this.getNodeParameter('labels', i) as number[]; + const options = this.getNodeParameter('options', i) as IDataObject; + + const body: IBodyCreateTask = { + content, + project_id: projectId, + priority: (options.priority!) ? parseInt(options.priority as string, 10) : 1, + }; + + if (options.dueDateTime) { + body.due_datetime = options.dueDateTime as string; + } + + if (options.dueString) { + body.due_string = options.dueString as string; + } + + if (labels !== undefined && labels.length !== 0) { + body.label_ids = labels; + } + + responseData = await todoistApiRequest.call(this, 'POST', '/tasks', body); + } + if (operation === 'close') { + //https://developer.todoist.com/rest/v1/#close-a-task + const id = this.getNodeParameter('taskId', i) as string; + + responseData = await todoistApiRequest.call(this, 'POST', `/tasks/${id}/close`); + + responseData = { success: true }; + + } + if (operation === 'delete') { + //https://developer.todoist.com/rest/v1/#delete-a-task + const id = this.getNodeParameter('taskId', i) as string; + + responseData = await todoistApiRequest.call(this, 'DELETE', `/tasks/${id}`); + + responseData = { success: true }; + + } + if (operation === 'get') { + //https://developer.todoist.com/rest/v1/#get-an-active-task + const id = this.getNodeParameter('taskId', i) as string; + + responseData = await todoistApiRequest.call(this, 'GET', `/tasks/${id}`); + } + if (operation === 'getAll') { + //https://developer.todoist.com/rest/v1/#get-active-tasks + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + if (filters.projectId) { + qs.project_id = filters.projectId as string; + } + if (filters.labelId) { + qs.label_id = filters.labelId as string; + } + if (filters.filter) { + qs.filter = filters.filter as string; + } + if (filters.lang) { + qs.lang = filters.lang as string; + } + if (filters.ids) { + qs.ids = filters.ids as string; + } + + responseData = await todoistApiRequest.call(this, 'GET', '/tasks', {}, qs); + + if (!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.splice(0, limit); + } + } + if (operation === 'reopen') { + //https://developer.todoist.com/rest/v1/#get-an-active-task + const id = this.getNodeParameter('taskId', i) as string; + + responseData = await todoistApiRequest.call(this, 'POST', `/tasks/${id}/reopen`); + + responseData = { success: true }; + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else { + returnData.push(responseData as IDataObject); + } } + + return [this.helpers.returnJsonArray(returnData)]; } } - -const OPERATIONS: { - [key: string]: { [key: string]: (this: IExecuteSingleFunctions) => any }; -} = { - task: { - create(this: IExecuteSingleFunctions) { - //https://developer.todoist.com/rest/v1/#create-a-new-task - const content = this.getNodeParameter('content') as string; - const projectId = this.getNodeParameter('project') as number; - const labels = this.getNodeParameter('labels') as number[]; - const options = this.getNodeParameter('options') as IDataObject; - - const body: IBodyCreateTask = { - content, - project_id: projectId, - priority: (options.priority!) ? parseInt(options.priority as string, 10) : 1, - }; - - if (options.dueDateTime) { - body.due_datetime = options.dueDateTime as string; - } - - if (options.dueString) { - body.due_string = options.dueString as string; - } - - if (labels !== undefined && labels.length !== 0) { - body.label_ids = labels; - } - - return todoistApiRequest.call(this, '/tasks', 'POST', body); - }, - close_id(this: IExecuteSingleFunctions) { - const id = this.getNodeParameter('id') as string; - return todoistApiRequest.call(this, `/tasks/${id}/close`, 'POST'); - }, - delete_id(this: IExecuteSingleFunctions) { - const id = this.getNodeParameter('id') as string; - return todoistApiRequest.call(this, `/tasks/${id}`, 'DELETE'); - }, - close_match(this) { - return filterAndExecuteForEachTask.call(this, t => - todoistApiRequest.call(this, `/tasks/${t.id}/close`, 'POST') - ); - }, - delete_match(this) { - return filterAndExecuteForEachTask.call(this, t => - todoistApiRequest.call(this, `/tasks/${t.id}`, 'DELETE') - ); - } - } -}; diff --git a/packages/nodes-base/nodes/Todoist/todoist.png b/packages/nodes-base/nodes/Todoist/todoist.png index 00af868776dba6a2481f26d5d68022af41ec6b86..db8c55479ddee91544f786a198a6cce6393443e3 100644 GIT binary patch literal 5648 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 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index acbba359853..e3e597819c4 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -151,6 +151,7 @@ "dist/credentials/SurveyMonkeyOAuth2Api.credentials.js", "dist/credentials/TelegramApi.credentials.js", "dist/credentials/TodoistApi.credentials.js", + "dist/credentials/TodoistOAuth2Api.credentials.js", "dist/credentials/TravisCiApi.credentials.js", "dist/credentials/TrelloApi.credentials.js", "dist/credentials/TwilioApi.credentials.js", From d768764a70144c6d41877de1ea1eb02557c25edb Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 26 Aug 2020 09:28:39 +0200 Subject: [PATCH 28/39] :zap: Minor improvements to Todoist-Node --- packages/cli/BREAKING-CHANGES.md | 2 +- .../nodes/Todoist/GenericFunctions.ts | 2 +- .../nodes-base/nodes/Todoist/Todoist.node.ts | 58 ++++++++++--------- 3 files changed, 32 insertions(+), 30 deletions(-) diff --git a/packages/cli/BREAKING-CHANGES.md b/packages/cli/BREAKING-CHANGES.md index c61a00fc5a7..1354c7d73cc 100644 --- a/packages/cli/BREAKING-CHANGES.md +++ b/packages/cli/BREAKING-CHANGES.md @@ -2,7 +2,7 @@ This list shows all the versions which include breaking changes and how to upgrade. -## 0.80.0 +## 0.79.0 ### What changed? diff --git a/packages/nodes-base/nodes/Todoist/GenericFunctions.ts b/packages/nodes-base/nodes/Todoist/GenericFunctions.ts index d8d7767680a..31d8d85f99c 100644 --- a/packages/nodes-base/nodes/Todoist/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Todoist/GenericFunctions.ts @@ -19,7 +19,7 @@ export async function todoistApiRequest( | ILoadOptionsFunctions, method: string, resource: string, - body: any = {}, + body: any = {}, // tslint:disable-line:no-any qs: IDataObject = {}, ): Promise { // tslint:disable-line:no-any const authentication = this.getNodeParameter('authentication', 0, 'apiKey'); diff --git a/packages/nodes-base/nodes/Todoist/Todoist.node.ts b/packages/nodes-base/nodes/Todoist/Todoist.node.ts index 66a8c1120a5..d2dbe002e3d 100644 --- a/packages/nodes-base/nodes/Todoist/Todoist.node.ts +++ b/packages/nodes-base/nodes/Todoist/Todoist.node.ts @@ -426,52 +426,54 @@ export class Todoist implements INodeType { 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++) { - const resource = this.getNodeParameter('resource', 0) as string; - const operation = this.getNodeParameter('operation', 0) as string; if (resource === 'task') { if (operation === 'create') { - //https://developer.todoist.com/rest/v1/#create-a-new-task - const content = this.getNodeParameter('content', i) as string; - const projectId = this.getNodeParameter('project', i) as number; - const labels = this.getNodeParameter('labels', i) as number[]; - const options = this.getNodeParameter('options', i) as IDataObject; + //https://developer.todoist.com/rest/v1/#create-a-new-task + const content = this.getNodeParameter('content', i) as string; + const projectId = this.getNodeParameter('project', i) as number; + const labels = this.getNodeParameter('labels', i) as number[]; + const options = this.getNodeParameter('options', i) as IDataObject; - const body: IBodyCreateTask = { - content, - project_id: projectId, - priority: (options.priority!) ? parseInt(options.priority as string, 10) : 1, - }; + const body: IBodyCreateTask = { + content, + project_id: projectId, + priority: (options.priority!) ? parseInt(options.priority as string, 10) : 1, + }; - if (options.dueDateTime) { - body.due_datetime = options.dueDateTime as string; - } + if (options.dueDateTime) { + body.due_datetime = options.dueDateTime as string; + } - if (options.dueString) { - body.due_string = options.dueString as string; - } + if (options.dueString) { + body.due_string = options.dueString as string; + } - if (labels !== undefined && labels.length !== 0) { - body.label_ids = labels; - } + if (labels !== undefined && labels.length !== 0) { + body.label_ids = labels; + } - responseData = await todoistApiRequest.call(this, 'POST', '/tasks', body); + responseData = await todoistApiRequest.call(this, 'POST', '/tasks', body); } if (operation === 'close') { - //https://developer.todoist.com/rest/v1/#close-a-task + //https://developer.todoist.com/rest/v1/#close-a-task const id = this.getNodeParameter('taskId', i) as string; - responseData = await todoistApiRequest.call(this, 'POST', `/tasks/${id}/close`); + responseData = await todoistApiRequest.call(this, 'POST', `/tasks/${id}/close`); responseData = { success: true }; } if (operation === 'delete') { - //https://developer.todoist.com/rest/v1/#delete-a-task + //https://developer.todoist.com/rest/v1/#delete-a-task const id = this.getNodeParameter('taskId', i) as string; - responseData = await todoistApiRequest.call(this, 'DELETE', `/tasks/${id}`); + responseData = await todoistApiRequest.call(this, 'DELETE', `/tasks/${id}`); responseData = { success: true }; @@ -480,7 +482,7 @@ export class Todoist implements INodeType { //https://developer.todoist.com/rest/v1/#get-an-active-task const id = this.getNodeParameter('taskId', i) as string; - responseData = await todoistApiRequest.call(this, 'GET', `/tasks/${id}`); + responseData = await todoistApiRequest.call(this, 'GET', `/tasks/${id}`); } if (operation === 'getAll') { //https://developer.todoist.com/rest/v1/#get-active-tasks @@ -513,7 +515,7 @@ export class Todoist implements INodeType { //https://developer.todoist.com/rest/v1/#get-an-active-task const id = this.getNodeParameter('taskId', i) as string; - responseData = await todoistApiRequest.call(this, 'POST', `/tasks/${id}/reopen`); + responseData = await todoistApiRequest.call(this, 'POST', `/tasks/${id}/reopen`); responseData = { success: true }; } From 2616b2b20013bd0e67b04edb06f9ccba75b3365c Mon Sep 17 00:00:00 2001 From: Tanay Pant <7481165+tanay1337@users.noreply.github.com> Date: Wed, 26 Aug 2020 09:58:38 +0200 Subject: [PATCH 29/39] Update BREAKING-CHANGES.md --- packages/cli/BREAKING-CHANGES.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cli/BREAKING-CHANGES.md b/packages/cli/BREAKING-CHANGES.md index 1354c7d73cc..9269427ae0b 100644 --- a/packages/cli/BREAKING-CHANGES.md +++ b/packages/cli/BREAKING-CHANGES.md @@ -6,11 +6,11 @@ This list shows all the versions which include breaking changes and how to upgra ### What changed? -We have renamed the operations on the Todoist Node to keep consistency with the codebase. Also, deleted the operations close_match and delete_match as these operations can be accomplished using the operations getAll, close, and delete. +We have renamed the operations in the Todoist Node for consistency with the codebase. We also deleted the `close_match` and `delete_match` operations as these can be accomplished using the following operations: `getAll`, `close`, and `delete`. ### When is action necessary? -When one of the following operations is used. +When one of the following operations is used: - close_by - close_match @@ -19,9 +19,9 @@ When one of the following operations is used. ### How to upgrade: -After upgrading open all workflows, which contain the Todoist Node, set the corresponding operation, and then save the workflow. +After upgrading, open all workflows which contain the Todoist Node. Set the corresponding operation, and then save the workflow. -If the operations close_match or delete_match are used, recreate them using the operations getAll, delete and close. +If the operations `close_match` or `delete_match` are used, recreate them using the operations: `getAll`, `delete`, and `close`. ## 0.69.0 From 4ef9826fffcc8b7753fbd9bf8be1260e54a9980b Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Wed, 26 Aug 2020 15:24:24 -0400 Subject: [PATCH 30/39] :bug: Coda bug fix (#891) --- packages/nodes-base/nodes/Coda/Coda.node.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/nodes-base/nodes/Coda/Coda.node.ts b/packages/nodes-base/nodes/Coda/Coda.node.ts index 7e175f5c7df..e7d8c9c8ddc 100644 --- a/packages/nodes-base/nodes/Coda/Coda.node.ts +++ b/packages/nodes-base/nodes/Coda/Coda.node.ts @@ -237,10 +237,6 @@ export class Coda implements INodeType { const options = this.getNodeParameter('options', i) as IDataObject; const endpoint = `/docs/${docId}/tables/${tableId}/rows`; - if (options.keyColumns) { - // @ts-ignore - items[i].json['keyColumns'] = options.keyColumns.split(',') as string[]; - } if (options.disableParsing) { qs.disableParsing = options.disableParsing as boolean; } @@ -264,6 +260,11 @@ export class Coda implements INodeType { }; } ((sendData[endpoint]! as IDataObject).rows! as IDataObject[]).push({ cells }); + + if (options.keyColumns) { + // @ts-ignore + (sendData[endpoint]! as IDataObject).keyColumns! = options.keyColumns.split(',') as string[]; + } } // Now that all data got collected make all the requests From f386d9e90af6ce9069e37be3b82d3dc3fe1f1eb3 Mon Sep 17 00:00:00 2001 From: maxtkacz Date: Wed, 26 Aug 2020 21:25:59 +0200 Subject: [PATCH 31/39] Made "easy wins" copy-edit pass through vue components (#889) Essentially scrubbed through .vue files in packages/editor-ui/src/components and copy edited labels/ tooltips etc. Tried to prevent opinionated editing, simply rewriting existing meaning for clarity. I did however make an opinionated decision on changing a node's "Notes" to "Note" (only labels, nothing code related) because it feels more like a singluar note - especially when used as a subtitle in the workflow. Singular form also futureproofs functionality like showing a list of all node notes across a workflow (or versioning/ collaborative notes features). So far, have gotten up to PageContentWrapper.vue (when sorted alphabetically). In a followup, will review ParameterInput.vue and onwards in /components folder --- .../src/components/CredentialsEdit.vue | 4 ++-- .../src/components/CredentialsInput.vue | 16 +++++++------- .../src/components/CredentialsList.vue | 4 ++-- .../editor-ui/src/components/MainHeader.vue | 16 +++++++------- .../src/components/NodeCreateList.vue | 2 +- .../editor-ui/src/components/NodeSettings.vue | 22 +++++++++---------- .../editor-ui/src/components/NodeWebhooks.vue | 2 +- packages/editor-ui/src/components/RunData.vue | 8 +++---- 8 files changed, 37 insertions(+), 37 deletions(-) diff --git a/packages/editor-ui/src/components/CredentialsEdit.vue b/packages/editor-ui/src/components/CredentialsEdit.vue index a69ad78e247..bc967e08e60 100644 --- a/packages/editor-ui/src/components/CredentialsEdit.vue +++ b/packages/editor-ui/src/components/CredentialsEdit.vue @@ -214,7 +214,7 @@ export default mixins( this.$showMessage({ title: 'Credentials created', - message: `The credential "${eventData.data.name}" got created!`, + message: `"${eventData.data.name}" credentials were successfully created!`, type: 'success', }); @@ -227,7 +227,7 @@ export default mixins( this.$showMessage({ title: 'Credentials updated', - message: `The credential "${eventData.data.name}" got updated!`, + message: `"${eventData.data.name}" credentials were successfully updated!`, type: 'success', }); diff --git a/packages/editor-ui/src/components/CredentialsInput.vue b/packages/editor-ui/src/components/CredentialsInput.vue index 3d8edec3ecf..27a4597331f 100644 --- a/packages/editor-ui/src/components/CredentialsInput.vue +++ b/packages/editor-ui/src/components/CredentialsInput.vue @@ -44,19 +44,19 @@ - Not all required credential properties are filled + Enter all required properties - Is connected + Connected - Is NOT connected + Not connected
@@ -91,7 +91,7 @@
- Important! + Important
Add at least one node which has access to the credentials!
@@ -163,8 +163,8 @@ export default mixins( isMinimized: true, helpTexts: { credentialsData: 'The credentials to set.', - credentialsName: 'The name the credentials should be saved as. Use a name
which makes it clear to what exactly they give access to.
For credentials of an Email account that could be the Email address itself.', - nodesWithAccess: 'The nodes which allowed to use this credentials.', + credentialsName: 'A recognizable label for the credentials. Descriptive names work
best here, so you can easily select it from a list later.', + nodesWithAccess: 'Nodes with access to these credentials.', }, credentialDataTemp: null as ICredentialsDecryptedResponse | null, nodesAccess: [] as string[], @@ -256,7 +256,7 @@ export default mixins( this.$showMessage({ title: 'Copied', - message: `The callback URL got copied!`, + message: `Callback URL was successfully copied!`, type: 'success', }); }, @@ -401,7 +401,7 @@ export default mixins( this.$showMessage({ title: 'Connected', - message: 'Got connected!', + message: 'Connected successfully!', type: 'success', }); } diff --git a/packages/editor-ui/src/components/CredentialsList.vue b/packages/editor-ui/src/components/CredentialsList.vue index 758adf1a4c5..061bfa7d1e7 100644 --- a/packages/editor-ui/src/components/CredentialsList.vue +++ b/packages/editor-ui/src/components/CredentialsList.vue @@ -124,7 +124,7 @@ export default mixins( try { this.credentials = JSON.parse(JSON.stringify(this.$store.getters.allCredentials)); } catch (error) { - this.$showError(error, 'Proble loading credentials', 'There was a problem loading the credentials:'); + this.$showError(error, 'Problem loading credentials', 'There was a problem loading the credentials:'); this.isDataLoading = false; return; } @@ -138,7 +138,7 @@ export default mixins( }, async deleteCredential (credential: ICredentialsResponse) { - const deleteConfirmed = await this.confirmMessage(`Are you sure that you want to delete the credentials "${credential.name}"?`, 'Delete Credentials?', 'warning', 'Yes, delete!'); + const deleteConfirmed = await this.confirmMessage(`Are you sure you want to delete "${credential.name}" credentials?`, 'Delete Credentials?', 'warning', 'Yes, delete!'); if (deleteConfirmed === false) { return; diff --git a/packages/editor-ui/src/components/MainHeader.vue b/packages/editor-ui/src/components/MainHeader.vue index a6d5710156f..2b9696e2b34 100644 --- a/packages/editor-ui/src/components/MainHeader.vue +++ b/packages/editor-ui/src/components/MainHeader.vue @@ -12,14 +12,15 @@ - of Workflow + of "{{workflowName}}" + workflow Workflow: {{workflowName}} - Workflow not saved! + Workflow was not saved! @@ -32,9 +33,9 @@
- Server connection could not be established.
- The server is down or there is a connection problem.
- It will reconnect automatically as soon as the backend can be reached. + Cannot connect to server.
+ It is either down or you have a connection issue.
+ It should reconnect automatically once the issue is resolved.
  @@ -50,9 +51,8 @@
- A past execution gets displayed. For that reason no data
- can be changed. To make changes or to execute it again open
- the workflow by clicking on it`s name on the left. + You're viewing the log of a previous execution. You cannot
+ make changes since this execution already occured. Make changes
to this workflow by clicking on it`s name on the left.
diff --git a/packages/editor-ui/src/components/NodeCreateList.vue b/packages/editor-ui/src/components/NodeCreateList.vue index a521c40ddd9..3d318c2c8dc 100644 --- a/packages/editor-ui/src/components/NodeCreateList.vue +++ b/packages/editor-ui/src/components/NodeCreateList.vue @@ -13,7 +13,7 @@
- No node found which matches active filter! + 🙃 no nodes matching your search criteria
diff --git a/packages/editor-ui/src/components/NodeSettings.vue b/packages/editor-ui/src/components/NodeSettings.vue index ea5f37ea150..6808b825665 100644 --- a/packages/editor-ui/src/components/NodeSettings.vue +++ b/packages/editor-ui/src/components/NodeSettings.vue @@ -5,7 +5,7 @@ -
+
@@ -22,7 +22,7 @@
- The node does not have any parameters. + This node does not have any parameters.
@@ -162,15 +162,15 @@ export default mixins( }, default: '', noDataExpression: true, - description: 'Notes to save with the node.', + description: 'Optional note to save with the node.', }, { - displayName: 'Notes In Flow', + displayName: 'Display note in flow?', name: 'notesInFlow', type: 'boolean', default: false, noDataExpression: true, - description: 'If activated it will display the above notes in the flow as subtitle.', + description: 'If active, the note above will display in the flow as a subtitle.', }, { displayName: 'Node Color', @@ -186,7 +186,7 @@ export default mixins( type: 'boolean', default: false, noDataExpression: true, - description: 'If activated and the node does not have any data for the first output,
it returns an empty item anyway. Be careful setting this on
IF-Nodes as it could easily cause an infinite loop.', + description: 'If active, the node will return an empty item even if the
node returns no data during an initial execution. Be careful setting
this on IF-Nodes as it could cause an infinite loop.', }, { displayName: 'Execute Once', @@ -194,7 +194,7 @@ export default mixins( type: 'boolean', default: false, noDataExpression: true, - description: 'Instead of executing once per item does it only execute once with the data of the first item.', + description: 'If active, the node executes only once, with data
from the first item it recieves. ', }, { displayName: 'Retry On Fail', @@ -202,7 +202,7 @@ export default mixins( type: 'boolean', default: false, noDataExpression: true, - description: 'If activated it will automatically retry the node again multiple times.', + description: 'If active, the node tries to execute a failed attempt
multiple times until it succeeds.', }, { displayName: 'Max. Tries', @@ -221,7 +221,7 @@ export default mixins( }, }, noDataExpression: true, - description: 'How often it should try to execute the node before it should fail.', + description: 'Number of times Retry On Fail should attempt to execute the node
before stopping and returning the execution as failed.', }, { displayName: 'Wait Between Tries', @@ -240,7 +240,7 @@ export default mixins( }, }, noDataExpression: true, - description: 'How long to wait between ties. Value in ms.', + description: 'How long to wait between each attempt. Value in ms.', }, { displayName: 'Continue On Fail', @@ -248,7 +248,7 @@ export default mixins( type: 'boolean', default: false, noDataExpression: true, - description: 'If activated and the node fails the workflow will simply continue running.
It will then simply pass through the input data so the workflow has
to be set up to handle the case that different data gets returned.', + description: 'If active, the workflow continues even if this node\'s
previous nodes - so your workflow should account for unexpected output data.', }, ] as INodeProperties[], diff --git a/packages/editor-ui/src/components/NodeWebhooks.vue b/packages/editor-ui/src/components/NodeWebhooks.vue index e2875a49fd6..051e97b7b57 100644 --- a/packages/editor-ui/src/components/NodeWebhooks.vue +++ b/packages/editor-ui/src/components/NodeWebhooks.vue @@ -88,7 +88,7 @@ export default mixins( this.$showMessage({ title: 'Copied', - message: `The webhook URL got copied!`, + message: `The webhook URL was successfully copied!`, type: 'success', }); }, diff --git a/packages/editor-ui/src/components/RunData.vue b/packages/editor-ui/src/components/RunData.vue index 6a0a138669c..b08f52e3dda 100644 --- a/packages/editor-ui/src/components/RunData.vue +++ b/packages/editor-ui/src/components/RunData.vue @@ -7,7 +7,7 @@ :disabled="workflowRunning" @click.stop="runWorkflow(node.name)" class="execute-node-button" - :title="`Executes node ${node.name} and all not already executed nodes before it.`" + :title="`Executes this ${node.name} node after executing any previous nodes that have not yet returned data`" >
@@ -72,14 +72,14 @@

- Node contains large amount of data + Node returned a large amount of data

The node contains {{parseInt(dataSize/1024).toLocaleString()}} KB of data.
Displaying it could cause problems!

- If you decide to display it anyway avoid the JSON view! + If you do decide to display it, avoid the JSON view!
@@ -162,7 +162,7 @@
No data

- To display data execute the node first by pressing the execute button above. + Data returned by this node will display here
From 317949d765571bfc94769b053aaee1d035d9b0dc Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 26 Aug 2020 21:30:24 +0200 Subject: [PATCH 32/39] :zap: Remove left behind debug code --- packages/editor-ui/src/components/mixins/pushConnection.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/editor-ui/src/components/mixins/pushConnection.ts b/packages/editor-ui/src/components/mixins/pushConnection.ts index ebda8f837ae..9992809b2ac 100644 --- a/packages/editor-ui/src/components/mixins/pushConnection.ts +++ b/packages/editor-ui/src/components/mixins/pushConnection.ts @@ -202,10 +202,6 @@ export const pushConnection = mixins( const runDataExecuted = pushData.data; - console.log('..pushData..'); - console.log(pushData); - - // @ts-ignore const workflow = this.getWorkflow(); if (runDataExecuted.finished !== true) { From 520933c7a40237a18ab00c1ca446cc6ceaba664e Mon Sep 17 00:00:00 2001 From: maxtkacz Date: Wed, 26 Aug 2020 21:31:08 +0200 Subject: [PATCH 33/39] :zap: Update CredentialsEdit.vue (#888) Changed width of Credentials modal from 55% to 75%. Result is the "Nodes with Access" UI now only collapses in a UX-unfriendly manner on viewport widths under 1108px (previously was 1506px) --- packages/editor-ui/src/components/CredentialsEdit.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor-ui/src/components/CredentialsEdit.vue b/packages/editor-ui/src/components/CredentialsEdit.vue index bc967e08e60..5497ab914eb 100644 --- a/packages/editor-ui/src/components/CredentialsEdit.vue +++ b/packages/editor-ui/src/components/CredentialsEdit.vue @@ -1,6 +1,6 @@