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 000000000..1685c11a2 --- /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 000000000..71f58a417 --- /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 000000000..5b1c6c227 --- /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 000000000..8966e60ef --- /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 000000000..3e266d18a --- /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 000000000..525aed043 --- /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 000000000..1b82b741f --- /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 000000000..a1a718ba3 --- /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 000000000..ce620fcdb --- /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 000000000..c59bab144 --- /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 000000000..18ccba825 --- /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 000000000..0a2ebfd1e --- /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 000000000..866cddb26 --- /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 000000000..781bcc6fe --- /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 000000000..87e7f6b93 --- /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 000000000..7557cbb75 --- /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 000000000..2baabee09 --- /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 37b9adecf..45b703b9d 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 ce620fcdb..d9078f0cc 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 c59bab144..246c43c22 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 866cddb26..e16fb51f2 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 e16fb51f2..930534e5f 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 d9078f0cc..c8db4ada7 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 000000000..d08b97e64 --- /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 c8db4ada7..000000000 --- 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 18ccba825..dfd523806 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 0a2ebfd1e..6b2fa4eb3 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 930534e5f..820e8b07a 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 781bcc6fe..c22e3ba0d 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 246c43c22..218a85cc7 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 87e7f6b93..45f2872ad 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 2baabee09..035f3c6e5 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 6542d3359..000000000 --- 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 45b703b9d..fed1f1360 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 71f58a417..8c9870639 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 8966e60ef..a8541c649 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 3e266d18a..f6e1a6f3e 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 525aed043..4844045e3 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 1b82b741f..493b713cd 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 a1a718ba3..b0d18adcf 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 000000000..f625dfed8 --- /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 5b1c6c227..cb5bdbeb2 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 4844045e3..81c8e43b3 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 493b713cd..5f1186cd5 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 1685c11a2..d7e869755 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 cb5bdbeb2..0332facf7 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 f6e1a6f3e..a8496805b 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 5f1186cd5..42437cc15 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 0332facf7..580bab7d5 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 60d2be5d7..015f3d30c 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 458a97483..c6cb23890 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 0896adac6..0150479ff 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 f3c662d57..ab8c4b56e 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 b67c5cc52..b7cbd5417 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 25790c01e..acdee495e 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 4306b2ed1..9b473a9a6 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 b089d689c..d9b0eead4 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 000000000..1ce873f16 --- /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 6b5158e6d..0352e3e2d 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 be11ba6f1..a8ecb03e6 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 831d3f3b4..b7c65c242 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 2b938ae5d..3aaa91809 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 b6b456563..105f84114 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 527c94bfb..66ba233dd 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 9b473a9a6..1fa0bc58d 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 d9b0eead4..9e75de40c 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 1ce873f16..6c4f38fcc 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 0352e3e2d..9a9661023 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 66ba233dd..c39b51a6c 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 015f3d30c..7cd767059 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 c6cb23890..cf53fdd3b 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 000000000..143a24b3b --- /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 940157e5f..9d61c4339 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 000000000..69ffc4e6e --- /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 000000000..caecdddee --- /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 000000000..367082a4b --- /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 000000000..417976230 --- /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 000000000..2d0ddf789 --- /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 000000000..912692ca9 --- /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 000000000..9a627a86e --- /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 000000000..d45cc2ddb --- /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 1ae8d4cee..691cca25e 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 69ffc4e6e..179ec825b 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 caecdddee..243dc56ff 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 367082a4b..d4afd8f7c 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 417976230..94e1246f4 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 2d0ddf789..b2020d4f5 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 912692ca9..152db9a7f 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 d45cc2ddb..50b1a9295 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 d08b97e64..8ce49fb14 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 820e8b07a..7edd88049 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 c22e3ba0d..24fbfbed2 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 45f2872ad..5f06237af 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 7557cbb75..9e89c340e 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 035f3c6e5..41c77ac6d 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 03f0dd80b..d3a8e90e5 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 f92234eb0..7c706a0fc 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 aa976970c..0703d23a1 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 0703d23a1..920c0d41f 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 41dedd25a..ca84937cb 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 1a40ce261..ae4aac9c4 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 baea830b0..85455c1f3 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 9592ecb4d..9383363b8 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 d9e77d929..bfc731820 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 000000000..eb931b244 --- /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 a32814f5b..5b064a308 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 37784f7e8..0caf2905c 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 10b789685..b16d4933a 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 881e938b6..2f3973b84 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 ae4aac9c4..a6d571015 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 85455c1f3..a54b53058 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 9383363b8..abedea5b4 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 bfc731820..ebda8f837 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 eb931b244..0f7a0bd78 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 5b064a308..08ac66a11 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 0caf2905c..37784f7e8 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 b16d4933a..dd4c75655 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 271c10259..5b4757abc 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 421d9e4b3..c8d1a8b37 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 8c9870639..7933379ad 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 580bab7d5..84fa78c93 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 81c8e43b3..2f034f689 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 68be5ce94..c61a00fc5 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 000000000..9341e69c6 --- /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 f4fdfad5e..d8d776768 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 fc70424c9..66a8c1120 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 zcmY*-bzD?!*Yz;cEhP*!3`0tnGy@DUq(ew|4>^P|A|cWsB8?*5ol190DIwh;2uO&4 zN`3G=_jkYVd!66@?X~w=d#!7qKTi}=ONE3GLj?& zc97SU2LNi~h%T*gZflI4p{l*6CV=}E;{$N8C;<0v5!USkzybm87`F(ZhDHA`*1_WZ zhXVuvB2WPAe>lds`R*WY<8J1^8JG?Hmm!pW?|<+ebx1-Dbejm!2qP~5fQa%gumJDg z)7~l=q4W&B4K+0+ZQNb?tZdz_?f5XR=({O^G)D3ky4rbLfibQwZeEfY8OVPalDGJ- z%?|+&vxm1tlaT_yvUcg@kx-8N6Q4+`O$Yyl!5s{}uWFI&eEL8&4G48|Cf>zSFg` zc7N(E1A*Kb{d@jB|aA z|A+a1djI%H^WT~Ougm;*)BkjDSCu7{=KuGz$r5UGa~J^tltZd;c|Av*He)=0y(eGg z&fB<9iB6m>Rt2*#Bt`UkQ4*5+4P-EPl`#5Jf6m?4wVY%jx|zFqUk6*Ri|;oJ;el)~0^b4~lYa9_Bv1q_TEROIw9w zL1?~_eOj#czr1)tyvOQv_NKz-UB}qAlb&PVbEu7WxQaHp zkLWWV(BqXplI!JyA^u3l-i_Y>D-&JC zJ~BZ|yxeD+tPTv+ z<n`mb~weN>>H!8fz^dz8;rGwXg zEG-yn9jWTwrC(M_`IG)|(WrBG*wKK^I=SzMQ>2f5GjATd<+aP}D`A>3R@8=Ufdir9 zCeOZ~=6l7sIxx$=tmsa#zU$YmWE;pHfuFIXr@uj}p~o!QcmK?tDHVNs_HldNO{LG& z)+Y`e`2#!e^xciwQTpS*H1SC50hJOIY5k;t;FfwBWAzv+S0uJmIjfqRx}HfeD%za2 zZwswlwZLk7b4?nbnCBk~ijSd@I5FVO-~sJEAY57__MD@Vjiin$u?=i~n{a-eX#M<5 zf8#n=ft&HYR<0{@yXnD4P#((oXMX@;`-=nox^}l%CPraepY?K#FW2-^&?D598?fMX zb`y6((x>2~dk`@PF1%T5%e_@0cKE^l`W!_@^aPQ08dM1O!y>APyFL*#SSj81{ zO+Cu*GnD*hc((XvZ)|Rge+{V&&9<=mmtQkbGPl0Y3Qc?a`#VRBaA`pz*9XvU30OF) z-r^L>b$n4*k(7{HMlK9kDM=LTj_+!F5h3?D5o^C`{_5p^IqtHzOXk=*;==j?d=;wv ziQepNH)hl1OeE@6W+%|jvGNb^x(A>&r$vW~A``2}hDg)$AkvPRp6t)Y=ts-R2}rir z({4nGcuv<+SaaH^qN83)d;fGSfz1B3HoGP{OKrsp-iR1jCshdI%;ljatM5EELt!X;&fmXD*u!M2^KT7l-~sPAz_p z(dp`B5*9cWbLso=o8Vp!YxZyThys4c7T3eI!6xaOZQhsQ=lC&3uv)ZbRXcQ~Di^$< zEDrl+6B6kfH6t(sBo?P&AT1?KV}=N%ds|~1&h7TvPprySiDF`&H`M6bV567NVp3RX zAPsDw82Yyeo4IoL3aQVJWcv#6F&TEu#Rh~Ac1!*g zRf=s3(OEIl{Ha$-!ZZ#Z=&>Tr5_p5uT?b0pUp02scDv6)!<2j6M_$;eTd<<&GC1?# zIurR^n`-0WnWUoZ1c!hJ_RTde#N@H7J#q5r;u4eC(}!h~QlJSK=S)I6tP$n9tWqJj zq~-dcqX;<@*p#7Fqjb28%jTvRaG&f4MXHTxR^?*R8FnQ3v6@huIrl-LW7d7qWN3nM zi6ZxR<`&LMGUk-(`UGpqC9&9FHHw3aE%Ehk_zBbrg$kYgq_U|Wgf|XnqTLvONpt?J z+nDrF&JTLX<#A>)IPxcav8SJNfHr+vy(ukMY5+Oc)?q(qGp3$&-Ii7Vm2S4^o5`TH zqQ5{!oTb&}Q5(;}?9fC%@z+7e{Q4)B%+{8Vm}LpJZ<0Z&?HYPd5Q1?XN<+SyeSAHN z7gu_o1CCJhOxUgL3Sy7*cF zl@U&#h_mTAOv~+`6@FkwAB!ZFhYJ)%!aG+Aldy(~7xSj2kE=oIw4(2XoY*A7H8@rr z4s`?Z7%GCEh1aV`8S|T{x<263J#h-bOAGqpnXr;O%vkZ)#1B?J>-^>MiJN&2Da&C$ ziIaKyBO2|!xS7PS5-}9^GAiHYfF7Pg3GaZSRrYJ96d6(T<@1$i!!nLgozH10=wjK% zOnsndYMA2l#7K3B*R;J;Mv&TfxpAa}WPK&38|@-pPqx9e!jPTjHp2;MXAa7qxXwVd z@IV-JSl95LA1N|W)YKw3qBivW{!ni=J_Xk@J^B#1$6D%t?&{>faXxF9+i0*HjHRXj zsdM|e;7fw(-$-XB^97ratXGIB`@W$aMz(yV5IYyot)FBiZeO7k2J$x~a^EvrbkhI0 zM~GJJ_c;tTTt206eN%oBbj+!Y=24FBmVf(vG+K9QDX*}_c=THMrvjYi^F{59d2as7 zV#pOjTklzeOMa=mqUtsvm#N$gb|UVrHd}1~E;tNm(&`r!mdq>{UEs3Xkc?)Q=FyOF z|Bd?1>DsFaICvtv4_vL|U#qVrL=`Y1Rxej$WW7KZNA7DjcM2LPtY_G)ZfabPW$lQx@#jj|A9!4Pe;8z?i0ox5wvaQOUcWy9ZBi z^eC)xX+tIJ6{>~A4w<7Xo{jbi3LJjv6YYt`*JD{la74b47m@E~U|KbtZ4qjNVoDwXOj&kNmbWLZx=eHC=={3k z?5BN+<5BGQ$9SdW*s||yM16;>7(SSVa9H7M?F5hl6?-&S-s9>^e?nI%;vyQ=9pk&Y4!S6DoT?G;Uy7bUDDn<{ zf?T9$K#d1oU!r|kT?)R6NMvUX85zm@#Kkh09JeBI(mE6?qOiAqZZ)R~bn!5J{!x)s zoK~_G7!tFzO{%1TH1?~TS`tTT%g5tHAiia=oYy_p>E3J^3P44$KP;W`S6+_KQ>7Q} ztaK5fBe^-svESf3r{lMG8NHx%Cg#A;(Ow{UiFHsce*M(=`iUMtDbssD_D><75|~Ek z&Tt+P0b`=W=ChGqIx4ENZv7!iAIo4$S|xF#q{3EyW%t$mVK|KB^GihR;pbtetoSm% zUaWRP)(n4n9M$9_^hwz)$L25uc#3?%NE1*$Aaum!OGLF0?1NMT(P7#6dhd$-yk!zz zS|={HtcD9`P3KwJ?3eElBmY;M?G&DJqA}}-@07I}?_UU3TWUsi(33Lf?%o)|`khZy z>Cq%nUw`D7i_ZqkVxC+ygTqAEPQ7Up*mx?`TU;+**C^_gCNrxqF-{h4vn@ZZqDh`h zY=DvV!8KkS7BQFn?eQHvpPhtOhF`Q2pFZAc{1EsEe^<*R2oo+H5?-9G=?S5GB{uR7 zR619m>6G;j_EX@67K7^798oBls;6AUPJT|XKYy!&@wubkM)QJ000Qyq_o8NZERlG) zWNL}8TXJ0kyn4;F!9yvt$V;uES^fE+t*%qPGkL){-hxnV8${f|nj^i04^Un&Kt|O6 zc_@SG(57;8S4GrR93$J|f_uVE!HnwON(S7MUdRAK*;DDq^hfxa+io0)P6rWAfLUx! ze}tsAmL8~><0Q{AXq&oR?SZ2Vsz?kMgg+h}@`FFn8JjKwiU02T1Z|a?3#0-Mud5it zK)7%BeB&>2-cre(%04aXfk6l!?d=xh#ykJ;63#X*$X}5K=1emsG!;;ZZqr3)iA%UV zh;K2yImP`62AX zcWBaBBi*2DnVer7==umhik^?7V!EnxihZK8C44cDBhkndas413cL|N7qw}gFe9C=C zIn#(=$MnW!ViB><>XUGSI^2DtcRry94P^grq=*iM&~Nc1_6!i|aw(bg_8`zZW89N} zkyNkfjdPe+lBmQTjlQVUI2|{R=;M4O&{9E0ry8O^FUm zXLP;q{pdm@+k8w?or(Oz#Tnu-Jt(g;{ctvLai#dxtj7wKTH|2{S7AiPz3@-9#N0?T zC1X8ruacRs3BEtJ-~81`V582rhy$K}PlWmgs%uxZ!U&MU1})x7n)!bE6{cFE8*XHo ztWRhgTvarsZ+h%|>t7QNA4+k*=hI>wfjxU`e)XM0?`B|y!bJBZq3O4lMV zYdvBGxbfH#*u1uL|0kcBMtiCSM*u#>b{d8dEcorcnA^8Jfded`zDM08c-&*KQQ5ra zh{Cmz;P9s%Of=EW<6#Rbk+}DQODWH#7vOyt!{69q8Lj}&)|-%|GXB$ek{sH$)L!@M>HDjm&h?TBW~ z=z2$D&8jO!fOfunKuJJYpFys?Zv~Cn?~vG~m$5t3_N%Je-s78Pn?rdK<(d~FS7S9P z+rbg_jmw`M5F82QLGrRsHlS|)=oE!Mq<4O2=G(f#dmHwtAR1sl@JX^>oe#4E|h;aTdc>OH(Zyl!D>m{?YVEkV}Q8Qh;tF^ z;87%(9-=%3?ST*pCdvtTX}r|6h|(aDr@Ur4cf?g=G`CC)>5n-V8SOMfMg;g4dwhB& z@~6Lkv`I?Qx35)p1pfx9y*cS4`&wt}lWnREvBLJ}EV#(q;gcIV(55ZR1WCQ8gjr*V zMJVL+D}|vkDjLOQz}rc_ka-60g}wQ3t-UOcfMv{$X2{PGcGg`baIrZ4rWYwp=v=xWCHXstGeJX6X;jC}tW zwj1t&TLWsvp!Hr3aIneJQ~-;G-WsN)SX&8?T8)b&80eYoB3fOie~@wX9aCzyfpU>4 z{ynF$p00>E&FT<5K%;;`+nt_o{F4pByU36a(iS|NmsN4ITeSrqZ?9}Szj$`639%SE z#@N~}m1GB+9p=-^!DQ7 z;?U60&d$#J`uO_)|K~(9;6*aY$;tot|Lx%7|2i%E{QLg@`u{{Y?LsR5@9^E)-`+ww z@kKD!;NR8V*2TretP~0C_4D@V>i_)d|K{Z9+uYCA&;RxG;_d0(-sb-9E)>GadQw^7#Dl=3DI6KlqHpnz4t{EHu{qybP?C9<2_2Jgo)Y8Yx!rX^^*GoG4LoWF@DEvMh{_Fef z_4w}Y_vi5S?C0^==H=h%&HmcL|D>1FPAtkW71izE&ehoD*Vo|A z)$i5O$kEaN$j%)r6H^`4o^fpYUZH|#w#%OoxCIVh|u9`^YD^X~lU|Mukb@b2C1 z^7iKb+~(-j;Q8d<+0)wC<<{?%n(UH_{7yvjH80yhEz?CO-a;SpKO4;+8OaS9@%H@Y z-sbG>;mX_C|Jc>X$I<@d&il8p{+O!rqL9*-hW|rN<55G+JVD`JK;=q3=Q%p{RXEu? zBI-pS-cBCFAQ-wO6r2+dyA2D-1_kQ=?%MP0($wVt;@8s9$iTU||FfjuesRZWX4FAe zw=yvBDJ%ajD91(|;wl%`G!feU_1)9s^xfe9&E40{+1J*{%)i6i$-?fYq}!pL;hU8H zafj%EgY{xmx>HK;O-Z^JB-2$N-4h@E>a)vi5vcXxT=?w@MJc^tCi~25jkE+_`1TixKA+L+QoE0Rj0lJiWYp z&KqY0_}nr9AP_Q|Bxwf2LASnQx!|2r>77fciMH$!S!}Gj#@LA&Ieyy#3&8ay# zsi`e_c?*5pn%eRZ4o6Xz;SfNgfYxBBSgkh9@EP){Fzc7Yw_4I${PH#pZnESxr8g~F z)cAt_h50$=dFjnLesVL#&}yZeV;KyBHKVztQhF&(-z{~AC%Z_qA|nSiBW`XZnptjb zZB4Tp7Y*rOl9qG4jZ~c$RV~r1S|urQIbo-wz^59F_IXJ%bo+EmW}3H=VL(=6bjFOM zMsHkZV;pi1k2BUpo z+IT)Zzq-6S3RmPh=dM^0QB&R-TYf$Q!c=yINMZ;K6%20_bl3}3xLG3GEs+Ly16<49 zU~u=cPH70oRyya##B9kAFc{Jd3@%_4DquJ#!zkda2Gx2m1EWakTCjCtrMIvgNw$&9 ztt6peo`aZMUubSk*US>5x=l-8eRcEvDvHGHMtKehHBr(9$qRSuN*YOt!Hkw{ zzd_CRO?l~>PqR}8vn_f*zw8kWZPs@;4lSC0Hi@9fQ6A%jIDQlX&UT7!=_Q%MvS1{| zIl%o^q{Y{5mS!;0%|?^1fQ0N%yH2mXbTYKAjds{eGUz?PQpSHENUQgQ=uXdccxC5iDTT(`p{r3|cRtGsI zWF`dZ?4P=w3jj{bSp*gX8U?<1n!&vyr=p@F)}v?I)%ULy7A3Af7BY5ijkfRlfsDtG zuh~0RW$Kmmy&I8a1AH$zXzH!rI2syg?1>6|Ntj&tklGsg~x z-25=)sqwqJ4&87#xG&VHe$1-*iF_0Va+-t%fEHOmcDPFyG+SHb9n!6B7hU(RTemi& zxOh#%`eR2w&iZV)(ZA>Z$H9*Kv~zVOS_rPhP|}6Ra8k+JAVA4`DK(!ybH*}HWjc3h zeD62M*3}jk&8e$PJpN#(k0guU^Ht*Nq97|_!L}!-tq>G|A0;W8zrX|FrbJ}3&YWp$ zNq38>ntHYHn{PtamDFn2oZ5doJn3{n;N=hcLPFz%oaP1xY3-kUA{Qeml>`k|!XyO? z+eNI>Z7@=Sv=78Y-+u69f_ zQv@NP&s&=|8{B7vyDtrQe&&j@N#!ue(>!g!IP;JRv+cj43 zDg#0il1h#N5{R&dxkQmJdK&)=K4vdd&l@wj;C)Z=$|ZtK0iUR$!y zAu~AMYBos&ScO!ioKV9WHYzRk@^O(2L;7lC>>oRaqdoj*x=WI0;Qg#q4|~r^(LL`S z$v7CQb<))a+h#$+a3!x&qqJOQ$AaV+!YStkUB2=T)6kOJUw;-@;9-|!9DZx#snAsg zX@-J!<6wTpX19UYUK?P;hn_R-x-#ziZlGz&uFz9i z@AgD!q~Voi<4#WTrTd)f6C8EWYnm@ds)@f`qNj%v$9o2N1|0#H88Tvv8Snink&8C23^9Sne74 z-j}Yf`+EyKJPLMRF59^G?ujFZ*Oml1WhTU1HH!o*SK85(Y?838EhtEv;lAVtz0ET} z{rXx&bad}~Z|{g$kiBjCglp*AJq0~|M_`6o3DZHsVi1H@69V`*RHrSQQqM|p?*86M zUHLB;_C{!u;E;yjxpr!NAOu>HT%3@s9DjkhycpUS6gUIEG_r3dczR z!Q=~iKgb`GmxKjA@#QP~-wy0OoaOo;Fxn$}NACw^2Qxm(czEdjjD*bk;#XFBG5|*a zif|+$px|}qisX)YmUhxSZ#(I8?&4>!WZk%t6}CIt-((7W@5J?zhZzaM+CzuN{sVJ3 zqg2BJO7Jc+@JYndrTUQt1J2U+Y#e_l&?6%G`MYJI2OjO~PdvQ-NJg+W z@#u!SvT0Q~h4D5d%gS7M8oYkq)YzfvI^(-t|7_!hH+P&aU-HWv$r_F4uCk4*wbvh& z9(MXTBt)l8Tz_;!=ESrr1uIkW1Wo}Nje`{tOJ8km8yJYpdA8O4_aCSI91Rmo|LPkL zPUsHbTn}N14oBA?Nt^`N9e=q1Wr08=6wabBS}W2Fi{=le#KZ{ZsF`~nOnk7rz{B*$ z-B+e)k3Bl}aow8u;4!BRZBdbSt%H^A07?WfH8BN)k{l?nPF{32+k4C68au-C30Hf2 z*8^|A<9cmxae_86Va}R4g+&>`2ZIU=9S^Sk=>4mA!T}~%s%a8|&7A@lc(dQU?Z(*7 z@|uVpm&cyIWJlm*YswgEE7zw}m zXE$N_9yOXxGL&>U1}q60Y7c5tKm4Y4+a( zjW?N-+U%Df@S?Hnb^qyE5c2-I>yIAwXV%w`>2x}+LuNz61_uWxr<)%(*t0s+XvT_KNWcH zf8Z4F&5h5_vs8*iQ#$3j`-7*RDh++|@#81^!}@RDxX~XLR@x6II3~jSPxb~1|13y*?VSBzxVm{*;A)qg45Kg@Uv>?u;#n}5q8?! zXVJ6sW-V-fcK+EW-xodJEbxD`+8yu+PH{`EGN(+0C4wW41$R?1DhKLi-DYgzapFf0E;jR90w@H zkvNJY3Y=pp7A096V*te|P@G~B79fCSP#i}YR)H%xhJtq-aj z5gH~51vrXjNmRiA7DeGRhEh-{hO?xCWEl!&S@;eK5Q@X0$WaVSF&tzKXBZ9$Ff>Y7 zSVXw0Lc~anVinL27Dk0aXsCpw<+R8F5feC0fD8f=W*CA1*r)+5BUrT!j5G9yB4GGY zR368xC_yQ4oTP-@%T*FBQnZ~CL9D$(gpz=fLAQxcQg(qnkbQa^`VvsOV zghv=vT!6 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index acbba3598..e3e597819 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 c61a00fc5..1354c7d73 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 d8d776768..31d8d85f9 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 66a8c1120..d2dbe002e 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 1354c7d73..9269427ae 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 7e175f5c7..e7d8c9c8d 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 a69ad78e2..bc967e08e 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 3d8edec3e..27a459733 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 758adf1a4..061bfa7d1 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 a6d571015..2b9696e2b 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 a521c40dd..3d318c2c8 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 ea5f37ea1..6808b8256 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 e2875a49f..051e97b7b 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 6a0a13866..b08f52e3d 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 ebda8f837..9992809b2 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 bc967e08e..5497ab914 100644 --- a/packages/editor-ui/src/components/CredentialsEdit.vue +++ b/packages/editor-ui/src/components/CredentialsEdit.vue @@ -1,6 +1,6 @@