From 96cffdfd1e0b6eb64fb94b6ac405c1a1d42d7cc7 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sat, 7 Nov 2020 13:44:13 -0500 Subject: [PATCH 01/42] :bug: Fix error when checking if webhook exists (#1129) * :bug: Fix error when checking if webhook exists * zap: Small fix --- .../nodes/Shopify/GenericFunctions.ts | 3 +- .../nodes/Shopify/ShopifyTrigger.node.ts | 163 +++++++++--------- 2 files changed, 79 insertions(+), 87 deletions(-) diff --git a/packages/nodes-base/nodes/Shopify/GenericFunctions.ts b/packages/nodes-base/nodes/Shopify/GenericFunctions.ts index e808846a25..6a7205e860 100644 --- a/packages/nodes-base/nodes/Shopify/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Shopify/GenericFunctions.ts @@ -24,7 +24,7 @@ export async function shopifyApiRequest(this: IHookFunctions | IExecuteFunctions throw new Error('No credentials got returned!'); } const headerWithAuthentication = Object.assign({}, - { Authorization: ` Basic ${Buffer.from(`${credentials.apiKey}:${credentials.password}`).toString(BINARY_ENCODING)}` }); + { Authorization: `Basic ${Buffer.from(`${credentials.apiKey}:${credentials.password}`).toString(BINARY_ENCODING)}` }); const options: OptionsWithUri = { headers: headerWithAuthentication, @@ -47,6 +47,7 @@ export async function shopifyApiRequest(this: IHookFunctions | IExecuteFunctions try { return await this.helpers.request!(options); } catch (error) { + console.log(error.response.body); if (error.response.body && error.response.body.errors) { let message = ''; if (typeof error.response.body.errors === 'object') { diff --git a/packages/nodes-base/nodes/Shopify/ShopifyTrigger.node.ts b/packages/nodes-base/nodes/Shopify/ShopifyTrigger.node.ts index f0d627d500..e6c49d4cf0 100644 --- a/packages/nodes-base/nodes/Shopify/ShopifyTrigger.node.ts +++ b/packages/nodes-base/nodes/Shopify/ShopifyTrigger.node.ts @@ -25,6 +25,7 @@ export class ShopifyTrigger implements INodeType { icon: 'file:shopify.png', group: ['trigger'], version: 1, + subtitle: '={{$parameter["event"]}}', description: 'Handle Shopify events via webhooks', defaults: { name: 'Shopify Trigger', @@ -55,271 +56,268 @@ export class ShopifyTrigger implements INodeType { options: [ { - name: 'App uninstalled', + name: 'App Uninstalled', value: 'app/uninstalled', }, { - name: 'Carts create', + name: 'Cart Created', value: 'carts/create', }, { - name: 'Carts update', + name: 'Cart Updated', value: 'carts/update', }, { - name: 'Checkouts create', + name: 'Checkout Created', value: 'checkouts/create', }, { - name: 'Checkouts delete', + name: 'Checkout Delete', value: 'checkouts/delete', }, { - name: 'Checkouts update', + name: 'Checkout Update', value: 'checkouts/update', }, { - name: 'Collection listings add', + name: 'Collection Listings Added', value: 'collection_listings/add', }, { - name: 'Collection listings remove', + name: 'Collection Listings Removed', value: 'collection_listings/remove', }, { - name: 'Collection listings update', + name: 'Collection Listings Updated', value: 'collection_listings/update', }, { - name: 'Collections create', + name: 'Collection Created', value: 'collections/create', }, { - name: 'Collections delete', + name: 'Collection Deleted', value: 'collections/delete', }, { - name: 'Collections update', + name: 'Collection Updated', value: 'collections/update', }, { - name: 'Customer groups create', + name: 'Customer Groups Created', value: 'customer_groups/create', }, { - name: 'Customer groups delete', + name: 'Customer Groups Deleted', value: 'customer_groups/delete', }, { - name: 'Customer groups update', + name: 'Customer Groups Updated', value: 'customer_groups/update', }, { - name: 'Customers create', + name: 'Customer Created', value: 'customers/create', }, { - name: 'Customers delete', + name: 'Customer Deleted', value: 'customers/delete', }, { - name: 'Customers disable', + name: 'Customer disabled', value: 'customers/disable', }, { - name: 'Customers enable', + name: 'Customer Enabled', value: 'customers/enable', }, { - name: 'Customers update', + name: 'Customer Updated', value: 'customers/update', }, { - name: 'Draft orders create', + name: 'Draft Orders Created', value: 'draft_orders/create', }, { - name: 'Draft orders delete', + name: 'Draft Orders Deleted', value: 'draft_orders/delete', }, { - name: 'Draft orders update', + name: 'Draft orders Updated', value: 'draft_orders/update', }, { - name: 'Fulfillment events create', + name: 'Fulfillment Events Created', value: 'fulfillment_events/create', }, { - name: 'Fulfillment events delete', + name: 'Fulfillment Events Deleted', value: 'fulfillment_events/delete', }, { - name: 'Fulfillments create', + name: 'Fulfillment created', value: 'fulfillments/create', }, { - name: 'Fulfillments update', + name: 'Fulfillment Updated', value: 'fulfillments/update', }, { - name: 'Inventory_items create', + name: 'Inventory Items Created', value: 'inventory_items/create', }, { - name: 'Inventory_items delete', + name: 'Inventory Items Deleted', value: 'inventory_items/delete', }, { - name: 'Inventory_items update', + name: 'Inventory Items Updated', value: 'inventory_items/update', }, { - name: 'Inventory_levels connect', + name: 'Inventory Levels Connected', value: 'inventory_levels/connect', }, { - name: 'Inventory_levels disconnect', + name: 'Inventory Levels Disconnected', value: 'inventory_levels/disconnect', }, { - name: 'Inventory_levels update', + name: 'Inventory Levels Updated', value: 'inventory_levels/update', }, { - name: 'Locales create', + name: 'Locale Created', value: 'locales/create', }, { - name: 'Locales update', + name: 'Locale Updated', value: 'locales/update', }, { - name: 'Locations create', + name: 'Location Created', value: 'locations/create', }, { - name: 'Locations delete', + name: 'Location Deleted', value: 'locations/delete', }, { - name: 'Locations update', + name: 'Location Updated', value: 'locations/update', }, { - name: 'Order transactions create', + name: 'Order transactions Created', value: 'order_transactions/create', }, { - name: 'Orders cancelled', + name: 'Order cancelled', value: 'orders/cancelled', }, { - name: 'Orders create', + name: 'Order Created', value: 'orders/create', }, { - name: 'Orders delete', + name: 'Orders Deleted', value: 'orders/delete', }, { - name: 'Orders fulfilled', + name: 'Order Fulfilled', value: 'orders/fulfilled', }, { - name: 'Orders paid', + name: 'Order Paid', value: 'orders/paid', }, { - name: 'Orders partially fulfilled', + name: 'Order Partially Fulfilled', value: 'orders/partially_fulfilled', }, { - name: 'Orders updated', + name: 'Order Updated', value: 'orders/updated', }, { - name: 'Product listings add', + name: 'Product Listings Added', value: 'product_listings/add', }, { - name: 'Product listings remove', + name: 'Product Listings Removed', value: 'product_listings/remove', }, { - name: 'Product listings update', + name: 'Product Listings Updated', value: 'product_listings/update', }, { - name: 'Products create', + name: 'Product Created', value: 'products/create', }, { - name: 'Products delete', + name: 'Product Deleted', value: 'products/delete', }, { - name: 'Products update', + name: 'Product Updated', value: 'products/update', }, { - name: 'Refunds create', + name: 'Refund Created', value: 'refunds/create', }, { - name: 'Shop update', + name: 'Shop Updated', value: 'shop/update', }, { - name: 'Tender transactions create', + name: 'Tender Transactions Created', value: 'tender_transactions/create', }, { - name: 'Themes create', + name: 'Theme Created', value: 'themes/create', }, { - name: 'Themes delete', + name: 'Theme Deleted', value: 'themes/delete', }, { - name: 'Themes publish', + name: 'Theme Published', value: 'themes/publish', }, { - name: 'Themes update', + name: 'Theme Updated', value: 'themes/update', }, ], description: 'Event that triggers the webhook', }, ], - }; // @ts-ignore (because of request) webhookMethods = { default: { async checkExists(this: IHookFunctions): Promise { + const topic = this.getNodeParameter('topic') as string; const webhookData = this.getWorkflowStaticData('node'); - if (webhookData.webhookId === undefined) { - return false; - } - const endpoint = `/webhooks/${webhookData.webhookId}.json`; - try { - await shopifyApiRequest.call(this, 'GET', endpoint, {}); - } catch (e) { - if (e.statusCode === 404) { - delete webhookData.webhookId; - return false; + const webhookUrl = this.getNodeWebhookUrl('default'); + const endpoint = `/webhooks`; + + const { webhooks } = await shopifyApiRequest.call(this, 'GET', endpoint, {}, { topic }); + for (const webhook of webhooks) { + if (webhook.address === webhookUrl) { + webhookData.webhookId = webhook.id; + return true; } - throw e; } - return true; + return false; }, async create(this: IHookFunctions): Promise { - const credentials = this.getCredentials('shopifyApi'); const webhookUrl = this.getNodeWebhookUrl('default'); const topic = this.getNodeParameter('topic') as string; + const webhookData = this.getWorkflowStaticData('node'); const endpoint = `/webhooks.json`; const body = { webhook: { @@ -330,21 +328,15 @@ export class ShopifyTrigger implements INodeType { }; let responseData; - try { - responseData = await shopifyApiRequest.call(this, 'POST', endpoint, body); - } catch (error) { - return false; - } + + responseData = await shopifyApiRequest.call(this, 'POST', endpoint, body); if (responseData.webhook === undefined || responseData.webhook.id === undefined) { // Required data is missing so was not successful return false; } - const webhookData = this.getWorkflowStaticData('node'); webhookData.webhookId = responseData.webhook.id as string; - webhookData.sharedSecret = credentials!.sharedSecret as string; - webhookData.topic = topic as string; return true; }, async delete(this: IHookFunctions): Promise { @@ -357,8 +349,6 @@ export class ShopifyTrigger implements INodeType { return false; } delete webhookData.webhookId; - delete webhookData.sharedSecret; - delete webhookData.topic; } return true; }, @@ -368,17 +358,18 @@ export class ShopifyTrigger implements INodeType { async webhook(this: IWebhookFunctions): Promise { const headerData = this.getHeaderData() as IDataObject; const req = this.getRequestObject(); - const webhookData = this.getWorkflowStaticData('node') as IDataObject; + const credentials = this.getCredentials('shopifyApi') as IDataObject; + const topic = this.getNodeParameter('topic') as string; if (headerData['x-shopify-topic'] !== undefined && headerData['x-shopify-hmac-sha256'] !== undefined && headerData['x-shopify-shop-domain'] !== undefined && headerData['x-shopify-api-version'] !== undefined) { // @ts-ignore - const computedSignature = createHmac('sha256', webhookData.sharedSecret as string).update(req.rawBody).digest('base64'); + const computedSignature = createHmac('sha256', credentials.sharedSecret as string).update(req.rawBody).digest('base64'); if (headerData['x-shopify-hmac-sha256'] !== computedSignature) { return {}; } - if (webhookData.topic !== headerData['x-shopify-topic']) { + if (topic !== headerData['x-shopify-topic']) { return {}; } } else { From 5288365104c588f2c7cb2cbc595a5ca4437873cd Mon Sep 17 00:00:00 2001 From: Vincent Barrier Date: Sun, 8 Nov 2020 14:43:16 +0100 Subject: [PATCH 02/42] add custom properties for deals add custom properties for deals --- .../nodes/Hubspot/DealDescription.ts | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/packages/nodes-base/nodes/Hubspot/DealDescription.ts b/packages/nodes-base/nodes/Hubspot/DealDescription.ts index 837c37a238..26868fc54c 100644 --- a/packages/nodes-base/nodes/Hubspot/DealDescription.ts +++ b/packages/nodes-base/nodes/Hubspot/DealDescription.ts @@ -151,6 +151,41 @@ export const dealFields = [ }, default: [], }, + { + displayName: 'Custom Properties', + name: 'customPropertiesUi', + placeholder: 'Add Custom Property', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'customPropertiesValues', + displayName: 'Custom Property', + values: [ + { + displayName: 'Property', + name: 'property', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getDealCustomProperties', + }, + default: '', + description: 'Name of the property.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the property', + }, + ], + }, + ], + } ], }, /* -------------------------------------------------------------------------- */ @@ -235,6 +270,41 @@ export const dealFields = [ }, default: '', }, + { + displayName: 'Custom Properties', + name: 'customPropertiesUi', + placeholder: 'Add Custom Property', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'customPropertiesValues', + displayName: 'Custom Property', + values: [ + { + displayName: 'Property', + name: 'property', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getDealCustomProperties', + }, + default: '', + description: 'Name of the property.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the property', + }, + ], + }, + ], + } ], }, /* -------------------------------------------------------------------------- */ From b1ba991367675d5a16ab5145260a217a5d2a6b5e Mon Sep 17 00:00:00 2001 From: Vincent Barrier Date: Sun, 8 Nov 2020 14:45:00 +0100 Subject: [PATCH 03/42] Manage custom properties on Hubspot deals --- .../nodes-base/nodes/Hubspot/Hubspot.node.ts | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts index aaae8c5895..e04a829d93 100644 --- a/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts +++ b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts @@ -552,6 +552,25 @@ export class Hubspot implements INodeType { } return returnData; }, + + // Get all the deal properties to display them to user so that he can + // select them easily + async getDealCustomProperties(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const endpoint = '/properties/v2/deals/properties'; + const properties = await hubspotApiRequest.call(this, 'GET', endpoint, {}); + for (const property of properties) { + if (property.hubspotDefined === null) { + const propertyName = property.label; + const propertyId = property.name; + returnData.push({ + name: propertyName, + value: propertyId, + }); + } + } + return returnData; + }, /* -------------------------------------------------------------------------- */ /* FORM */ @@ -1801,6 +1820,17 @@ export class Hubspot implements INodeType { value: additionalFields.pipeline as string, }); } + if (additionalFields.customPropertiesUi) { + const customProperties = (additionalFields.customPropertiesUi as IDataObject).customPropertiesValues as IDataObject[]; + if (customProperties) { + for (const customProperty of customProperties) { + body.properties.push({ + name: customProperty.property, + value: customProperty.value, + }); + } + } + } body.associations = association; const endpoint = '/deals/v1/deal'; responseData = await hubspotApiRequest.call(this, 'POST', endpoint, body); @@ -1846,6 +1876,17 @@ export class Hubspot implements INodeType { value: updateFields.pipeline as string, }); } + if (updateFields.customPropertiesUi) { + const customProperties = (updateFields.customPropertiesUi as IDataObject).customPropertiesValues as IDataObject[]; + if (customProperties) { + for (const customProperty of customProperties) { + body.properties.push({ + name: customProperty.property, + value: customProperty.value, + }); + } + } + } const endpoint = `/deals/v1/deal/${dealId}`; responseData = await hubspotApiRequest.call(this, 'PUT', endpoint, body); } From 83ce722dcf82aa01e94493d79b5f60a74d31c6e3 Mon Sep 17 00:00:00 2001 From: ricardo Date: Sun, 8 Nov 2020 11:14:43 -0500 Subject: [PATCH 04/42] :zap: Fields ordered alphabetically --- .../nodes/Hubspot/DealDescription.ts | 126 +++++++++--------- 1 file changed, 63 insertions(+), 63 deletions(-) diff --git a/packages/nodes-base/nodes/Hubspot/DealDescription.ts b/packages/nodes-base/nodes/Hubspot/DealDescription.ts index 26868fc54c..28e2261fed 100644 --- a/packages/nodes-base/nodes/Hubspot/DealDescription.ts +++ b/packages/nodes-base/nodes/Hubspot/DealDescription.ts @@ -100,39 +100,12 @@ export const dealFields = [ }, }, options: [ - { - displayName: 'Deal Name', - name: 'dealName', - type: 'string', - default: '', - }, - { - displayName: 'Pipeline', - name: 'pipeline', - type: 'string', - default: '', - }, - { - displayName: 'Close Date', - name: 'closeDate', - type: 'dateTime', - default: '', - }, { displayName: 'Amount', name: 'amount', type: 'string', default: '', }, - { - displayName: 'Deal Type', - name: 'dealType', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getDealTypes', - }, - default: '', - }, { displayName: 'Associated Company', name: 'associatedCompany', @@ -151,6 +124,12 @@ export const dealFields = [ }, default: [], }, + { + displayName: 'Close Date', + name: 'closeDate', + type: 'dateTime', + default: '', + }, { displayName: 'Custom Properties', name: 'customPropertiesUi', @@ -185,7 +164,28 @@ export const dealFields = [ ], }, ], - } + }, + { + displayName: 'Deal Name', + name: 'dealName', + type: 'string', + default: '', + }, + { + displayName: 'Deal Type', + name: 'dealType', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getDealTypes', + }, + default: '', + }, + { + displayName: 'Pipeline', + name: 'pipeline', + type: 'string', + default: '', + }, ], }, /* -------------------------------------------------------------------------- */ @@ -227,25 +227,8 @@ export const dealFields = [ }, options: [ { - displayName: 'Deal Name', - name: 'dealName', - type: 'string', - default: '', - }, - { - displayName: 'Deal Stage', - name: 'stage', - type: 'options', - required: true, - typeOptions: { - loadOptionsMethod: 'getDealStages', - }, - default: '', - description: 'The dealstage is required when creating a deal. See the CRM Pipelines API for details on managing pipelines and stages.', - }, - { - displayName: 'Pipeline', - name: 'pipeline', + displayName: 'Amount', + name: 'amount', type: 'string', default: '', }, @@ -255,21 +238,6 @@ export const dealFields = [ type: 'dateTime', default: '', }, - { - displayName: 'Amount', - name: 'amount', - type: 'string', - default: '', - }, - { - displayName: 'Deal Type', - name: 'dealType', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getDealTypes', - }, - default: '', - }, { displayName: 'Custom Properties', name: 'customPropertiesUi', @@ -304,7 +272,39 @@ export const dealFields = [ ], }, ], - } + }, + { + displayName: 'Deal Name', + name: 'dealName', + type: 'string', + default: '', + }, + { + displayName: 'Deal Stage', + name: 'stage', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getDealStages', + }, + default: '', + description: 'The dealstage is required when creating a deal. See the CRM Pipelines API for details on managing pipelines and stages.', + }, + { + displayName: 'Deal Type', + name: 'dealType', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getDealTypes', + }, + default: '', + }, + { + displayName: 'Pipeline', + name: 'pipeline', + type: 'string', + default: '', + }, ], }, /* -------------------------------------------------------------------------- */ From 9fee42b2a8a49025bc34d0d33f7e865fcda4dc62 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 9 Nov 2020 11:26:46 +0100 Subject: [PATCH 05/42] :zap: Add shear option, resize with alpha color and fix bug --- .../src/components/ParameterInput.vue | 50 +++++++++++++- packages/nodes-base/nodes/EditImage.node.ts | 65 ++++++++++++++++++- packages/workflow/src/Interfaces.ts | 1 + 3 files changed, 111 insertions(+), 5 deletions(-) diff --git a/packages/editor-ui/src/components/ParameterInput.vue b/packages/editor-ui/src/components/ParameterInput.vue index 893c61aa33..336e27041a 100644 --- a/packages/editor-ui/src/components/ParameterInput.vue +++ b/packages/editor-ui/src/components/ParameterInput.vue @@ -82,8 +82,8 @@
- - + +
@@ -213,6 +213,10 @@ export default mixins( this.loadRemoteParameterOptions(); }, value () { + if (this.parameter.type === 'color' && this.getArgument('showAlpha') === true) { + // Do not set for color with alpha else wrong value gets displayed in field + return; + } this.tempValue = this.displayValue as string; }, }, @@ -274,6 +278,18 @@ export default mixins( returnValue = this.expressionValueComputed; } + if (this.parameter.type === 'color' && this.getArgument('showAlpha') === true && returnValue.charAt(0) === '#') { + // Convert the value to rgba that el-color-picker can display it correctly + const bigint = parseInt(returnValue.slice(1), 16); + const h = []; + h.push((bigint >> 24) & 255); + h.push((bigint >> 16) & 255); + h.push((bigint >> 8) & 255); + h.push((255 - bigint & 255) / 255); + + returnValue = 'rgba('+h.join()+')'; + } + if (returnValue !== undefined && returnValue !== null && this.parameter.type === 'string') { const rows = this.getArgument('rows'); if (rows === undefined || rows === 1) { @@ -537,14 +553,35 @@ export default mixins( // Set focus on field setTimeout(() => { // @ts-ignore - (this.$refs.inputField.$el.querySelector('input') as HTMLInputElement).focus(); + if (this.$refs.inputField.$el) { + // @ts-ignore + (this.$refs.inputField.$el.querySelector('input') as HTMLInputElement).focus(); + } }); }, + rgbaToHex (value: string): string | null { + // Convert rgba to hex from: https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb + const valueMatch = (value as string).match(/^rgba\((\d+),\s*(\d+),\s*(\d+),\s*(\d+(\.\d+)?)\)$/); + if (valueMatch === null) { + // TODO: Display something if value is not valid + return null; + } + const [r, g, b, a] = valueMatch.splice(1, 4).map(v => Number(v)); + return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1) + ((1 << 8) + Math.floor((1-a)*255)).toString(16).slice(1); + }, valueChanged (value: string | number | boolean | Date | null) { if (value instanceof Date) { value = value.toISOString(); } + if (this.parameter.type === 'color' && this.getArgument('showAlpha') === true && value !== null && value.toString().charAt(0) !== '#') { + const newValue = this.rgbaToHex(value as string); + if (newValue !== null) { + this.tempValue = newValue; + value = newValue; + } + } + const parameterData = { node: this.node !== null ? this.node.name : this.nodeName, name: this.path, @@ -570,6 +607,13 @@ export default mixins( this.nodeName = this.node.name; } + if (this.parameter.type === 'color' && this.getArgument('showAlpha') === true && this.displayValue !== null && this.displayValue.toString().charAt(0) !== '#') { + const newValue = this.rgbaToHex(this.displayValue as string); + if (newValue !== null) { + this.tempValue = newValue; + } + } + if (this.remoteMethod !== undefined && this.node !== null) { // Make sure to load the parameter options // directly and whenever the credentials change diff --git a/packages/nodes-base/nodes/EditImage.node.ts b/packages/nodes-base/nodes/EditImage.node.ts index 9e10000ffd..5c3b9c53aa 100644 --- a/packages/nodes-base/nodes/EditImage.node.ts +++ b/packages/nodes-base/nodes/EditImage.node.ts @@ -9,6 +9,12 @@ import { INodeTypeDescription, } from 'n8n-workflow'; import * as gm from 'gm'; +import { file } from 'tmp-promise'; +import { + writeFile as fsWriteFile, +} from 'fs'; +import { promisify } from 'util'; +const fsWriteFileAsync = promisify(fsWriteFile); export class EditImage implements INodeType { @@ -61,6 +67,11 @@ export class EditImage implements INodeType { value: 'resize', description: 'Change the size of image', }, + { + name: 'Shear', + value: 'shear', + description: 'Shear image along the X or Y axis', + }, { name: 'Text', value: 'text', @@ -385,6 +396,11 @@ export class EditImage implements INodeType { value: 'onlyIfSmaller', description: 'Resize only if image is smaller than width or height', }, + { + name: 'Percent', + value: 'percent', + description: 'Width and height are specified in percents.', + }, ], default: 'maximumArea', displayOptions: { @@ -422,7 +438,10 @@ export class EditImage implements INodeType { displayName: 'Background Color', name: 'backgroundColor', type: 'color', - default: '#ffffff', + default: '#ffffffff', + typeOptions: { + showAlpha: true, + }, displayOptions: { show: { operation: [ @@ -433,6 +452,39 @@ export class EditImage implements INodeType { description: 'The color to use for the background when image gets rotated by anything which is not a multiple of 90..', }, + + // ---------------------------------- + // shear + // ---------------------------------- + { + displayName: 'Degrees X', + name: 'degreesX', + type: 'number', + default: 0, + displayOptions: { + show: { + operation: [ + 'shear', + ], + }, + }, + description: 'X (horizontal) shear degrees.', + }, + { + displayName: 'Degrees Y', + name: 'degreesY', + type: 'number', + default: 0, + displayOptions: { + show: { + operation: [ + 'shear', + ], + }, + }, + description: 'Y (vertical) shear degrees.', + }, + { displayName: 'Options', name: 'options', @@ -503,7 +555,6 @@ export class EditImage implements INodeType { }, description: 'Sets the jpeg|png|tiff compression level from 0 to 100 (best).', }, - ], }, ], @@ -529,6 +580,8 @@ export class EditImage implements INodeType { let gmInstance = gm(Buffer.from(item.binary![dataPropertyName as string].data, BINARY_ENCODING)); + gmInstance = gmInstance.background('transparent'); + if (operation === 'blur') { const blur = this.getNodeParameter('blur') as number; const sigma = this.getNodeParameter('sigma') as number; @@ -574,6 +627,8 @@ export class EditImage implements INodeType { option = '<'; } else if (resizeOption === 'onlyIfLarger') { option = '>'; + } else if (resizeOption === 'percent') { + option = '%'; } gmInstance = gmInstance.resize(width, height, option); @@ -581,6 +636,10 @@ export class EditImage implements INodeType { const rotate = this.getNodeParameter('rotate') as number; const backgroundColor = this.getNodeParameter('backgroundColor') as string; gmInstance = gmInstance.rotate(backgroundColor, rotate); + } else if (operation === 'shear') { + const xDegrees = this.getNodeParameter('degreesX') as number; + const yDegress = this.getNodeParameter('degreesY') as number; + gmInstance = gmInstance.shear(xDegrees, yDegress); } else if (operation === 'text') { const fontColor = this.getNodeParameter('fontColor') as string; const fontSize = this.getNodeParameter('fontSize') as number; @@ -624,6 +683,8 @@ export class EditImage implements INodeType { // data references which do not get changed still stay behind // but the incoming data does not get changed. Object.assign(newItem.binary, item.binary); + // Make a deep copy of the binary data we change + newItem.binary![dataPropertyName as string] = JSON.parse(JSON.stringify(newItem.binary![dataPropertyName as string])); } if (options.quality !== undefined) { diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index bf0fc63e13..c6e42e0d28 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -408,6 +408,7 @@ export interface INodePropertyTypeOptions { numberStepSize?: number; // Supported by: number password?: boolean; // Supported by: string rows?: number; // Supported by: string + showAlpha?: boolean; // Supported by: color [key: string]: boolean | number | string | EditorTypes | undefined | string[]; } From 639bdd2ceb799a6be6d57eb120e90bc2c0a59a97 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 9 Nov 2020 12:23:53 +0100 Subject: [PATCH 06/42] :sparkles: Allow custom documentation URLs on credentials & nodes --- packages/editor-ui/src/components/CredentialsEdit.vue | 8 ++++++-- packages/editor-ui/src/components/DataDisplay.vue | 11 +++++++++-- packages/workflow/src/Interfaces.ts | 1 + 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/editor-ui/src/components/CredentialsEdit.vue b/packages/editor-ui/src/components/CredentialsEdit.vue index 49e3a12acd..6d01db51ac 100644 --- a/packages/editor-ui/src/components/CredentialsEdit.vue +++ b/packages/editor-ui/src/components/CredentialsEdit.vue @@ -20,7 +20,7 @@ - Need help? Open credential docs + Need help? Open credential docs
@@ -119,7 +119,11 @@ export default mixins( const credentialType = this.$store.getters.credentialType(credentialTypeName); if (credentialType.documentationUrl !== undefined) { - return `${credentialType.documentationUrl}`; + if (credentialType.documentationUrl.startsWith('http')) { + return credentialType.documentationUrl; + } else { + return 'https://docs.n8n.io/credentials/' + credentialType.documentationUrl + '/?utm_source=n8n_app&utm_medium=left_nav_menu&utm_campaign=create_new_credentials_modal'; + } } return undefined; }, diff --git a/packages/editor-ui/src/components/DataDisplay.vue b/packages/editor-ui/src/components/DataDisplay.vue index 6eb2b90ea3..58a7849e04 100644 --- a/packages/editor-ui/src/components/DataDisplay.vue +++ b/packages/editor-ui/src/components/DataDisplay.vue @@ -9,7 +9,7 @@
-
@@ -65,6 +65,13 @@ export default Vue.extend({ }; }, computed: { + documentationUrl (): string { + if (this.nodeType.documentationUrl && this.nodeType.documentationUrl.startsWith('http')) { + return this.nodeType.documentationUrl; + } + + return 'https://docs.n8n.io/nodes/' + (this.nodeType.documentationUrl || this.nodeType.name) + '?utm_source=n8n_app&utm_medium=node_settings_modal-credential_link&utm_campaign=' + this.nodeType.name; + }, node (): INodeUi { return this.$store.getters.activeNode; }, diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index c6e42e0d28..492810cf2c 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -536,6 +536,7 @@ export interface INodeTypeDescription { version: number; description: string; defaults: INodeParameters; + documentationUrl?: string; inputs: string[]; inputNames?: string[]; outputs: string[]; From c6f21804d73dd000dc8af3fbbd4e16b2dd79c4cc Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 9 Nov 2020 12:35:44 +0100 Subject: [PATCH 07/42] :shirt: Fix lint issue --- packages/editor-ui/src/components/DataDisplay.vue | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/editor-ui/src/components/DataDisplay.vue b/packages/editor-ui/src/components/DataDisplay.vue index 58a7849e04..c694173540 100644 --- a/packages/editor-ui/src/components/DataDisplay.vue +++ b/packages/editor-ui/src/components/DataDisplay.vue @@ -66,6 +66,10 @@ export default Vue.extend({ }, computed: { documentationUrl (): string { + if (!this.nodeType) { + return ''; + } + if (this.nodeType.documentationUrl && this.nodeType.documentationUrl.startsWith('http')) { return this.nodeType.documentationUrl; } From ad6155068e7072c35aee85b6824858e456fb771d Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Tue, 10 Nov 2020 01:43:39 -0500 Subject: [PATCH 08/42] :zap: Jira retrieve all projects (#1140) Fixes #1138 --- packages/nodes-base/nodes/Jira/GenericFunctions.ts | 2 +- packages/nodes-base/nodes/Jira/Jira.node.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/nodes-base/nodes/Jira/GenericFunctions.ts b/packages/nodes-base/nodes/Jira/GenericFunctions.ts index 93cbdd5cc1..3d2198d34f 100644 --- a/packages/nodes-base/nodes/Jira/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Jira/GenericFunctions.ts @@ -73,7 +73,7 @@ export async function jiraSoftwareCloudApiRequest(this: IHookFunctions | IExecut } } -export async function jiraSoftwareCloudApiRequestAllItems(this: IHookFunctions | IExecuteFunctions, propertyName: string, endpoint: string, method: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any +export async function jiraSoftwareCloudApiRequestAllItems(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, endpoint: string, method: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any const returnData: IDataObject[] = []; diff --git a/packages/nodes-base/nodes/Jira/Jira.node.ts b/packages/nodes-base/nodes/Jira/Jira.node.ts index 0418f0b42c..0a455fd405 100644 --- a/packages/nodes-base/nodes/Jira/Jira.node.ts +++ b/packages/nodes-base/nodes/Jira/Jira.node.ts @@ -112,12 +112,16 @@ export class Jira implements INodeType { async getProjects(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; const jiraVersion = this.getCurrentNodeParameter('jiraVersion') as string; + let endpoint = ''; + let projects; - let endpoint = '/api/2/project/search'; if (jiraVersion === 'server') { endpoint = '/api/2/project'; + projects = await jiraSoftwareCloudApiRequest.call(this, endpoint, 'GET'); + } else { + endpoint = '/api/2/project/search'; + projects = await jiraSoftwareCloudApiRequestAllItems.call(this, 'values', endpoint, 'GET'); } - let projects = await jiraSoftwareCloudApiRequest.call(this, endpoint, 'GET'); if (projects.values && Array.isArray(projects.values)) { projects = projects.values; From f8af77742338af3d84c4ae8d23ff3fa2f4e4c714 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Tue, 10 Nov 2020 07:49:43 +0100 Subject: [PATCH 09/42] :zap: Sort Jira options --- packages/nodes-base/nodes/Jira/Jira.node.ts | 47 +++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/packages/nodes-base/nodes/Jira/Jira.node.ts b/packages/nodes-base/nodes/Jira/Jira.node.ts index 0a455fd405..98ecc8757f 100644 --- a/packages/nodes-base/nodes/Jira/Jira.node.ts +++ b/packages/nodes-base/nodes/Jira/Jira.node.ts @@ -134,6 +134,13 @@ export class Jira implements INodeType { value: projectId, }); } + + returnData.sort((a, b) => { + if (a.name < b.name) { return -1; } + if (a.name > b.name) { return 1; } + return 0; + }); + return returnData; }, @@ -169,6 +176,12 @@ export class Jira implements INodeType { } } + returnData.sort((a, b) => { + if (a.name < b.name) { return -1; } + if (a.name > b.name) { return 1; } + return 0; + }); + return returnData; }, @@ -188,6 +201,13 @@ export class Jira implements INodeType { value: labelId, }); } + + returnData.sort((a, b) => { + if (a.name < b.name) { return -1; } + if (a.name > b.name) { return 1; } + return 0; + }); + return returnData; }, @@ -207,6 +227,13 @@ export class Jira implements INodeType { value: priorityId, }); } + + returnData.sort((a, b) => { + if (a.name < b.name) { return -1; } + if (a.name > b.name) { return 1; } + return 0; + }); + return returnData; }, @@ -245,6 +272,12 @@ export class Jira implements INodeType { } } + returnData.sort((a, b) => { + if (a.name < b.name) { return -1; } + if (a.name > b.name) { return 1; } + return 0; + }); + return returnData; }, @@ -264,6 +297,13 @@ export class Jira implements INodeType { value: groupId, }); } + + returnData.sort((a, b) => { + if (a.name < b.name) { return -1; } + if (a.name > b.name) { return 1; } + return 0; + }); + return returnData; }, @@ -281,6 +321,13 @@ export class Jira implements INodeType { value: transition.id, }); } + + returnData.sort((a, b) => { + if (a.name < b.name) { return -1; } + if (a.name > b.name) { return 1; } + return 0; + }); + return returnData; }, }, From bbef8b7f71dac4d3df121b916ec2f76fffb1fcad Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Tue, 10 Nov 2020 12:28:03 -0500 Subject: [PATCH 10/42] :sparkles: Add GetResponse-Node (#1139) --- .../credentials/GetResponseApi.credentials.ts | 17 + .../GetResponseOAuth2Api.credentials.ts | 47 ++ .../nodes/GetResponse/ContactDescription.ts | 646 ++++++++++++++++++ .../nodes/GetResponse/GenericFunctions.ts | 71 ++ .../nodes/GetResponse/GetResponse.node.ts | 320 +++++++++ .../nodes/GetResponse/getResponse.png | Bin 0 -> 2851 bytes packages/nodes-base/package.json | 3 + 7 files changed, 1104 insertions(+) create mode 100644 packages/nodes-base/credentials/GetResponseApi.credentials.ts create mode 100644 packages/nodes-base/credentials/GetResponseOAuth2Api.credentials.ts create mode 100644 packages/nodes-base/nodes/GetResponse/ContactDescription.ts create mode 100644 packages/nodes-base/nodes/GetResponse/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/GetResponse/GetResponse.node.ts create mode 100644 packages/nodes-base/nodes/GetResponse/getResponse.png diff --git a/packages/nodes-base/credentials/GetResponseApi.credentials.ts b/packages/nodes-base/credentials/GetResponseApi.credentials.ts new file mode 100644 index 0000000000..1494a01930 --- /dev/null +++ b/packages/nodes-base/credentials/GetResponseApi.credentials.ts @@ -0,0 +1,17 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class GetResponseApi implements ICredentialType { + name = 'getResponseApi'; + displayName = 'GetResponse API'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/credentials/GetResponseOAuth2Api.credentials.ts b/packages/nodes-base/credentials/GetResponseOAuth2Api.credentials.ts new file mode 100644 index 0000000000..76ce3acb0f --- /dev/null +++ b/packages/nodes-base/credentials/GetResponseOAuth2Api.credentials.ts @@ -0,0 +1,47 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class GetResponseOAuth2Api implements ICredentialType { + name = 'getResponseOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'GetResponse OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://app.getresponse.com/oauth2_authorize.html', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://api.getresponse.com/v3/token', + required: true, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'header', + description: 'Resource to consume.', + }, + ]; +} diff --git a/packages/nodes-base/nodes/GetResponse/ContactDescription.ts b/packages/nodes-base/nodes/GetResponse/ContactDescription.ts new file mode 100644 index 0000000000..ba173edde4 --- /dev/null +++ b/packages/nodes-base/nodes/GetResponse/ContactDescription.ts @@ -0,0 +1,646 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const contactOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'contact', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new contact', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a contact', + }, + { + name: 'Get', + value: 'get', + description: 'Get a contact', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all contacts', + }, + { + name: 'Update', + value: 'update', + description: 'Update contact properties', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const contactFields = [ + /* -------------------------------------------------------------------------- */ + /* contact:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Email', + name: 'email', + type: 'string', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: '', + }, + { + displayName: 'Campaign ID', + name: 'campaignId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: '', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Custom Fields', + name: 'customFieldsUi', + type: 'fixedCollection', + default: '', + placeholder: 'Add Custom Field', + typeOptions: { + multipleValues: true, + }, + options: [ + { + name: 'customFieldValues', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field ID', + name: 'customFieldId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCustomFields', + }, + description: 'The end user specified key of the user defined data.', + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + description: 'The end user specified value of the user defined data.', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Day Of Cycle', + name: 'dayOfCycle', + type: 'string', + description: `The day on which the contact is in the Autoresponder cycle. null indicates the contacts is not in the cycle.`, + default: '', + }, + { + displayName: 'IP Address', + name: 'ipAddress', + type: 'string', + description: `The contact's IP address. IPv4 and IPv6 formats are accepted.`, + default: '', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + }, + { + displayName: 'Note', + name: 'note', + type: 'string', + default: '', + }, + { + displayName: 'Scoring', + name: 'scoring', + type: 'number', + default: '', + description: 'Contact scoring, pass null to remove the score from a contact', + typeOptions: { + minValue: 0, + }, + }, + { + displayName: 'Tag IDs', + name: 'tags', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + default: '', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* contact:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'delete', + ], + }, + }, + default: '', + description: 'Id of contact to delete.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'delete', + ], + }, + }, + options: [ + { + displayName: 'IP Address', + name: 'ipAddress', + type: 'string', + description: `This makes it possible to pass the IP from which the contact unsubscribed. Used only if the messageId was send.`, + default: '', + }, + { + displayName: 'Message ID', + name: 'messageId', + type: 'string', + description: `The ID of a message (such as a newsletter, an autoresponder, or an RSS-newsletter). When passed, this method will simulate the unsubscribe process, as if the contact clicked the unsubscribe link in a given message.`, + default: '', + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* contact:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'get', + ], + }, + }, + default: '', + description: 'Unique identifier for a particular contact', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + description: `List of fields that should be returned. Id is always returned. Fields should be separated by comma`, + default: '', + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* contact:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'contact', + ], + 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: 20, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Campaign ID', + name: 'campaignId', + type: 'string', + description: `Search contacts by campaign ID`, + default: '', + }, + { + displayName: 'Change On From', + name: 'changeOnFrom', + type: 'dateTime', + default: '', + description: `Search contacts edited from this date`, + }, + { + displayName: 'Change On To', + name: 'changeOnTo', + type: 'dateTime', + default: '', + description: `Search contacts edited to this date`, + }, + { + displayName: 'Created On From', + name: 'createdOnFrom', + type: 'dateTime', + default: '', + description: `Count data from this date`, + }, + { + displayName: 'Created On To', + name: 'createdOnTo', + type: 'dateTime', + default: '', + description: `Count data from this date`, + }, + { + displayName: 'Exact Match', + name: 'exactMatch', + type: 'boolean', + default: false, + description: `When set to true it will search for contacts with the exact value
+ of the email and name provided in the query string. Without this flag, matching is done via a standard 'like' comparison,
+ which may sometimes be slow.`, + }, + { + displayName: 'Fields', + name: 'fields', + type: 'string', + description: `List of fields that should be returned. Id is always returned. Fields should be separated by comma`, + default: '', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + description: `Search contacts by name`, + default: '', + }, + { + displayName: 'Origin', + name: 'origin', + type: 'options', + options: [ + { + name: 'API', + value: 'api', + }, + { + name: 'Copy', + value: 'copy', + }, + { + name: 'Email', + value: 'email', + }, + { + name: 'Forward', + value: 'forward', + }, + { + name: 'import', + value: 'import', + }, + { + name: 'Iphone', + value: 'iphone', + }, + { + name: 'Landing Page', + value: 'landing_page', + }, + { + name: 'Leads', + value: 'leads', + }, + { + name: 'Panel', + value: 'panel', + }, + { + name: 'Sale', + value: 'sale', + }, + { + name: 'Survey', + value: 'survey', + }, + { + name: 'Webinar', + value: 'webinar', + }, + { + name: 'WWW', + value: 'www', + }, + ], + description: `Search contacts by origin`, + default: '', + }, + { + displayName: 'Sort By', + name: 'sortBy', + type: 'options', + options: [ + { + name: 'Campaign ID', + value: 'campaignId', + }, + { + name: 'Changed On', + value: 'changedOn', + }, + { + name: 'Created On', + value: 'createdOn', + }, + { + name: 'Email', + value: 'email', + }, + ], + default: '', + }, + { + displayName: 'Sort Order', + name: 'sortOrder', + type: 'options', + options: [ + { + name: 'ASC', + value: 'ASC', + }, + { + name: 'DESC', + value: 'DESC', + }, + ], + default: '', + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* contact:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + description: 'Unique identifier for a particular contact', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Campaign ID', + name: 'campaignId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + default: '', + description: '', + }, + { + displayName: 'Custom Fields', + name: 'customFieldsUi', + type: 'fixedCollection', + default: '', + placeholder: 'Add Custom Field', + typeOptions: { + multipleValues: true, + }, + options: [ + { + name: 'customFieldValues', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field ID', + name: 'customFieldId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCustomFields', + }, + description: 'The end user specified key of the user defined data.', + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + description: 'The end user specified value of the user defined data.', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Day Of Cycle', + name: 'dayOfCycle', + type: 'string', + description: `The day on which the contact is in the Autoresponder cycle. null indicates the contacts is not in the cycle.`, + default: '', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: '', + }, + { + displayName: 'IP Address', + name: 'ipAddress', + type: 'string', + description: `The contact's IP address. IPv4 and IPv6 formats are accepted.`, + default: '', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + }, + { + displayName: 'Note', + name: 'note', + type: 'string', + default: '', + }, + { + displayName: 'Scoring', + name: 'scoring', + type: 'number', + default: '', + description: 'Contact scoring, pass null to remove the score from a contact', + typeOptions: { + minValue: 0, + }, + }, + { + displayName: 'Tag IDs', + name: 'tags', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + default: '', + }, + ], + }, + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/GetResponse/GenericFunctions.ts b/packages/nodes-base/nodes/GetResponse/GenericFunctions.ts new file mode 100644 index 0000000000..eaae8d4df1 --- /dev/null +++ b/packages/nodes-base/nodes/GetResponse/GenericFunctions.ts @@ -0,0 +1,71 @@ +import { + OptionsWithUri, + } from 'request'; + +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject +} from 'n8n-workflow'; + +export async function getresponseApiRequest(this: IWebhookFunctions | IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const authentication = this.getNodeParameter('authentication', 0, 'apiKey') as string; + + let options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: uri || `https://api.getresponse.com/v3${resource}`, + json: true, + }; + try { + options = Object.assign({}, options, option); + if (Object.keys(body).length === 0) { + delete options.body; + } + + if (authentication === 'apiKey') { + const credentials = this.getCredentials('getResponseApi') as IDataObject; + options!.headers!['X-Auth-Token'] = `api-key ${credentials.apiKey}`; + //@ts-ignore + return await this.helpers.request.call(this, options); + } else { + //@ts-ignore + return await this.helpers.requestOAuth2.call(this, 'getResponseOAuth2Api', options); + } + } catch (error) { + if (error.response && error.response.body && error.response.body.message) { + // Try to return the error prettier + throw new Error(`GetResponse error response [${error.statusCode}]: ${error.response.body.message}`); + } + throw error; + } +} + +export async function getResponseApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + query.page = 1; + + do { + responseData = await getresponseApiRequest.call(this, method, endpoint, body, query, undefined, { resolveWithFullResponse: true }); + query.page++; + returnData.push.apply(returnData, responseData.body); + } while ( + responseData.headers.TotalPages !== responseData.headers.CurrentPage + ); + + return returnData; +} + diff --git a/packages/nodes-base/nodes/GetResponse/GetResponse.node.ts b/packages/nodes-base/nodes/GetResponse/GetResponse.node.ts new file mode 100644 index 0000000000..01b069b79c --- /dev/null +++ b/packages/nodes-base/nodes/GetResponse/GetResponse.node.ts @@ -0,0 +1,320 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + getresponseApiRequest, + getResponseApiRequestAllItems, +} from './GenericFunctions'; + +import { + contactFields, + contactOperations, +} from './ContactDescription'; + +import * as moment from 'moment-timezone'; + +export class GetResponse implements INodeType { + description: INodeTypeDescription = { + displayName: 'GetResponse', + name: 'getResponse', + icon: 'file:getResponse.png', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume GetResponse API.', + defaults: { + name: 'GetResponse', + color: '#00afec', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'getResponseApi', + required: true, + displayOptions: { + show: { + authentication: [ + 'apiKey', + ], + }, + }, + }, + { + name: 'getResponseOAuth2Api', + 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', + type: 'options', + options: [ + { + name: 'Contact', + value: 'contact', + }, + ], + default: 'contact', + description: 'The resource to operate on.', + }, + ...contactOperations, + ...contactFields, + ], + }; + + methods = { + loadOptions: { + // Get all the campaigns to display them to user so that he can + // select them easily + async getCampaigns( + this: ILoadOptionsFunctions, + ): Promise { + const returnData: INodePropertyOptions[] = []; + const campaigns = await getresponseApiRequest.call( + this, + 'GET', + `/campaigns`, + ); + for (const campaign of campaigns) { + returnData.push({ + name: campaign.name as string, + value: campaign.campaignId, + }); + } + return returnData; + }, + // Get all the tagd to display them to user so that he can + // select them easily + async getTags( + this: ILoadOptionsFunctions, + ): Promise { + const returnData: INodePropertyOptions[] = []; + const tags = await getresponseApiRequest.call( + this, + 'GET', + `/tags`, + ); + for (const tag of tags) { + returnData.push({ + name: tag.name as string, + value: tag.tagId, + }); + } + return returnData; + }, + // Get all the custom fields to display them to user so that he can + // select them easily + async getCustomFields( + this: ILoadOptionsFunctions, + ): Promise { + const returnData: INodePropertyOptions[] = []; + const customFields = await getresponseApiRequest.call( + this, + 'GET', + `/custom-fields`, + ); + for (const customField of customFields) { + returnData.push({ + name: customField.name as string, + value: customField.customFieldId, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = (items.length as unknown) as number; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < length; i++) { + + if (resource === 'contact') { + //https://apireference.getresponse.com/#operation/createContact + if (operation === 'create') { + const email = this.getNodeParameter('email', i) as string; + + const campaignId = this.getNodeParameter('campaignId', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = { + email, + campaign: { + campaignId, + }, + }; + + Object.assign(body, additionalFields); + + if (additionalFields.customFieldsUi) { + const customFieldValues = (additionalFields.customFieldsUi as IDataObject).customFieldValues as IDataObject[]; + if (customFieldValues) { + body.customFieldValues = customFieldValues; + for (let i = 0; i < customFieldValues.length; i++) { + if (!Array.isArray(customFieldValues[i].value)) { + customFieldValues[i].value = [customFieldValues[i].value]; + } + } + delete body.customFieldsUi; + } + } + + responseData = await getresponseApiRequest.call(this, 'POST', '/contacts', body); + + responseData = { success: true }; + } + //https://apireference.getresponse.com/?_ga=2.160836350.2102802044.1604719933-1897033509.1604598019#operation/deleteContact + if (operation === 'delete') { + const contactId = this.getNodeParameter('contactId', i) as string; + + const options = this.getNodeParameter('options', i) as IDataObject; + + Object.assign(qs, options); + + responseData = await getresponseApiRequest.call(this, 'DELETE', `/contacts/${contactId}`, {}, qs); + + responseData = { success: true }; + } + //https://apireference.getresponse.com/?_ga=2.160836350.2102802044.1604719933-1897033509.1604598019#operation/getContactById + if (operation === 'get') { + const contactId = this.getNodeParameter('contactId', i) as string; + + const options = this.getNodeParameter('options', i) as IDataObject; + + Object.assign(qs, options); + + responseData = await getresponseApiRequest.call(this, 'GET', `/contacts/${contactId}`, {}, qs); + } + //https://apireference.getresponse.com/?_ga=2.160836350.2102802044.1604719933-1897033509.1604598019#operation/getContactList + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + const options = this.getNodeParameter('options', i) as IDataObject; + + const timezone = this.getTimezone(); + + Object.assign(qs, options); + + const isNotQuery = [ + 'sortBy', + 'sortOrder', + 'additionalFlags', + 'fields', + 'exactMatch', + ]; + + const isDate = [ + 'createdOnFrom', + 'createdOnTo', + 'changeOnFrom', + 'changeOnTo', + ]; + + const dateMapToKey: { [key: string]: string; } = { + 'createdOnFrom': '[createdOn][from]', + 'createdOnTo': '[createdOn][to]', + 'changeOnFrom': '[changeOn][from]', + 'changeOnTo': '[changeOn][to]', + }; + + for (const key of Object.keys(qs)) { + if (!isNotQuery.includes(key)) { + if (isDate.includes(key)) { + qs[`query${dateMapToKey[key]}`] = moment.tz(qs[key], timezone).format('YYYY-MM-DDTHH:mm:ssZZ'); + } else { + qs[`query[${key}]`] = qs[key]; + } + delete qs[key]; + } + } + + if (qs.sortBy) { + qs[`sort[${qs.sortBy}]`] = qs.sortOrder || 'ASC'; + } + + if (qs.exactMatch === true) { + qs['additionalFlags'] = 'exactMatch'; + delete qs.exactMatch; + } + + if (returnAll) { + responseData = await getResponseApiRequestAllItems.call(this, 'GET', `/contacts`, {}, qs); + } else { + qs.perPage = this.getNodeParameter('limit', i) as number; + responseData = await getresponseApiRequest.call(this, 'GET', `/contacts`, {}, qs); + } + } + //https://apireference.getresponse.com/?_ga=2.160836350.2102802044.1604719933-1897033509.1604598019#operation/updateContact + if (operation === 'update') { + + const contactId = this.getNodeParameter('contactId', i) as string; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + const body: IDataObject = {}; + + Object.assign(body, updateFields); + + if (updateFields.customFieldsUi) { + const customFieldValues = (updateFields.customFieldsUi as IDataObject).customFieldValues as IDataObject[]; + if (customFieldValues) { + body.customFieldValues = customFieldValues; + delete body.customFieldsUi; + } + } + + responseData = await getresponseApiRequest.call(this, 'POST', `/contacts/${contactId}`, body); + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/GetResponse/getResponse.png b/packages/nodes-base/nodes/GetResponse/getResponse.png new file mode 100644 index 0000000000000000000000000000000000000000..78a1717949755d09a00ea979daf7b76bbb9ea65b GIT binary patch literal 2851 zcmY*bdpy%^8~@Gbkh96z#KO>QOG(zqY)(mXDhfH|Y+t1pTAPH>g@GFiDl04Fg1vmOwnWok>D-+GoD>og zh()8r!^2VG2T;^tU-W)`eSI`W2d$%{&1GoQA_79(>DmFby?=}RSBL0L^9&{jhLEWN z$~(I59@Nkf12}xA(C_j0IYY=k|5OT~{c4NbAbRHsdOr$-{;kbT#qC6~*1=?NZsi?) z!~M9Q%>Rr1s)IxC6#t*Y{N3r#D7RHZK^*$`Ycmu)-h^=Ce*CuAVOQtF5L@WTt{nZ9&W<|=rYfzX z8eq4Ai0eTK4)|2i^{^W+af&N}3@2sjH#VPo1P<>PGs{wKllM@%u9IZSPL8N8vhXL3 zN@*WtCPjoP&Q6)lvoz_+3*9t1x4`jnATQf5Xz3DC&S>^-Vy43U{RWUVe{=OAqgl)t zsIEI8{pkZ2C7j>b8w1}`IL22a8kJ65fkS}zN;hCEoi3_LuvwI(%HRn?}|YE6PTxpj!#q@yHzTSTH3Xd-;+r?XB_&9pW2`|kYY13dZBGU zB9g1L-fFAFnfU6lMcj_#XDb~W|11wwyAn^98$Wk%*9RUJKX|}!(8!NPWBRFNj7~Xb zP(Qv+b!kfk`T1P125M{S2uqr-k^>WRs5=>o2(@Vsw&W^FrTpl>U4j1csLYG3p8VeY zX8WUg?ritT0q}Lp>UnphPkKS^oYtLP&ldE|s;CFW*ZSN(yj*{LDfx6(xBi4}VC zN8q47yCAO#m+-+ttXrAqg9o99F)Cf^^;4epP4x<864H@I>}qh9brut)jGxb%%*eHT z9%nSSj0}Z{$xQo3A2OY3Uk*CtdB21p_-K%^L9%U$g&6o&5kw7dX%sr{%eht{g^5a? z%ca>MPQ_c$HRF^vKdHC9Ou<2^Gv+Q9!rpKGJ?=9=%h!ftjwo;-)=FAbsSQUlpFIXv z7t|0o4IP$g#*Sf1!d`7Ko6%3b=PQ!jrjM01-Yu5Aj#_*5Fe%l)?8%;)e$Zse=C>jI zz{wt@+%5w_F&!I?6tSc0Tki2QW_k98jkK3(o9 zp>tk@O1NHnBZ)96y|`TVq2D*uWBh%uo~UUFKF%dtUICS3tmP<_^A}7O43b-Xx;nvLKZbc z>hq)f^k}YhHpW#Q-{s}p3bbvEwsMxLC;_*S=Al~T)q+rtQ|QKwbN;i_yVJfC>vtQ= zJQ_i`9h-=)lb0+vo&FB-(;=MYy=4}Wn*pY(L5H7A-Svv(y#*V5!$M**A&VmdBYZgi zDv*%;9xt`gs@QG8Sda3quP-=7#S8a1!6`$%@hg6n%dni87d2pW+sY=8{z+yFr2U5> z$nzRB4Sd;BydrW%aaswqQ1tsRVMDDq zum-Spgp@UEkCmWC6lrrh%&?pWtAnOh``%S;_D-*#wi_zFEw=#X`SiyhjX%^s66EX_ z6Hebww9C^DoHmJvTI&l+sysGg7;$mQUDf`t4J{D8tP~ z8<~{s-AAdTYLa=y%?&l9UoX^MYvygvW`>xDrs|9cFl4;bDyIsK)?S}rDyB3FJitjy zL56sG>$?uUdeNAwdp5PJfoqoC?Iqf26YsY}QC&=H;mdpKjAE1gwi^zxhuJUF*)W4cc; z_C{iNz5&~>>FWw%kajntz;kun|Y!CN(5dMYa=u2AER zoz&gUum6|ZLyK~|6|?AFSC9;gDCiW+XVUiLwfO#_CyZ*X7tXSq=KvE*5xvu0aS$yc z#L6g+eay5bddf8>Hvwng8yUwj>dFH@^tK4EpS#iPCD+|C!iSJzPJ?W@`xa96=a@>c73S+B3W{_ z@yYfAE6%^QTyS$uS2h2D171I2*8aIfC3#Wf2jjHU{m&64%_pbxK(m}OL9cQC_@|8N z5|~mM<`lfHgGjl{i!N+h`3`MsOP?An{VU4(I&>W(L`6vmCZzd$5v~?9h0{Wn^1Vr4 zH~Ac`{T!|&-B5L2C4pmxJ@`D3rIvdEF#mpHpq1H0#2CjmdsFkP1JtSG^jc)?laU90 zwN<@`OKt5>?dv#!X4eJ}7q91(dEX6UJbo=PPo8O!l*1fBBM? Date: Tue, 10 Nov 2020 18:32:19 +0100 Subject: [PATCH 11/42] :zap: Minor improvement to GetResponse Node --- .../nodes/GetResponse/ContactDescription.ts | 4 ++-- .../nodes/GetResponse/GenericFunctions.ts | 5 ++--- .../nodes/GetResponse/GetResponse.node.ts | 10 +++++----- .../nodes/GetResponse/getResponse.png | Bin 2851 -> 750 bytes 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/nodes-base/nodes/GetResponse/ContactDescription.ts b/packages/nodes-base/nodes/GetResponse/ContactDescription.ts index ba173edde4..0a8e60cdfe 100644 --- a/packages/nodes-base/nodes/GetResponse/ContactDescription.ts +++ b/packages/nodes-base/nodes/GetResponse/ContactDescription.ts @@ -241,6 +241,7 @@ export const contactFields = [ }, ], }, + /* -------------------------------------------------------------------------- */ /* contact:get */ /* -------------------------------------------------------------------------- */ @@ -288,6 +289,7 @@ export const contactFields = [ }, ], }, + /* -------------------------------------------------------------------------- */ /* contact:getAll */ /* -------------------------------------------------------------------------- */ @@ -551,7 +553,6 @@ export const contactFields = [ loadOptionsMethod: 'getCampaigns', }, default: '', - description: '', }, { displayName: 'Custom Fields', @@ -600,7 +601,6 @@ export const contactFields = [ name: 'email', type: 'string', default: '', - description: '', }, { displayName: 'IP Address', diff --git a/packages/nodes-base/nodes/GetResponse/GenericFunctions.ts b/packages/nodes-base/nodes/GetResponse/GenericFunctions.ts index eaae8d4df1..f07d130da4 100644 --- a/packages/nodes-base/nodes/GetResponse/GenericFunctions.ts +++ b/packages/nodes-base/nodes/GetResponse/GenericFunctions.ts @@ -1,6 +1,6 @@ import { OptionsWithUri, - } from 'request'; +} from 'request'; import { IExecuteFunctions, @@ -41,7 +41,7 @@ export async function getresponseApiRequest(this: IWebhookFunctions | IHookFunct } else { //@ts-ignore return await this.helpers.requestOAuth2.call(this, 'getResponseOAuth2Api', options); - } + } } catch (error) { if (error.response && error.response.body && error.response.body.message) { // Try to return the error prettier @@ -68,4 +68,3 @@ export async function getResponseApiRequestAllItems(this: IExecuteFunctions | IL return returnData; } - diff --git a/packages/nodes-base/nodes/GetResponse/GetResponse.node.ts b/packages/nodes-base/nodes/GetResponse/GetResponse.node.ts index 01b069b79c..09e772e1ac 100644 --- a/packages/nodes-base/nodes/GetResponse/GetResponse.node.ts +++ b/packages/nodes-base/nodes/GetResponse/GetResponse.node.ts @@ -47,7 +47,7 @@ export class GetResponse implements INodeType { authentication: [ 'apiKey', ], - }, + }, }, }, { @@ -97,7 +97,7 @@ export class GetResponse implements INodeType { ...contactFields, ], }; - + methods = { loadOptions: { // Get all the campaigns to display them to user so that he can @@ -252,7 +252,7 @@ export class GetResponse implements INodeType { 'changeOnTo', ]; - const dateMapToKey: { [key: string]: string; } = { + const dateMapToKey: { [key: string]: string; } = { 'createdOnFrom': '[createdOn][from]', 'createdOnTo': '[createdOn][to]', 'changeOnFrom': '[changeOn][from]', @@ -264,7 +264,7 @@ export class GetResponse implements INodeType { if (isDate.includes(key)) { qs[`query${dateMapToKey[key]}`] = moment.tz(qs[key], timezone).format('YYYY-MM-DDTHH:mm:ssZZ'); } else { - qs[`query[${key}]`] = qs[key]; + qs[`query[${key}]`] = qs[key]; } delete qs[key]; } @@ -310,7 +310,7 @@ export class GetResponse implements INodeType { } if (Array.isArray(responseData)) { returnData.push.apply(returnData, responseData as IDataObject[]); - + } else if (responseData !== undefined) { returnData.push(responseData as IDataObject); } diff --git a/packages/nodes-base/nodes/GetResponse/getResponse.png b/packages/nodes-base/nodes/GetResponse/getResponse.png index 78a1717949755d09a00ea979daf7b76bbb9ea65b..f533a87a64ce9a996f1928503f56eef39e3b1a8b 100644 GIT binary patch delta 737 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 Date: Tue, 10 Nov 2020 13:08:48 -0500 Subject: [PATCH 12/42] :sparkles: Add Line-Node (#1137) * :sparkles: Line-Node * :zap: Improvements --- .../LineNotifyOAuth2Api.credentials.ts | 47 +++++ .../nodes-base/nodes/Line/GenericFunctions.ts | 50 +++++ packages/nodes-base/nodes/Line/Line.node.ts | 144 ++++++++++++++ .../nodes/Line/NotificationDescription.ts | 176 ++++++++++++++++++ packages/nodes-base/nodes/Line/line.png | Bin 0 -> 4529 bytes packages/nodes-base/package.json | 2 + 6 files changed, 419 insertions(+) create mode 100644 packages/nodes-base/credentials/LineNotifyOAuth2Api.credentials.ts create mode 100644 packages/nodes-base/nodes/Line/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Line/Line.node.ts create mode 100644 packages/nodes-base/nodes/Line/NotificationDescription.ts create mode 100644 packages/nodes-base/nodes/Line/line.png diff --git a/packages/nodes-base/credentials/LineNotifyOAuth2Api.credentials.ts b/packages/nodes-base/credentials/LineNotifyOAuth2Api.credentials.ts new file mode 100644 index 0000000000..f97af2e872 --- /dev/null +++ b/packages/nodes-base/credentials/LineNotifyOAuth2Api.credentials.ts @@ -0,0 +1,47 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class LineNotifyOAuth2Api implements ICredentialType { + name = 'lineNotifyOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Line Notify OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://notify-bot.line.me/oauth/authorize', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://notify-bot.line.me/oauth/token', + required: true, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: 'notify', + required: true, + }, + { + 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/Line/GenericFunctions.ts b/packages/nodes-base/nodes/Line/GenericFunctions.ts new file mode 100644 index 0000000000..cc95fadd37 --- /dev/null +++ b/packages/nodes-base/nodes/Line/GenericFunctions.ts @@ -0,0 +1,50 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function lineApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IHookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + + let options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: uri || ``, + json: true, + }; + options = Object.assign({}, options, option); + + try { + if (Object.keys(body).length === 0) { + delete options.body; + } + + //@ts-ignore + return await this.helpers.requestOAuth2.call(this, 'lineNotifyOAuth2Api', options, { tokenType: 'Bearer' }); + + } catch (error) { + + let errorMessage; + + if (error.response && error.response.body && error.response.body.message) { + + errorMessage = error.response.body.message; + + throw new Error(`Line error response [${error.statusCode}]: ${errorMessage}`); + } + throw error; + } +} diff --git a/packages/nodes-base/nodes/Line/Line.node.ts b/packages/nodes-base/nodes/Line/Line.node.ts new file mode 100644 index 0000000000..970429a17f --- /dev/null +++ b/packages/nodes-base/nodes/Line/Line.node.ts @@ -0,0 +1,144 @@ +import { + BINARY_ENCODING, + IExecuteFunctions, +} from 'n8n-core'; + +import { + IBinaryKeyData, + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + lineApiRequest, +} from './GenericFunctions'; + +import { + notificationFields, + notificationOperations, +} from './NotificationDescription'; + +export class Line implements INodeType { + description: INodeTypeDescription = { + displayName: 'Line', + name: 'line', + icon: 'file:line.png', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Line API.', + defaults: { + name: 'Line', + color: '#00b900', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'lineNotifyOAuth2Api', + required: true, + displayOptions: { + show: { + resource: [ + 'notification', + ], + }, + }, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Notification', + value: 'notification', + }, + ], + default: 'notification', + description: 'The resource to operate on.', + }, + ...notificationOperations, + ...notificationFields, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = (items.length as unknown) as number; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < length; i++) { + + if (resource === 'notification') { + //https://notify-bot.line.me/doc/en/ + if (operation === 'send') { + const message = this.getNodeParameter('message', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = { + message, + }; + + Object.assign(body, additionalFields); + + if (body.hasOwnProperty('notificationDisabled')) { + body.notificationDisabled = (body.notificationDisabled) ? 'true' : 'false'; + } + + if (body.stickerUi) { + const sticker = (body.stickerUi as IDataObject).stickerValue as IDataObject; + if (sticker) { + body.stickerId = sticker.stickerId; + body.stickerPackageId = sticker.stickerPackageId; + } + delete body.stickerUi; + } + + if (body.imageUi) { + const image = (body.imageUi as IDataObject).imageValue as IDataObject; + + if (image && image.binaryData === true) { + if (items[i].binary === undefined) { + throw new Error('No binary data exists on item!'); + } + //@ts-ignore + if (items[i].binary[image.binaryProperty] === undefined) { + throw new Error(`No binary data property "${image.binaryProperty}" does not exists on item!`); + } + + const binaryData = (items[i].binary as IBinaryKeyData)[image.binaryProperty as string]; + + body.imageFile = { + value: Buffer.from(binaryData.data, BINARY_ENCODING), + options: { + filename: binaryData.fileName, + }, + }; + } else { + body.imageFullsize = image.imageFullsize; + body.imageThumbnail = image.imageThumbnail; + } + delete body.imageUi; + } + responseData = await lineApiRequest.call(this, 'POST', '', {}, {}, 'https://notify-api.line.me/api/notify', { formData: body }); + } + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Line/NotificationDescription.ts b/packages/nodes-base/nodes/Line/NotificationDescription.ts new file mode 100644 index 0000000000..bcc053499f --- /dev/null +++ b/packages/nodes-base/nodes/Line/NotificationDescription.ts @@ -0,0 +1,176 @@ +import { + INodeProperties, + } from 'n8n-workflow'; + +export const notificationOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'notification', + ], + }, + }, + options: [ + { + name: 'Send', + value: 'send', + description: 'Sends notifications to users or groups', + }, + ], + default: 'send', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const notificationFields = [ + +/* -------------------------------------------------------------------------- */ +/* notification:send */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Message', + name: 'message', + required: true, + type: 'string', + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'notification', + ], + }, + }, + default: '', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'notification', + ], + }, + }, + options: [ + { + displayName: 'Image', + name: 'imageUi', + placeholder: 'Add Image', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + default: {}, + options: [ + { + name: 'imageValue', + displayName: 'image', + values: [ + { + displayName: 'Binary Data', + name: 'binaryData', + type: 'boolean', + default: false, + }, + { + displayName: 'Image Full Size', + name: 'imageFullsize', + type: 'string', + default: '', + displayOptions: { + show: { + binaryData: [ + false, + ], + }, + }, + description: 'HTTP/HTTPS URL. Maximum size of 2048×2048px JPEG', + }, + { + displayName: 'Image Thumbnail', + name: 'imageThumbnail', + type: 'string', + displayOptions: { + show: { + binaryData: [ + false, + ], + }, + }, + default: '', + description: 'HTTP/HTTPS URL. Maximum size of 240×240px JPEG', + }, + { + displayName: 'Binary Property', + name: 'binaryProperty', + type: 'string', + displayOptions: { + show: { + binaryData: [ + true, + ], + }, + }, + default: 'data', + description: `Name of the property that holds the binary data.
`, + }, + ], + }, + ], + }, + { + displayName: 'Notification Disabled', + name: 'notificationDisabled', + type: 'boolean', + default: false, + description: `true: The user doesn't receive a push notification when the message is sent.
+ false: The user receives a push notification when the message is sent`, + }, + { + displayName: 'Sticker', + name: 'stickerUi', + placeholder: 'Add Sticker', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + default: {}, + options: [ + { + name: 'stickerValue', + displayName: 'Sticker', + values: [ + { + displayName: 'Sticker ID', + name: 'stickerId', + type: 'number', + default: '', + description: 'Sticker ID', + }, + { + displayName: 'Sticker Package ID', + name: 'stickerPackageId', + type: 'number', + default: '', + description: 'Package ID', + }, + ], + }, + ], + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Line/line.png b/packages/nodes-base/nodes/Line/line.png new file mode 100644 index 0000000000000000000000000000000000000000..41753ce3104a2c3a6cd50a9c5caedbab6281b393 GIT binary patch literal 4529 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 literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index b88362d29f..c695f8c001 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -107,6 +107,7 @@ "dist/credentials/JotFormApi.credentials.js", "dist/credentials/Kafka.credentials.js", "dist/credentials/KeapOAuth2Api.credentials.js", + "dist/credentials/LineNotifyOAuth2Api.credentials.js", "dist/credentials/LinkedInOAuth2Api.credentials.js", "dist/credentials/MailerLiteApi.credentials.js", "dist/credentials/MailchimpApi.credentials.js", @@ -315,6 +316,7 @@ "dist/nodes/Kafka/Kafka.node.js", "dist/nodes/Keap/Keap.node.js", "dist/nodes/Keap/KeapTrigger.node.js", + "dist/nodes/Line/Line.node.js", "dist/nodes/LinkedIn/LinkedIn.node.js", "dist/nodes/MailerLite/MailerLite.node.js", "dist/nodes/MailerLite/MailerLiteTrigger.node.js", From 6125dee8f16fcd540d7325ef8861b21b3bba0100 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Tue, 10 Nov 2020 19:09:13 +0100 Subject: [PATCH 13/42] :zap: Minor improvements to Line-Node --- packages/nodes-base/nodes/Line/Line.node.ts | 6 +++--- .../nodes/Line/NotificationDescription.ts | 8 ++++---- packages/nodes-base/nodes/Line/line.png | Bin 4529 -> 1346 bytes 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/nodes-base/nodes/Line/Line.node.ts b/packages/nodes-base/nodes/Line/Line.node.ts index 970429a17f..8c50baa354 100644 --- a/packages/nodes-base/nodes/Line/Line.node.ts +++ b/packages/nodes-base/nodes/Line/Line.node.ts @@ -102,7 +102,7 @@ export class Line implements INodeType { } delete body.stickerUi; } - + if (body.imageUi) { const image = (body.imageUi as IDataObject).imageValue as IDataObject; @@ -126,8 +126,8 @@ export class Line implements INodeType { } else { body.imageFullsize = image.imageFullsize; body.imageThumbnail = image.imageThumbnail; - } - delete body.imageUi; + } + delete body.imageUi; } responseData = await lineApiRequest.call(this, 'POST', '', {}, {}, 'https://notify-api.line.me/api/notify', { formData: body }); } diff --git a/packages/nodes-base/nodes/Line/NotificationDescription.ts b/packages/nodes-base/nodes/Line/NotificationDescription.ts index bcc053499f..f26dd68068 100644 --- a/packages/nodes-base/nodes/Line/NotificationDescription.ts +++ b/packages/nodes-base/nodes/Line/NotificationDescription.ts @@ -1,6 +1,6 @@ import { INodeProperties, - } from 'n8n-workflow'; +} from 'n8n-workflow'; export const notificationOperations = [ { @@ -28,9 +28,9 @@ export const notificationOperations = [ export const notificationFields = [ -/* -------------------------------------------------------------------------- */ -/* notification:send */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* notification:send */ + /* -------------------------------------------------------------------------- */ { displayName: 'Message', name: 'message', diff --git a/packages/nodes-base/nodes/Line/line.png b/packages/nodes-base/nodes/Line/line.png index 41753ce3104a2c3a6cd50a9c5caedbab6281b393..da40b65f5a6e7ca35ebeef43b04013381ea5399d 100644 GIT binary patch delta 1338 zcmV-A1;zTYBf<)h8Gi!+000dlDL?=K0QOK!R7C)}006lF00000x&Q#V006lF0J#7F zxc~sa009300J#7Fx&Q#V006lF0Js1Exc~sU0RR900Kx$M{`~;6006cC^!oH9yCeX( z006}RAi5v`umH~J&i4EECc7rm>e3v#90s`t0IdKSx*5Xb!hfCBofNth>GkQS*{5N| zVG_9#54jKB@7)Wz3jn$R`26_P>(rOimsrABGQ2VX$^Zbr00620;_>3R-?*^cuvEfS zKfXWg_Uz{K=GW}kht7v|$#g@%Lpr@W$>zzE(vyMAfqct+X2oVsz)s`x{7|++3moISPVuh$3LsYOS{a|Ic)G6KV;Gfa8-u z;LFZz_T}wpI>oN8?`-d-x7LbuW_w2x3c_D>{b=jbwtus0hHYPN9V({DmjgSiX(?^} z$lCi-%d|5Wn)Z!t5Z|=aMMkE*tM6ZYWgExWivxXE(+}+w`$TB=5v9GA)jox@Te`M$ znN)ARTy&gbxtUb=<&L&(#|+#onobE30T8XaX_iZId2ef(<&yn8BWArlKHT3=&PMVQ zzFB9HNPpRTS~^;?Zz5QeVY`wOoI)5kUiFbqMY)zuYmxaXSAS^_oMoJcfwZgjcQB*a$BuZvsp>d? zFJDCvs%%92V1^qque(;wbqbs#$J6n;pfJ<{S@He5$xmB zGX8mddk@|I<8i^jI=FEB7~s1IE@I<9Eav#!aPUl~jjTJHvT#yQBG?3Pj!!wod}Qu{ zRfDi*5#Hz_n335YhS+^5c-|fVt?(~Sg@2C-GaQTyUO&LM;6&1X2v@F)t_!`JyIzM^ zd$)A%6lwer}AvDemroN z*0#!14_J)gX1KVQLqtBcY2XN^p!C9XR@qrzq<+FH98c#U!BwIkM%#tuG zGPtcsE@CLvOEwv^rU}*yadm-+twPOb$ylXc0_&G&gyP*A_h2^J3d~Ao3EocYcU&l1nLxSB*2Zl9bdmPik|}S7!zb#~ z1en9(aXJattr^X^QANx*Su9R#drAA&8cvPe^9X8A{P!t4qqdLnQL;KCz6-whg+)TRxJMh!~X8Z+%vYqWl( wWNUN~wnn56H*Ly}6nk$6GwZg4d;i+Oza!@d7&Lft>i_@%07*qoM6N<$f_d$gKL7v# literal 4529 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 From 13e3efe3129539bb088c037f6626fe6362030f12 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Tue, 10 Nov 2020 13:15:56 -0500 Subject: [PATCH 14/42] :sparkles: Add Gotify-Node (#1135) --- .../credentials/GotifyApi.credentials.ts | 31 +++ .../nodes/Gotify/GenericFunctions.ts | 68 +++++ .../nodes-base/nodes/Gotify/Gotify.node.ts | 262 ++++++++++++++++++ packages/nodes-base/nodes/Gotify/gotify.png | Bin 0 -> 6631 bytes packages/nodes-base/package.json | 2 + 5 files changed, 363 insertions(+) create mode 100644 packages/nodes-base/credentials/GotifyApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Gotify/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Gotify/Gotify.node.ts create mode 100644 packages/nodes-base/nodes/Gotify/gotify.png diff --git a/packages/nodes-base/credentials/GotifyApi.credentials.ts b/packages/nodes-base/credentials/GotifyApi.credentials.ts new file mode 100644 index 0000000000..2205ea9176 --- /dev/null +++ b/packages/nodes-base/credentials/GotifyApi.credentials.ts @@ -0,0 +1,31 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class GotifyApi implements ICredentialType { + name = 'gotifyApi'; + displayName = 'Gotify API'; + properties = [ + { + displayName: 'APP API Token', + name: 'appApiToken', + type: 'string' as NodePropertyTypes, + default: '', + description: '(Optional) Needed for message creation', + }, + { + displayName: 'Client API Token', + name: 'clientApiToken', + type: 'string' as NodePropertyTypes, + default: '', + description: '(Optional) Needed for everything (delete, getAll) but message creation', + }, + { + displayName: 'URL', + name: 'url', + type: 'string' as NodePropertyTypes, + default: `Host's URL`, + }, + ]; +} diff --git a/packages/nodes-base/nodes/Gotify/GenericFunctions.ts b/packages/nodes-base/nodes/Gotify/GenericFunctions.ts new file mode 100644 index 0000000000..b9655dc4ed --- /dev/null +++ b/packages/nodes-base/nodes/Gotify/GenericFunctions.ts @@ -0,0 +1,68 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function gotifyApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, path: string, body: any = {}, qs: IDataObject = {}, uri?: string | undefined, option = {}): Promise { // tslint:disable-line:no-any + + const credentials = this.getCredentials('gotifyApi') as IDataObject; + + const options: OptionsWithUri = { + method, + headers: { + 'X-Gotify-Key': (method === 'POST') ? credentials.appApiToken : credentials.clientApiToken, + accept: 'application/json', + }, + body, + qs, + uri: uri || `${credentials.url}${path}`, + json: true, + }; + try { + if (Object.keys(body).length === 0) { + delete options.body; + } + + //@ts-ignore + return await this.helpers.request.call(this, options); + } catch (error) { + + if (error.response && error.response.body && error.response.body.errorDescription) { + const message = error.response.body.errorDescription; + // Try to return the error prettier + throw new Error( + `Gotify error response [${error.statusCode}]: ${message}`, + ); + } + throw error; + } +} + +export async function gotifyApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + let uri: string | undefined; + query.limit = 100; + do { + responseData = await gotifyApiRequest.call(this, method, endpoint, body, query, uri); + if (responseData.paging.next) { + uri = responseData.paging.next; + } + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData.paging.next + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Gotify/Gotify.node.ts b/packages/nodes-base/nodes/Gotify/Gotify.node.ts new file mode 100644 index 0000000000..a2cb407484 --- /dev/null +++ b/packages/nodes-base/nodes/Gotify/Gotify.node.ts @@ -0,0 +1,262 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + gotifyApiRequest, + gotifyApiRequestAllItems, +} from './GenericFunctions'; + +export class Gotify implements INodeType { + description: INodeTypeDescription = { + displayName: 'Gotify', + name: 'gotify', + icon: 'file:gotify.png', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Gotify API.', + defaults: { + name: 'Gotify', + color: '#71c8ec', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'gotifyApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Message', + value: 'message', + }, + ], + default: 'message', + description: 'The resource to operate on.', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'message', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Delete', + value: 'delete', + }, + { + name: 'Get All', + value: 'getAll', + }, + ], + default: 'create', + description: 'The resource to operate on.', + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: `The message. Markdown (excluding html) is allowed.`, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'create', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Priority', + name: 'priority', + type: 'number', + default: 1, + description: 'The priority of the message.', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: `The title of the message.`, + }, + ], + }, + { + displayName: 'Message ID', + name: 'messageId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'delete', + ], + }, + }, + default: '', + description: `The message id.`, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'message', + ], + 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: 20, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = (items.length as unknown) as number; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < length; i++) { + if (resource === 'message') { + if (operation === 'create') { + + const message = this.getNodeParameter('message', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = { + message, + }; + + Object.assign(body, additionalFields); + + responseData = await gotifyApiRequest.call( + this, + 'POST', + `/message`, + body, + ); + } + if (operation === 'delete') { + const messageId = this.getNodeParameter('messageId', i) as string; + + responseData = await gotifyApiRequest.call( + this, + 'DELETE', + `/message/${messageId}`, + ); + responseData = { success: true }; + } + + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + if (returnAll) { + responseData = await gotifyApiRequestAllItems.call( + this, + 'messages', + 'GET', + '/message', + {}, + qs, + ); + + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await gotifyApiRequest.call( + this, + 'GET', + `/message`, + {}, + qs, + ); + responseData = responseData.messages; + } + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Gotify/gotify.png b/packages/nodes-base/nodes/Gotify/gotify.png new file mode 100644 index 0000000000000000000000000000000000000000..df58200b8dac0341af198a5b3e18e825b95ca61e GIT binary patch literal 6631 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 Y)!%dN0>n$E|M}aYAfqZ>D`_6`KjK_g{{R30 literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index c695f8c001..7fe2ff5992 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -88,6 +88,7 @@ "dist/credentials/GSuiteAdminOAuth2Api.credentials.js", "dist/credentials/GoogleTasksOAuth2Api.credentials.js", "dist/credentials/GoogleTranslateOAuth2Api.credentials.js", + "dist/credentials/GotifyApi.credentials.js", "dist/credentials/YouTubeOAuth2Api.credentials.js", "dist/credentials/GumroadApi.credentials.js", "dist/credentials/HarvestApi.credentials.js", @@ -294,6 +295,7 @@ "dist/nodes/Google/Task/GoogleTasks.node.js", "dist/nodes/Google/Translate/GoogleTranslate.node.js", "dist/nodes/Google/YouTube/YouTube.node.js", + "dist/nodes/Gotify/Gotify.node.js", "dist/nodes/GraphQL/GraphQL.node.js", "dist/nodes/Gumroad/GumroadTrigger.node.js", "dist/nodes/HackerNews/HackerNews.node.js", From 001b93151b442ff4cbe97230cdb5071ebbe0c441 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Tue, 10 Nov 2020 19:16:35 +0100 Subject: [PATCH 15/42] :zap: Minor improvements to Gotify Node --- packages/nodes-base/credentials/GotifyApi.credentials.ts | 9 +++++---- packages/nodes-base/nodes/Gotify/GenericFunctions.ts | 2 +- packages/nodes-base/nodes/Gotify/Gotify.node.ts | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/nodes-base/credentials/GotifyApi.credentials.ts b/packages/nodes-base/credentials/GotifyApi.credentials.ts index 2205ea9176..5f90e6726b 100644 --- a/packages/nodes-base/credentials/GotifyApi.credentials.ts +++ b/packages/nodes-base/credentials/GotifyApi.credentials.ts @@ -8,24 +8,25 @@ export class GotifyApi implements ICredentialType { displayName = 'Gotify API'; properties = [ { - displayName: 'APP API Token', + displayName: 'App API Token', name: 'appApiToken', type: 'string' as NodePropertyTypes, default: '', - description: '(Optional) Needed for message creation', + description: '(Optional) Needed for message creation.', }, { displayName: 'Client API Token', name: 'clientApiToken', type: 'string' as NodePropertyTypes, default: '', - description: '(Optional) Needed for everything (delete, getAll) but message creation', + description: '(Optional) Needed for everything (delete, getAll) but message creation.', }, { displayName: 'URL', name: 'url', type: 'string' as NodePropertyTypes, - default: `Host's URL`, + default: '', + description: 'The URL of the Gotify host.', }, ]; } diff --git a/packages/nodes-base/nodes/Gotify/GenericFunctions.ts b/packages/nodes-base/nodes/Gotify/GenericFunctions.ts index b9655dc4ed..ab694fe577 100644 --- a/packages/nodes-base/nodes/Gotify/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Gotify/GenericFunctions.ts @@ -35,7 +35,7 @@ export async function gotifyApiRequest(this: IExecuteFunctions | IExecuteSingleF //@ts-ignore return await this.helpers.request.call(this, options); } catch (error) { - + if (error.response && error.response.body && error.response.body.errorDescription) { const message = error.response.body.errorDescription; // Try to return the error prettier diff --git a/packages/nodes-base/nodes/Gotify/Gotify.node.ts b/packages/nodes-base/nodes/Gotify/Gotify.node.ts index a2cb407484..1c6179efc6 100644 --- a/packages/nodes-base/nodes/Gotify/Gotify.node.ts +++ b/packages/nodes-base/nodes/Gotify/Gotify.node.ts @@ -204,7 +204,7 @@ export class Gotify implements INodeType { const body: IDataObject = { message, }; - + Object.assign(body, additionalFields); responseData = await gotifyApiRequest.call( From 86d19fb11fa310f4300cc57e031b0af7fc7d941a Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Tue, 10 Nov 2020 19:17:47 +0100 Subject: [PATCH 16/42] :zap: Minor improvements to Gotify Node --- packages/nodes-base/nodes/Gotify/gotify.png | Bin 6631 -> 2752 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/packages/nodes-base/nodes/Gotify/gotify.png b/packages/nodes-base/nodes/Gotify/gotify.png index df58200b8dac0341af198a5b3e18e825b95ca61e..f38a25e88af63f073d727b831ceb8f6a505ea169 100644 GIT binary patch literal 2752 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 Y)!%dN0>n$E|M}aYAfqZ>D`_6`KjK_g{{R30 From 1630d20e399b8f9bd5f4fe53b6a67efd6749b33a Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Tue, 10 Nov 2020 21:27:20 +0100 Subject: [PATCH 17/42] :zap: Add OAuth2 authentication to Pipedrive Trigger Node --- .../nodes/Pipedrive/PipedriveTrigger.node.ts | 47 ++++++++++++++++--- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/packages/nodes-base/nodes/Pipedrive/PipedriveTrigger.node.ts b/packages/nodes-base/nodes/Pipedrive/PipedriveTrigger.node.ts index e212fb0fed..eb1b10f747 100644 --- a/packages/nodes-base/nodes/Pipedrive/PipedriveTrigger.node.ts +++ b/packages/nodes-base/nodes/Pipedrive/PipedriveTrigger.node.ts @@ -54,13 +54,31 @@ export class PipedriveTrigger implements INodeType { { name: 'pipedriveApi', required: true, + displayOptions: { + show: { + authentication: [ + 'apiToken', + ], + }, + }, + }, + { + name: 'pipedriveOAuth2Api', + required: true, + displayOptions: { + show: { + authentication: [ + 'oAuth2', + ], + }, + }, }, { name: 'httpBasicAuth', required: true, displayOptions: { show: { - authentication: [ + incomingAuthentication: [ 'basicAuth', ], }, @@ -80,6 +98,23 @@ export class PipedriveTrigger implements INodeType { displayName: 'Authentication', name: 'authentication', type: 'options', + options: [ + { + name: 'API Token', + value: 'apiToken', + }, + { + name: 'OAuth2', + value: 'oAuth2', + }, + ], + default: 'apiToken', + description: 'Method of authentication.', + }, + { + displayName: 'Incoming Authentication', + name: 'incomingAuthentication', + type: 'options', options: [ { name: 'Basic Auth', @@ -91,7 +126,7 @@ export class PipedriveTrigger implements INodeType { }, ], default: 'none', - description: 'If authentication should be activated for the webhook (makes it more scure).', + description: 'If authentication should be activated for the webhook (makes it more secure).', }, { displayName: 'Action', @@ -218,7 +253,7 @@ export class PipedriveTrigger implements INodeType { }, async create(this: IHookFunctions): Promise { const webhookUrl = this.getNodeWebhookUrl('default'); - const authentication = this.getNodeParameter('authentication', 0) as string; + const incomingAuthentication = this.getNodeParameter('incomingAuthentication', 0) as string; const eventAction = this.getNodeParameter('action') as string; const eventObject = this.getNodeParameter('object') as string; @@ -232,7 +267,7 @@ export class PipedriveTrigger implements INodeType { http_auth_password: undefined as string | undefined, }; - if (authentication === 'basicAuth') { + if (incomingAuthentication === 'basicAuth') { const httpBasicAuth = this.getCredentials('httpBasicAuth'); if (httpBasicAuth === undefined || !httpBasicAuth.user || !httpBasicAuth.password) { @@ -285,9 +320,9 @@ export class PipedriveTrigger implements INodeType { const resp = this.getResponseObject(); const realm = 'Webhook'; - const authentication = this.getNodeParameter('authentication', 0) as string; + const incomingAuthentication = this.getNodeParameter('incomingAuthentication', 0) as string; - if (authentication === 'basicAuth') { + if (incomingAuthentication === 'basicAuth') { // Basic authorization is needed to call webhook const httpBasicAuth = this.getCredentials('httpBasicAuth'); From 03a672300f5e8f7ef571982cff6e0bdc9b560cfc Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Tue, 10 Nov 2020 17:05:08 -0500 Subject: [PATCH 18/42] :zap: Add custom traits to Segment Node (#1145) --- .../nodes/Segment/IdentifyDescription.ts | 32 +++++++++++++++ .../nodes-base/nodes/Segment/Segment.node.ts | 40 +++++++++++++++++++ .../nodes/Segment/TrackDescription.ts | 32 +++++++++++++++ 3 files changed, 104 insertions(+) diff --git a/packages/nodes-base/nodes/Segment/IdentifyDescription.ts b/packages/nodes-base/nodes/Segment/IdentifyDescription.ts index 44b2c60b48..72b78fef35 100644 --- a/packages/nodes-base/nodes/Segment/IdentifyDescription.ts +++ b/packages/nodes-base/nodes/Segment/IdentifyDescription.ts @@ -262,6 +262,38 @@ export const identifyFields = [ }, ], }, + { + displayName: 'Custom Traits', + name: 'customTraitsUi', + placeholder: 'Add Custom Trait', + type: 'fixedCollection', + default: '', + typeOptions: { + multipleValues: true, + }, + options: [ + { + name: 'customTraitValues', + displayName: 'Custom Traits', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + default: '', + description: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: '', + }, + ], + }, + ], + }, ], }, ], diff --git a/packages/nodes-base/nodes/Segment/Segment.node.ts b/packages/nodes-base/nodes/Segment/Segment.node.ts index b4ad23e9c1..954193969e 100644 --- a/packages/nodes-base/nodes/Segment/Segment.node.ts +++ b/packages/nodes-base/nodes/Segment/Segment.node.ts @@ -38,6 +38,7 @@ import { } from './TrackInterface'; import * as uuid from 'uuid/v4'; +import { customerFields } from '../CustomerIo/CustomerDescription'; export class Segment implements INodeType { description: INodeTypeDescription = { @@ -170,6 +171,7 @@ export class Segment implements INodeType { if (traits.id) { body.traits!.id = traits.id as string; } + if (traits.company) { const company = (traits.company as IDataObject).companyUi as IDataObject; if (company) { @@ -384,6 +386,14 @@ export class Segment implements INodeType { if (traits.id) { body.traits!.id = traits.id as string; } + if (traits.customTraitsUi) { + const customTraits = (traits.customTraitsUi as IDataObject).customTraitValues as IDataObject[]; + if (customTraits && customTraits.length !== 0) { + for (const customTrait of customTraits) { + body.traits![customTrait.key as string] = customTrait.value; + } + } + } if (traits.company) { const company = (traits.company as IDataObject).companyUi as IDataObject; if (company) { @@ -531,6 +541,17 @@ export class Segment implements INodeType { body.integrations!.salesforce = integrations.salesforce as boolean; } } + + if (Object.keys(traits.company as IDataObject).length === 0) { + //@ts-ignore + delete body.traits.company; + } + + if (Object.keys(traits.address as IDataObject).length === 0) { + //@ts-ignore + delete body.traits.address; + } + responseData = await segmentApiRequest.call(this, 'POST', '/identify', body); } } @@ -602,6 +623,14 @@ export class Segment implements INodeType { if (traits.id) { body.traits!.id = traits.id as string; } + if (traits.customTraitsUi) { + const customTraits = (traits.customTraitsUi as IDataObject).customTraitValues as IDataObject[]; + if (customTraits && customTraits.length !== 0) { + for (const customTrait of customTraits) { + body.traits![customTrait.key as string] = customTrait.value; + } + } + } if (traits.company) { const company = (traits.company as IDataObject).companyUi as IDataObject; if (company) { @@ -760,6 +789,17 @@ export class Segment implements INodeType { body.properties!.value = properties.value as string; } } + + if (Object.keys(traits.company as IDataObject).length === 0) { + //@ts-ignore + delete body.traits.company; + } + + if (Object.keys(traits.address as IDataObject).length === 0) { + //@ts-ignore + delete body.traits.address; + } + responseData = await segmentApiRequest.call(this, 'POST', '/track', body); } //https://segment.com/docs/connections/sources/catalog/libraries/server/http-api/#page diff --git a/packages/nodes-base/nodes/Segment/TrackDescription.ts b/packages/nodes-base/nodes/Segment/TrackDescription.ts index f32b03278c..1e49c5e579 100644 --- a/packages/nodes-base/nodes/Segment/TrackDescription.ts +++ b/packages/nodes-base/nodes/Segment/TrackDescription.ts @@ -285,6 +285,38 @@ export const trackFields = [ }, ], }, + { + displayName: 'Custom Traits', + name: 'customTraitsUi', + placeholder: 'Add Custom Trait', + type: 'fixedCollection', + default: '', + typeOptions: { + multipleValues: true, + }, + options: [ + { + name: 'customTraitValues', + displayName: 'Custom Traits', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + default: '', + description: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: '', + }, + ], + }, + ], + }, ], }, ], From 10645c2ef56727f290a5308fe5d874ffb253e554 Mon Sep 17 00:00:00 2001 From: bsanderrmg <53356092+bsanderrmg@users.noreply.github.com> Date: Tue, 10 Nov 2020 17:30:03 -0500 Subject: [PATCH 19/42] :zap: TLS option for MSSQL credentials type (#1132) * Update MicrosoftSql.node.ts * Update MicrosoftSql.credentials.ts * Update MicrosoftSql.node.ts Add TLS encryption toggle to MSSQL credentials type and implement into the connection config object. This will help users connect to certain Azure MSSQL products and older MSSQL instances. --- packages/nodes-base/credentials/MicrosoftSql.credentials.ts | 6 ++++++ .../nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts | 3 +++ 2 files changed, 9 insertions(+) diff --git a/packages/nodes-base/credentials/MicrosoftSql.credentials.ts b/packages/nodes-base/credentials/MicrosoftSql.credentials.ts index 598c9ded21..1b14ec5778 100644 --- a/packages/nodes-base/credentials/MicrosoftSql.credentials.ts +++ b/packages/nodes-base/credentials/MicrosoftSql.credentials.ts @@ -44,5 +44,11 @@ export class MicrosoftSql implements ICredentialType { type: 'string' as NodePropertyTypes, default: '', }, + { + displayName: 'TLS', + name: 'tls', + type: 'boolean' as NodePropertyTypes, + default: true, + }, ]; } diff --git a/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts b/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts index 46128fa895..0761debf52 100644 --- a/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts +++ b/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts @@ -217,6 +217,9 @@ export class MicrosoftSql implements INodeType { user: credentials.user as string, password: credentials.password as string, domain: credentials.domain ? (credentials.domain as string) : undefined, + options: { + encrypt: credentials.tls as boolean + }, }; const pool = new mssql.ConnectionPool(config); From 97289a244f3237a028e5ac52627306cbb272df86 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Tue, 10 Nov 2020 23:30:40 +0100 Subject: [PATCH 20/42] :zap: Fix indentation --- packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts b/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts index 0761debf52..39516f94f1 100644 --- a/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts +++ b/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts @@ -218,8 +218,8 @@ export class MicrosoftSql implements INodeType { password: credentials.password as string, domain: credentials.domain ? (credentials.domain as string) : undefined, options: { - encrypt: credentials.tls as boolean - }, + encrypt: credentials.tls as boolean, + }, }; const pool = new mssql.ConnectionPool(config); From da5fddad5a9ec6284308fd597965de8100eb2ad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther?= Date: Wed, 11 Nov 2020 08:44:59 +0100 Subject: [PATCH 21/42] :zap: Amqp - Message Throttling - Close Connection on Send (#1127) * Azuer Service Bus * Message throttling * remove the Events the WF is desabled * close connections after send * sendable once --- packages/nodes-base/nodes/Amqp/Amqp.node.ts | 12 +++-- .../nodes-base/nodes/Amqp/AmqpTrigger.node.ts | 47 ++++++++++++++++--- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/packages/nodes-base/nodes/Amqp/Amqp.node.ts b/packages/nodes-base/nodes/Amqp/Amqp.node.ts index 6cbdcf4b4d..50f6f20a90 100644 --- a/packages/nodes-base/nodes/Amqp/Amqp.node.ts +++ b/packages/nodes-base/nodes/Amqp/Amqp.node.ts @@ -66,7 +66,7 @@ export class Amqp implements INodeType { }, ], }, - ], + ] }; async executeSingle(this: IExecuteSingleFunctions): Promise { @@ -110,7 +110,7 @@ export class Amqp implements INodeType { } const allSent = new Promise(( resolve ) => { - container.on('sendable', (context: any) => { // tslint:disable-line:no-any + container.once('sendable', (context: any) => { // tslint:disable-line:no-any let body: IDataObject | string = item.json; const sendOnlyProperty = options.sendOnlyProperty as string; @@ -125,7 +125,7 @@ export class Amqp implements INodeType { const message = { application_properties: headerProperties, - body, + body }; const sendResult = context.sender.send(message); @@ -134,10 +134,14 @@ export class Amqp implements INodeType { }); }); - container.connect(connectOptions).open_sender(sink); + const conn = container.connect(connectOptions); + const sender = conn.open_sender(sink); const sendResult: Delivery = await allSent as Delivery; // sendResult has a a property that causes circular reference if returned + sender.close(); + conn.close(); + return { json: { id: sendResult.id } } as INodeExecutionData; } } diff --git a/packages/nodes-base/nodes/Amqp/AmqpTrigger.node.ts b/packages/nodes-base/nodes/Amqp/AmqpTrigger.node.ts index 1b1e679f13..d63d785c24 100644 --- a/packages/nodes-base/nodes/Amqp/AmqpTrigger.node.ts +++ b/packages/nodes-base/nodes/Amqp/AmqpTrigger.node.ts @@ -55,6 +55,20 @@ export class AmqpTrigger implements INodeType { description: 'Leave empty for non-durable topic subscriptions or queues', }, { + displayName: 'Pull N Messages per Cicle', + name: 'pullMessagesNumber', + type: 'number', + default: 100, + description: 'Number of messages to pull from the bus for every cicle', + }, + { + displayName: 'Sleep time after cicle', + name: 'sleepTime', + type: 'number', + default: 10, + description: 'Milliseconds to sleep after every cicle', + }, + { displayName: 'Options', name: 'options', type: 'collection', @@ -84,7 +98,7 @@ export class AmqpTrigger implements INodeType { }, ], }, - ], + ] }; @@ -99,6 +113,8 @@ export class AmqpTrigger implements INodeType { const clientname = this.getNodeParameter('clientname', '') as string; const subscription = this.getNodeParameter('subscription', '') as string; const options = this.getNodeParameter('options', {}) as IDataObject; + const pullMessagesNumber = this.getNodeParameter('pullMessagesNumber', {}) as number; + const sleepTime = this.getNodeParameter('sleepTime', {}) as number; if (sink === '') { throw new Error('Queue or Topic required!'); @@ -117,7 +133,7 @@ export class AmqpTrigger implements INodeType { port: credentials.port, reconnect: true, // this id the default anyway reconnect_limit: 50, // try for max 50 times, based on a back-off algorithm - container_id: (durable ? clientname : null), + container_id: (durable ? clientname : null) }; if (credentials.username || credentials.password) { // Old rhea implementation. not shure if it is neccessary @@ -131,9 +147,15 @@ export class AmqpTrigger implements INodeType { } + let lastMsgId: number | undefined = undefined; const self = this; + container.on('receiver_open', function (context: any) { + console.log("Connection opened"); + context.receiver.add_credit(pullMessagesNumber); + }); + container.on('message', (context: any) => { // tslint:disable-line:no-any // ignore duplicate message check, don't think it's necessary, but it was in the rhea-lib example code if (context.message.message_id && context.message.message_id === lastMsgId) { @@ -142,6 +164,12 @@ export class AmqpTrigger implements INodeType { lastMsgId = context.message.message_id; let data = context.message; + + if(options.jsonConvertByteArrayToString === true && data.body.content !== undefined) { + // The buffer is not ready... Stringify and parse back to load it. + let cont = JSON.stringify(data.body.content); + data.body = String.fromCharCode.apply(null,JSON.parse(cont).data); + } if (options.jsonConvertByteArrayToString === true && data.body.content !== undefined) { // The buffer is not ready... Stringify and parse back to load it. @@ -158,6 +186,9 @@ export class AmqpTrigger implements INodeType { self.emit([self.helpers.returnJsonArray([data])]); + + if(context.receiver.credit ==0) + setTimeout(function(){ context.receiver.add_credit(pullMessagesNumber); }, sleepTime || 0); }); const connection = container.connect(connectOptions); @@ -168,16 +199,16 @@ export class AmqpTrigger implements INodeType { source: { address: sink, durable: 2, - expiry_policy: 'never', + expiry_policy: 'never' }, - credit_window: 1, // prefetch 1 + credit_window: 0 // prefetch 1 }; } else { clientOptions = { source: { address: sink, }, - credit_window: 1, // prefetch 1 + credit_window: 0 // prefetch 1 }; } connection.open_receiver(clientOptions); @@ -186,9 +217,11 @@ export class AmqpTrigger implements INodeType { // The "closeFunction" function gets called by n8n whenever // the workflow gets deactivated and can so clean up. async function closeFunction() { + container.removeAllListeners("receiver_open"); + container.removeAllListeners("message"); connection.close(); } - + // The "manualTriggerFunction" function gets called by n8n // when a user is in the workflow editor and starts the // workflow manually. @@ -198,7 +231,7 @@ export class AmqpTrigger implements INodeType { await new Promise((resolve, reject) => { const timeoutHandler = setTimeout(() => { reject(new Error('Aborted, no message received within 30secs. This 30sec timeout is only set for "manually triggered execution". Active Workflows will listen indefinitely.')); - }, 30000); + }, 3000); container.on('message', (context: any) => { // tslint:disable-line:no-any // Check if the only property present in the message is body // in which case we only emit the content of the body property From 702a8bf3bfdba7410dc22cd583d2cb03caacc61d Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 11 Nov 2020 08:45:31 +0100 Subject: [PATCH 22/42] :zap: Additional improvements to AMQP node --- packages/nodes-base/nodes/Amqp/Amqp.node.ts | 72 ++++++++++--------- .../nodes-base/nodes/Amqp/AmqpTrigger.node.ts | 69 +++++++++--------- 2 files changed, 71 insertions(+), 70 deletions(-) diff --git a/packages/nodes-base/nodes/Amqp/Amqp.node.ts b/packages/nodes-base/nodes/Amqp/Amqp.node.ts index 50f6f20a90..3fd8133711 100644 --- a/packages/nodes-base/nodes/Amqp/Amqp.node.ts +++ b/packages/nodes-base/nodes/Amqp/Amqp.node.ts @@ -1,6 +1,6 @@ import { ContainerOptions, Delivery } from 'rhea'; -import { IExecuteSingleFunctions } from 'n8n-core'; +import { IExecuteFunctions } from 'n8n-core'; import { IDataObject, INodeExecutionData, @@ -66,20 +66,18 @@ export class Amqp implements INodeType { }, ], }, - ] + ], }; - async executeSingle(this: IExecuteSingleFunctions): Promise { - const item = this.getInputData(); - + async execute(this: IExecuteFunctions): Promise < INodeExecutionData[][] > { const credentials = this.getCredentials('amqp'); if (!credentials) { throw new Error('Credentials are mandatory!'); } - const sink = this.getNodeParameter('sink', '') as string; - const applicationProperties = this.getNodeParameter('headerParametersJson', {}) as string | object; - const options = this.getNodeParameter('options', {}) as IDataObject; + const sink = this.getNodeParameter('sink', 0, '') as string; + const applicationProperties = this.getNodeParameter('headerParametersJson', 0, {}) as string | object; + const options = this.getNodeParameter('options', 0, {}) as IDataObject; let headerProperties = applicationProperties; if (typeof applicationProperties === 'string' && applicationProperties !== '') { @@ -109,39 +107,43 @@ export class Amqp implements INodeType { connectOptions.transport = credentials.transportType; } - const allSent = new Promise(( resolve ) => { - container.once('sendable', (context: any) => { // tslint:disable-line:no-any - - let body: IDataObject | string = item.json; - const sendOnlyProperty = options.sendOnlyProperty as string; - - if (sendOnlyProperty) { - body = body[sendOnlyProperty] as string; - } - - if (options.dataAsObject !== true) { - body = JSON.stringify(body); - } - - const message = { - application_properties: headerProperties, - body - }; - - const sendResult = context.sender.send(message); - - resolve(sendResult); - }); - }); - const conn = container.connect(connectOptions); const sender = conn.open_sender(sink); - const sendResult: Delivery = await allSent as Delivery; // sendResult has a a property that causes circular reference if returned + const responseData: IDataObject[] = await new Promise((resolve) => { + container.once('sendable', (context: any) => { // tslint:disable-line:no-any + const returnData = []; + + const items = this.getInputData(); + for (let i = 0; i < items.length; i++) { + const item = items[i]; + + let body: IDataObject | string = item.json; + const sendOnlyProperty = options.sendOnlyProperty as string; + + if (sendOnlyProperty) { + body = body[sendOnlyProperty] as string; + } + + if (options.dataAsObject !== true) { + body = JSON.stringify(body); + } + + const result = context.sender.send({ + application_properties: headerProperties, + body, + }); + + returnData.push({ id: result.id }); + } + + resolve(returnData); + }); + }); sender.close(); conn.close(); - return { json: { id: sendResult.id } } as INodeExecutionData; + return [this.helpers.returnJsonArray(responseData)]; } } diff --git a/packages/nodes-base/nodes/Amqp/AmqpTrigger.node.ts b/packages/nodes-base/nodes/Amqp/AmqpTrigger.node.ts index d63d785c24..40af50c4f2 100644 --- a/packages/nodes-base/nodes/Amqp/AmqpTrigger.node.ts +++ b/packages/nodes-base/nodes/Amqp/AmqpTrigger.node.ts @@ -55,20 +55,6 @@ export class AmqpTrigger implements INodeType { description: 'Leave empty for non-durable topic subscriptions or queues', }, { - displayName: 'Pull N Messages per Cicle', - name: 'pullMessagesNumber', - type: 'number', - default: 100, - description: 'Number of messages to pull from the bus for every cicle', - }, - { - displayName: 'Sleep time after cicle', - name: 'sleepTime', - type: 'number', - default: 10, - description: 'Milliseconds to sleep after every cicle', - }, - { displayName: 'Options', name: 'options', type: 'collection', @@ -96,9 +82,23 @@ export class AmqpTrigger implements INodeType { default: false, description: 'Returns only the body property.', }, + { + displayName: 'Messages per Cicle', + name: 'pullMessagesNumber', + type: 'number', + default: 100, + description: 'Number of messages to pull from the bus for every cicle', + }, + { + displayName: 'Sleep Time', + name: 'sleepTime', + type: 'number', + default: 10, + description: 'Milliseconds to sleep after every cicle.', + }, ], }, - ] + ], }; @@ -113,8 +113,7 @@ export class AmqpTrigger implements INodeType { const clientname = this.getNodeParameter('clientname', '') as string; const subscription = this.getNodeParameter('subscription', '') as string; const options = this.getNodeParameter('options', {}) as IDataObject; - const pullMessagesNumber = this.getNodeParameter('pullMessagesNumber', {}) as number; - const sleepTime = this.getNodeParameter('sleepTime', {}) as number; + const pullMessagesNumber = options.pullMessagesNumber || 100; if (sink === '') { throw new Error('Queue or Topic required!'); @@ -133,7 +132,7 @@ export class AmqpTrigger implements INodeType { port: credentials.port, reconnect: true, // this id the default anyway reconnect_limit: 50, // try for max 50 times, based on a back-off algorithm - container_id: (durable ? clientname : null) + container_id: (durable ? clientname : null), }; if (credentials.username || credentials.password) { // Old rhea implementation. not shure if it is neccessary @@ -146,13 +145,10 @@ export class AmqpTrigger implements INodeType { connectOptions.transport = credentials.transportType; } - - let lastMsgId: number | undefined = undefined; const self = this; - container.on('receiver_open', function (context: any) { - console.log("Connection opened"); + container.on('receiver_open', (context: any) => { // tslint:disable-line:no-any context.receiver.add_credit(pullMessagesNumber); }); @@ -164,11 +160,11 @@ export class AmqpTrigger implements INodeType { lastMsgId = context.message.message_id; let data = context.message; - - if(options.jsonConvertByteArrayToString === true && data.body.content !== undefined) { + + if (options.jsonConvertByteArrayToString === true && data.body.content !== undefined) { // The buffer is not ready... Stringify and parse back to load it. - let cont = JSON.stringify(data.body.content); - data.body = String.fromCharCode.apply(null,JSON.parse(cont).data); + const cont = JSON.stringify(data.body.content); + data.body = String.fromCharCode.apply(null, JSON.parse(cont).data); } if (options.jsonConvertByteArrayToString === true && data.body.content !== undefined) { @@ -187,8 +183,11 @@ export class AmqpTrigger implements INodeType { self.emit([self.helpers.returnJsonArray([data])]); - if(context.receiver.credit ==0) - setTimeout(function(){ context.receiver.add_credit(pullMessagesNumber); }, sleepTime || 0); + if (context.receiver.credit === 0) { + setTimeout(() => { + context.receiver.add_credit(pullMessagesNumber); + }, options.sleepTime as number || 10); + } }); const connection = container.connect(connectOptions); @@ -199,16 +198,16 @@ export class AmqpTrigger implements INodeType { source: { address: sink, durable: 2, - expiry_policy: 'never' + expiry_policy: 'never', }, - credit_window: 0 // prefetch 1 + credit_window: 0, // prefetch 1 }; } else { clientOptions = { source: { address: sink, }, - credit_window: 0 // prefetch 1 + credit_window: 0, // prefetch 1 }; } connection.open_receiver(clientOptions); @@ -217,11 +216,11 @@ export class AmqpTrigger implements INodeType { // The "closeFunction" function gets called by n8n whenever // the workflow gets deactivated and can so clean up. async function closeFunction() { - container.removeAllListeners("receiver_open"); - container.removeAllListeners("message"); + container.removeAllListeners('receiver_open'); + container.removeAllListeners('message'); connection.close(); } - + // The "manualTriggerFunction" function gets called by n8n // when a user is in the workflow editor and starts the // workflow manually. @@ -231,7 +230,7 @@ export class AmqpTrigger implements INodeType { await new Promise((resolve, reject) => { const timeoutHandler = setTimeout(() => { reject(new Error('Aborted, no message received within 30secs. This 30sec timeout is only set for "manually triggered execution". Active Workflows will listen indefinitely.')); - }, 3000); + }, 30000); container.on('message', (context: any) => { // tslint:disable-line:no-any // Check if the only property present in the message is body // in which case we only emit the content of the body property From a5ecf3d58d5347f9f666d9cdd55d6ad89b9ac039 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Wed, 11 Nov 2020 03:44:53 -0500 Subject: [PATCH 23/42] :sparkles: Add Strapi-Node (#1143) * :sparkles: Strapi-Node * :zap: Small improvements * :zap: Small improvement --- .../credentials/StrapiApi.credentials.ts | 29 ++ .../nodes/Strapi/EntryDescription.ts | 346 ++++++++++++++++++ .../nodes/Strapi/GenericFunctions.ts | 106 ++++++ .../nodes-base/nodes/Strapi/Strapi.node.ts | 191 ++++++++++ packages/nodes-base/nodes/Strapi/strapi.svg | 72 ++++ packages/nodes-base/package.json | 2 + 6 files changed, 746 insertions(+) create mode 100644 packages/nodes-base/credentials/StrapiApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Strapi/EntryDescription.ts create mode 100644 packages/nodes-base/nodes/Strapi/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Strapi/Strapi.node.ts create mode 100644 packages/nodes-base/nodes/Strapi/strapi.svg diff --git a/packages/nodes-base/credentials/StrapiApi.credentials.ts b/packages/nodes-base/credentials/StrapiApi.credentials.ts new file mode 100644 index 0000000000..395bbc7f9d --- /dev/null +++ b/packages/nodes-base/credentials/StrapiApi.credentials.ts @@ -0,0 +1,29 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class StrapiApi implements ICredentialType { + name = 'strapiApi'; + displayName = 'Strapi API'; + properties = [ + { + displayName: 'Email', + name: 'email', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Password', + name: 'password', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'URL', + name: 'url', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Strapi/EntryDescription.ts b/packages/nodes-base/nodes/Strapi/EntryDescription.ts new file mode 100644 index 0000000000..293304a929 --- /dev/null +++ b/packages/nodes-base/nodes/Strapi/EntryDescription.ts @@ -0,0 +1,346 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const entryOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'entry', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create an entry', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an entry', + }, + { + name: 'Get', + value: 'get', + description: 'Get an entry', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all entries', + }, + { + name: 'Update', + value: 'update', + description: 'Update an entry', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const entryFields = [ + /* -------------------------------------------------------------------------- */ + /* entry:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Content Type', + name: 'contentType', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'entry', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Name of the content type', + }, + { + displayName: 'Columns', + name: 'columns', + type: 'string', + displayOptions: { + show: { + resource: [ + 'entry', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + placeholder: 'id,name,description', + description: 'Comma separated list of the properties which should used as columns for the new rows.', + }, + /* -------------------------------------------------------------------------- */ + /* entry:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Content Type', + name: 'contentType', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'entry', + ], + operation: [ + 'delete', + ], + }, + }, + description: 'Name of the content type', + }, + { + displayName: 'Entry ID', + name: 'entryId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'entry', + ], + operation: [ + 'delete', + ], + }, + }, + description: 'The ID of the entry to get.', + }, + /* -------------------------------------------------------------------------- */ + /* entry:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Content Type', + name: 'contentType', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'entry', + ], + operation: [ + 'get', + ], + }, + }, + description: 'Name of the content type', + }, + { + displayName: 'Entry ID', + name: 'entryId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'entry', + ], + operation: [ + 'get', + ], + }, + }, + description: 'The ID of the entry to get.', + }, + /* -------------------------------------------------------------------------- */ + /* entry:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Content Type', + name: 'contentType', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'entry', + ], + operation: [ + 'getAll', + ], + }, + }, + description: 'Name of the content type', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'entry', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'Returns a list of your user contacts.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'entry', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'entry', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Publication State', + name: 'publicationState', + type: 'options', + options: [ + { + name: 'Live', + value: 'live', + }, + { + name: 'Preview', + value: 'preview', + }, + ], + default: '', + description: 'Only select entries matching the publication state provided.', + }, + { + displayName: 'Sort Fields', + name: 'sort', + type: 'string', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add Sort Field', + }, + default: '', + placeholder: 'name:asc', + description: `Name of the fields to sort the data by. By default will be sorted ascendingly.
+ To modify that behavior, you have to add the sort direction after the name of sort field preceded by a colon. + For example: name:asc`, + }, + { + displayName: 'Where (JSON)', + name: 'where', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'JSON query to filter the data. Info', + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* entry:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Content Type', + name: 'contentType', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'entry', + ], + operation: [ + 'update', + ], + }, + }, + description: 'Name of the content type', + }, + { + displayName: 'Update Key', + name: 'updateKey', + type: 'string', + displayOptions: { + show: { + resource: [ + 'entry', + ], + operation: [ + 'update', + ], + }, + }, + default: 'id', + required: true, + description: 'Name of the property which decides which rows in the database should be updated. Normally that would be "id".', + }, + { + displayName: 'Columns', + name: 'columns', + type: 'string', + displayOptions: { + show: { + resource: [ + 'entry', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + placeholder: 'id,name,description', + description: 'Comma separated list of the properties which should used as columns for the new rows.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Strapi/GenericFunctions.ts b/packages/nodes-base/nodes/Strapi/GenericFunctions.ts new file mode 100644 index 0000000000..6431ad0bd9 --- /dev/null +++ b/packages/nodes-base/nodes/Strapi/GenericFunctions.ts @@ -0,0 +1,106 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function strapiApiRequest(this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions | IWebhookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, headers: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const credentials = this.getCredentials('strapiApi') as IDataObject; + + try { + const options: OptionsWithUri = { + headers: { + 'Authorization': `Bearer ${qs.jwt}`, + }, + method, + body, + qs, + uri: uri || `${credentials.url}${resource}`, + json: true, + }; + if (Object.keys(headers).length !== 0) { + options.headers = Object.assign({}, options.headers, headers); + } + if (Object.keys(body).length === 0) { + delete options.body; + } + delete qs.jwt; + + //@ts-ignore + return await this.helpers?.request(options); + } catch (error) { + if (error.response && error.response.body && error.response.body.message) { + + let messages = error.response.body.message; + + if (Array.isArray(error.response.body.message)) { + messages = messages[0].messages.map((e: IDataObject) => e.message).join('|'); + } + // Try to return the error prettier + throw new Error( + `Strapi error response [${error.statusCode}]: ${messages}`, + ); + } + throw error; + } +} + +export async function getToken(this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions | IWebhookFunctions): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('strapiApi') as IDataObject; + + const options: OptionsWithUri = { + headers: { + 'content-type': `application/json`, + }, + method: 'POST', + uri: `${credentials.url}/auth/local`, + body: { + identifier: credentials.email, + password: credentials.password, + }, + json: true, + }; + + return this.helpers.request!(options); +} + +export async function strapiApiRequestAllItems(this: IHookFunctions | ILoadOptionsFunctions | IExecuteFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + query._limit = 20; + + query._start = 0; + + do { + responseData = await strapiApiRequest.call(this, method, resource, body, query); + query._start += query._limit; + returnData.push.apply(returnData, responseData); + } while ( + responseData.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/Strapi/Strapi.node.ts b/packages/nodes-base/nodes/Strapi/Strapi.node.ts new file mode 100644 index 0000000000..3b38ae7aad --- /dev/null +++ b/packages/nodes-base/nodes/Strapi/Strapi.node.ts @@ -0,0 +1,191 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + getToken, + strapiApiRequest, + strapiApiRequestAllItems, + validateJSON, +} from './GenericFunctions'; + +import { + entryFields, + entryOperations, +} from './EntryDescription'; + +export class Strapi implements INodeType { + description: INodeTypeDescription = { + displayName: 'Strapi', + name: 'strapi', + icon: 'file:strapi.svg', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Strapi API.', + defaults: { + name: 'Strapi', + color: '#725ed8', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'strapiApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Entry', + value: 'entry', + }, + ], + default: 'entry', + description: 'The resource to operate on.', + }, + ...entryOperations, + ...entryFields, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = (items.length as unknown) as number; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + const { jwt } = await getToken.call(this); + + qs.jwt = jwt; + + if (resource === 'entry') { + if (operation === 'create') { + for (let i = 0; i < length; i++) { + + const body: IDataObject = {}; + + const contentType = this.getNodeParameter('contentType', i) as string; + + const columns = this.getNodeParameter('columns', i) as string; + + const columnList = columns.split(',').map(column => column.trim()); + + for (const key of Object.keys(items[i].json)) { + if (columnList.includes(key)) { + body[key] = items[i].json[key]; + } + } + responseData = await strapiApiRequest.call(this, 'POST', `/${contentType}`, body, qs); + + returnData.push(responseData); + } + } + + if (operation === 'delete') { + for (let i = 0; i < length; i++) { + const contentType = this.getNodeParameter('contentType', i) as string; + + const entryId = this.getNodeParameter('entryId', i) as string; + + responseData = await strapiApiRequest.call(this, 'DELETE', `/${contentType}/${entryId}`, {}, qs); + + returnData.push(responseData); + } + } + + if (operation === 'getAll') { + for (let i = 0; i < length; i++) { + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + const contentType = this.getNodeParameter('contentType', i) as string; + + const options = this.getNodeParameter('options', i) as IDataObject; + + if (options.sort && (options.sort as string[]).length !== 0) { + const sortFields = options.sort as string[]; + qs._sort = sortFields.join(','); + } + + if (options.where) { + const query = validateJSON(options.where as string); + if (query !== undefined) { + qs._where = query; + } else { + throw new Error('Query must be a valid JSON'); + } + } + + if (options.publicationState) { + qs._publicationState = options.publicationState as string; + } + + if (returnAll) { + responseData = await strapiApiRequestAllItems.call(this, 'GET', `/${contentType}`, {}, qs); + } else { + qs._limit = this.getNodeParameter('limit', i) as number; + + responseData = await strapiApiRequest.call(this, 'GET', `/${contentType}`, {}, qs); + } + returnData.push.apply(returnData, responseData); + } + } + + if (operation === 'get') { + for (let i = 0; i < length; i++) { + + const contentType = this.getNodeParameter('contentType', i) as string; + + const entryId = this.getNodeParameter('entryId', i) as string; + + responseData = await strapiApiRequest.call(this, 'GET', `/${contentType}/${entryId}`, {}, qs); + + returnData.push(responseData); + } + } + + if (operation === 'update') { + for (let i = 0; i < length; i++) { + + const body: IDataObject = {}; + + const contentType = this.getNodeParameter('contentType', i) as string; + + const columns = this.getNodeParameter('columns', i) as string; + + const updateKey = this.getNodeParameter('updateKey', i) as string; + + const columnList = columns.split(',').map(column => column.trim()); + + const entryId = items[i].json[updateKey]; + + for (const key of Object.keys(items[i].json)) { + if (columnList.includes(key)) { + body[key] = items[i].json[key]; + } + } + responseData = await strapiApiRequest.call(this, 'PUT', `/${contentType}/${entryId}`, body, qs); + + returnData.push(responseData); + } + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Strapi/strapi.svg b/packages/nodes-base/nodes/Strapi/strapi.svg new file mode 100644 index 0000000000..bf9f95847a --- /dev/null +++ b/packages/nodes-base/nodes/Strapi/strapi.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 7fe2ff5992..64cbc4de9e 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -180,6 +180,7 @@ "dist/credentials/SpotifyOAuth2Api.credentials.js", "dist/credentials/StoryblokContentApi.credentials.js", "dist/credentials/StoryblokManagementApi.credentials.js", + "dist/credentials/StrapiApi.credentials.js", "dist/credentials/SurveyMonkeyApi.credentials.js", "dist/credentials/SurveyMonkeyOAuth2Api.credentials.js", "dist/credentials/TaigaCloudApi.credentials.js", @@ -387,6 +388,7 @@ "dist/nodes/SseTrigger.node.js", "dist/nodes/Start.node.js", "dist/nodes/Storyblok/Storyblok.node.js", + "dist/nodes/Strapi/Strapi.node.js", "dist/nodes/Strava/Strava.node.js", "dist/nodes/Strava/StravaTrigger.node.js", "dist/nodes/Stripe/StripeTrigger.node.js", From 73e1399c424edbfea7bbdac3e54b16b366e04dea Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 11 Nov 2020 09:45:50 +0100 Subject: [PATCH 24/42] :zap: Minor improvements to Strapi-Node --- .../credentials/StrapiApi.credentials.ts | 4 ++++ .../nodes-base/nodes/Strapi/EntryDescription.ts | 14 +++++++++----- .../nodes-base/nodes/Strapi/GenericFunctions.ts | 2 +- packages/nodes-base/nodes/Strapi/Strapi.node.ts | 8 ++++---- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/nodes-base/credentials/StrapiApi.credentials.ts b/packages/nodes-base/credentials/StrapiApi.credentials.ts index 395bbc7f9d..801503193c 100644 --- a/packages/nodes-base/credentials/StrapiApi.credentials.ts +++ b/packages/nodes-base/credentials/StrapiApi.credentials.ts @@ -17,6 +17,9 @@ export class StrapiApi implements ICredentialType { displayName: 'Password', name: 'password', type: 'string' as NodePropertyTypes, + typeOptions: { + password: true, + }, default: '', }, { @@ -24,6 +27,7 @@ export class StrapiApi implements ICredentialType { name: 'url', type: 'string' as NodePropertyTypes, default: '', + placeholder: 'https://api.example.com', }, ]; } diff --git a/packages/nodes-base/nodes/Strapi/EntryDescription.ts b/packages/nodes-base/nodes/Strapi/EntryDescription.ts index 293304a929..7305aa6ce7 100644 --- a/packages/nodes-base/nodes/Strapi/EntryDescription.ts +++ b/packages/nodes-base/nodes/Strapi/EntryDescription.ts @@ -66,7 +66,7 @@ export const entryFields = [ ], }, }, - description: 'Name of the content type', + description: 'Name of the content type.', }, { displayName: 'Columns', @@ -86,6 +86,7 @@ export const entryFields = [ placeholder: 'id,name,description', description: 'Comma separated list of the properties which should used as columns for the new rows.', }, + /* -------------------------------------------------------------------------- */ /* entry:delete */ /* -------------------------------------------------------------------------- */ @@ -105,7 +106,7 @@ export const entryFields = [ ], }, }, - description: 'Name of the content type', + description: 'Name of the content type.', }, { displayName: 'Entry ID', @@ -123,8 +124,9 @@ export const entryFields = [ ], }, }, - description: 'The ID of the entry to get.', + description: 'The ID of the entry to delete.', }, + /* -------------------------------------------------------------------------- */ /* entry:get */ /* -------------------------------------------------------------------------- */ @@ -144,7 +146,7 @@ export const entryFields = [ ], }, }, - description: 'Name of the content type', + description: 'Name of the content type.', }, { displayName: 'Entry ID', @@ -164,6 +166,7 @@ export const entryFields = [ }, description: 'The ID of the entry to get.', }, + /* -------------------------------------------------------------------------- */ /* entry:getAll */ /* -------------------------------------------------------------------------- */ @@ -286,6 +289,7 @@ export const entryFields = [ }, ], }, + /* -------------------------------------------------------------------------- */ /* entry:update */ /* -------------------------------------------------------------------------- */ @@ -305,7 +309,7 @@ export const entryFields = [ ], }, }, - description: 'Name of the content type', + description: 'Name of the content type.', }, { displayName: 'Update Key', diff --git a/packages/nodes-base/nodes/Strapi/GenericFunctions.ts b/packages/nodes-base/nodes/Strapi/GenericFunctions.ts index 6431ad0bd9..e6061998e3 100644 --- a/packages/nodes-base/nodes/Strapi/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Strapi/GenericFunctions.ts @@ -16,7 +16,7 @@ import { export async function strapiApiRequest(this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions | IWebhookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, headers: IDataObject = {}): Promise { // tslint:disable-line:no-any const credentials = this.getCredentials('strapiApi') as IDataObject; - + try { const options: OptionsWithUri = { headers: { diff --git a/packages/nodes-base/nodes/Strapi/Strapi.node.ts b/packages/nodes-base/nodes/Strapi/Strapi.node.ts index 3b38ae7aad..3a973a1f99 100644 --- a/packages/nodes-base/nodes/Strapi/Strapi.node.ts +++ b/packages/nodes-base/nodes/Strapi/Strapi.node.ts @@ -92,7 +92,7 @@ export class Strapi implements INodeType { } } responseData = await strapiApiRequest.call(this, 'POST', `/${contentType}`, body, qs); - + returnData.push(responseData); } } @@ -102,7 +102,7 @@ export class Strapi implements INodeType { const contentType = this.getNodeParameter('contentType', i) as string; const entryId = this.getNodeParameter('entryId', i) as string; - + responseData = await strapiApiRequest.call(this, 'DELETE', `/${contentType}/${entryId}`, {}, qs); returnData.push(responseData); @@ -140,7 +140,7 @@ export class Strapi implements INodeType { responseData = await strapiApiRequestAllItems.call(this, 'GET', `/${contentType}`, {}, qs); } else { qs._limit = this.getNodeParameter('limit', i) as number; - + responseData = await strapiApiRequest.call(this, 'GET', `/${contentType}`, {}, qs); } returnData.push.apply(returnData, responseData); @@ -181,7 +181,7 @@ export class Strapi implements INodeType { } } responseData = await strapiApiRequest.call(this, 'PUT', `/${contentType}/${entryId}`, body, qs); - + returnData.push(responseData); } } From c15637a0a8feb50856af3e0bc05b0b67a046c379 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 11 Nov 2020 09:52:05 +0100 Subject: [PATCH 25/42] :zap: Minor improvements to Strapi-Node --- .../nodes-base/nodes/Strapi/GenericFunctions.ts | 9 +++------ packages/nodes-base/nodes/Strapi/Strapi.node.ts | 15 ++++++++------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/nodes-base/nodes/Strapi/GenericFunctions.ts b/packages/nodes-base/nodes/Strapi/GenericFunctions.ts index e6061998e3..fbd7c57ff0 100644 --- a/packages/nodes-base/nodes/Strapi/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Strapi/GenericFunctions.ts @@ -19,9 +19,7 @@ export async function strapiApiRequest(this: IExecuteFunctions | ILoadOptionsFun try { const options: OptionsWithUri = { - headers: { - 'Authorization': `Bearer ${qs.jwt}`, - }, + headers: {}, method, body, qs, @@ -34,7 +32,6 @@ export async function strapiApiRequest(this: IExecuteFunctions | ILoadOptionsFun if (Object.keys(body).length === 0) { delete options.body; } - delete qs.jwt; //@ts-ignore return await this.helpers?.request(options); @@ -74,7 +71,7 @@ export async function getToken(this: IExecuteFunctions | ILoadOptionsFunctions | return this.helpers.request!(options); } -export async function strapiApiRequestAllItems(this: IHookFunctions | ILoadOptionsFunctions | IExecuteFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any +export async function strapiApiRequestAllItems(this: IHookFunctions | ILoadOptionsFunctions | IExecuteFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, headers: IDataObject = {}): Promise { // tslint:disable-line:no-any const returnData: IDataObject[] = []; @@ -85,7 +82,7 @@ export async function strapiApiRequestAllItems(this: IHookFunctions | ILoadOptio query._start = 0; do { - responseData = await strapiApiRequest.call(this, method, resource, body, query); + responseData = await strapiApiRequest.call(this, method, resource, body, query, undefined, headers); query._start += query._limit; returnData.push.apply(returnData, responseData); } while ( diff --git a/packages/nodes-base/nodes/Strapi/Strapi.node.ts b/packages/nodes-base/nodes/Strapi/Strapi.node.ts index 3a973a1f99..74849e6187 100644 --- a/packages/nodes-base/nodes/Strapi/Strapi.node.ts +++ b/packages/nodes-base/nodes/Strapi/Strapi.node.ts @@ -66,13 +66,14 @@ export class Strapi implements INodeType { const returnData: IDataObject[] = []; const length = (items.length as unknown) as number; const qs: IDataObject = {}; + const headers: IDataObject = {}; let responseData; const resource = this.getNodeParameter('resource', 0) as string; const operation = this.getNodeParameter('operation', 0) as string; const { jwt } = await getToken.call(this); - qs.jwt = jwt; + headers.Authorization = `Bearer ${jwt}`; if (resource === 'entry') { if (operation === 'create') { @@ -91,7 +92,7 @@ export class Strapi implements INodeType { body[key] = items[i].json[key]; } } - responseData = await strapiApiRequest.call(this, 'POST', `/${contentType}`, body, qs); + responseData = await strapiApiRequest.call(this, 'POST', `/${contentType}`, body, qs, undefined, headers); returnData.push(responseData); } @@ -103,7 +104,7 @@ export class Strapi implements INodeType { const entryId = this.getNodeParameter('entryId', i) as string; - responseData = await strapiApiRequest.call(this, 'DELETE', `/${contentType}/${entryId}`, {}, qs); + responseData = await strapiApiRequest.call(this, 'DELETE', `/${contentType}/${entryId}`, {}, qs, undefined, headers); returnData.push(responseData); } @@ -137,11 +138,11 @@ export class Strapi implements INodeType { } if (returnAll) { - responseData = await strapiApiRequestAllItems.call(this, 'GET', `/${contentType}`, {}, qs); + responseData = await strapiApiRequestAllItems.call(this, 'GET', `/${contentType}`, {}, qs, headers); } else { qs._limit = this.getNodeParameter('limit', i) as number; - responseData = await strapiApiRequest.call(this, 'GET', `/${contentType}`, {}, qs); + responseData = await strapiApiRequest.call(this, 'GET', `/${contentType}`, {}, qs, undefined, headers); } returnData.push.apply(returnData, responseData); } @@ -154,7 +155,7 @@ export class Strapi implements INodeType { const entryId = this.getNodeParameter('entryId', i) as string; - responseData = await strapiApiRequest.call(this, 'GET', `/${contentType}/${entryId}`, {}, qs); + responseData = await strapiApiRequest.call(this, 'GET', `/${contentType}/${entryId}`, {}, qs, undefined, headers); returnData.push(responseData); } @@ -180,7 +181,7 @@ export class Strapi implements INodeType { body[key] = items[i].json[key]; } } - responseData = await strapiApiRequest.call(this, 'PUT', `/${contentType}/${entryId}`, body, qs); + responseData = await strapiApiRequest.call(this, 'PUT', `/${contentType}/${entryId}`, body, qs, undefined, headers); returnData.push(responseData); } From 0f95ad4446628f0d75198e467df3513375cc5f26 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 11 Nov 2020 10:24:10 +0100 Subject: [PATCH 26/42] :bug: Fix retry buy with current workflow #1144 --- packages/cli/src/Server.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 4d3a9e1aec..4b38988b71 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -1534,17 +1534,21 @@ class App { // Loads the currently saved workflow to execute instead of the // one saved at the time of the execution. const workflowId = fullExecutionData.workflowData.id; - data.workflowData = await Db.collections.Workflow!.findOne(workflowId) as IWorkflowBase; + const workflowData = await Db.collections.Workflow!.findOne(workflowId) as IWorkflowBase; - if (data.workflowData === undefined) { + if (workflowData === undefined) { throw new Error(`The workflow with the ID "${workflowId}" could not be found and so the data not be loaded for the retry.`); } + data.workflowData = workflowData; + const nodeTypes = NodeTypes(); + const workflowInstance = new Workflow({ id: workflowData.id as string, name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: false, nodeTypes, staticData: undefined, settings: workflowData.settings }); + // Replace all of the nodes in the execution stack with the ones of the new workflow for (const stack of data!.executionData!.executionData!.nodeExecutionStack) { // Find the data of the last executed node in the new workflow - const node = data.workflowData.nodes.find(node => node.name === stack.node.name); - if (node === undefined) { + const node = workflowInstance.getNode(stack.node.name); + if (node === null) { throw new Error(`Could not find the node "${stack.node.name}" in workflow. It probably got deleted or renamed. Without it the workflow can sadly not be retried.`); } From d03b21554cb9b85ce6937e8891d66766aa463ba2 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 11 Nov 2020 11:56:29 +0100 Subject: [PATCH 27/42] :bookmark: Release n8n-workflow@0.44.0 --- packages/workflow/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/workflow/package.json b/packages/workflow/package.json index bcaed2e5d4..cfe35eac2a 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -1,6 +1,6 @@ { "name": "n8n-workflow", - "version": "0.43.0", + "version": "0.44.0", "description": "Workflow base code of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From 16caa14065a619aee34a40932bcca1d808064f88 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 11 Nov 2020 11:57:23 +0100 Subject: [PATCH 28/42] :arrow_up: Set n8n-workflow@0.44.0 on n8n-core --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index dad78b0bcd..1ecd0c51a0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -47,7 +47,7 @@ "file-type": "^14.6.2", "lodash.get": "^4.4.2", "mime-types": "^2.1.27", - "n8n-workflow": "~0.43.0", + "n8n-workflow": "~0.44.0", "oauth-1.0a": "^2.2.6", "p-cancelable": "^2.0.0", "request": "^2.88.2", From dce3e0561b1160b3a52d35130608176afce159ae Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 11 Nov 2020 11:57:47 +0100 Subject: [PATCH 29/42] :bookmark: Release n8n-core@0.51.0 --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 1ecd0c51a0..bb7dd473aa 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "n8n-core", - "version": "0.50.0", + "version": "0.51.0", "description": "Core functionality of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From 1a9d2a824fcf8fd2af6a57c832d98154e953991f Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 11 Nov 2020 11:59:00 +0100 Subject: [PATCH 30/42] :arrow_up: Set n8n-core@0.51.0 and n8n-workflow@0.44.0 on n8n-nodes-base --- packages/nodes-base/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 64cbc4de9e..ae947d08e9 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -457,7 +457,7 @@ "@types/xml2js": "^0.4.3", "gulp": "^4.0.0", "jest": "^26.4.2", - "n8n-workflow": "~0.43.0", + "n8n-workflow": "~0.44.0", "ts-jest": "^26.3.0", "tslint": "^6.1.2", "typescript": "~3.9.7" @@ -487,7 +487,7 @@ "mqtt": "^4.2.0", "mssql": "^6.2.0", "mysql2": "~2.1.0", - "n8n-core": "~0.50.0", + "n8n-core": "~0.51.0", "nodemailer": "^6.4.6", "pdf-parse": "^1.1.1", "pg": "^8.3.0", From 46509e2fcdc2e7eadbe9674528bc32d44e1b6ca0 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 11 Nov 2020 11:59:30 +0100 Subject: [PATCH 31/42] :bookmark: Release n8n-nodes-base@0.88.0 --- packages/nodes-base/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index ae947d08e9..7214127df7 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -1,6 +1,6 @@ { "name": "n8n-nodes-base", - "version": "0.87.0", + "version": "0.88.0", "description": "Base nodes of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From bf47cc03377c553505f49281b423a29c607a2e18 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 11 Nov 2020 12:00:54 +0100 Subject: [PATCH 32/42] :arrow_up: Set n8n-workflow@0.44.0 on n8n-editor-ui --- packages/editor-ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 805de89a55..1e9c350aea 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -65,7 +65,7 @@ "lodash.debounce": "^4.0.8", "lodash.get": "^4.4.2", "lodash.set": "^4.3.2", - "n8n-workflow": "~0.43.0", + "n8n-workflow": "~0.44.0", "node-sass": "^4.12.0", "normalize-wheel": "^1.0.1", "prismjs": "^1.17.1", From cdea2af2488c09a82259bcd59dbf4fc09c33b54b Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 11 Nov 2020 12:01:18 +0100 Subject: [PATCH 33/42] :bookmark: Release n8n-editor-ui@0.63.0 --- packages/editor-ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 1e9c350aea..4e5f0d91d0 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -1,6 +1,6 @@ { "name": "n8n-editor-ui", - "version": "0.62.0", + "version": "0.63.0", "description": "Workflow Editor UI for n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From 887e777e9aaf8037c0a052367fa64f049379c5a6 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 11 Nov 2020 12:02:45 +0100 Subject: [PATCH 34/42] :arrow_up: Set n8n-core@0.51.0, n8n-editor-ui@0.63.0, n8n-nodes-base@0.88.0 and n8n-workflow@0.44.0 on n8n --- packages/cli/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index ea41423761..0e546a821c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -103,10 +103,10 @@ "lodash.get": "^4.4.2", "mongodb": "^3.5.5", "mysql2": "~2.1.0", - "n8n-core": "~0.50.0", - "n8n-editor-ui": "~0.62.0", - "n8n-nodes-base": "~0.87.0", - "n8n-workflow": "~0.43.0", + "n8n-core": "~0.51.0", + "n8n-editor-ui": "~0.63.0", + "n8n-nodes-base": "~0.88.0", + "n8n-workflow": "~0.44.0", "oauth-1.0a": "^2.2.6", "open": "^7.0.0", "pg": "^8.3.0", From 7641d2ad81fdde5597d7be3624ff3c2e8f0bde1b Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 11 Nov 2020 12:03:16 +0100 Subject: [PATCH 35/42] :bookmark: Release n8n@0.93.0 --- packages/cli/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 0e546a821c..14968d585f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "0.92.0", + "version": "0.93.0", "description": "n8n Workflow Automation Tool", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From 241fc064c20eb35d416489ac206901677e8c18e2 Mon Sep 17 00:00:00 2001 From: Harshil Agrawal Date: Wed, 11 Nov 2020 20:16:52 +0530 Subject: [PATCH 36/42] :zap: Add credential documentation URL for the Gotify node (#1152) --- packages/nodes-base/credentials/GotifyApi.credentials.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/nodes-base/credentials/GotifyApi.credentials.ts b/packages/nodes-base/credentials/GotifyApi.credentials.ts index 5f90e6726b..e417f60592 100644 --- a/packages/nodes-base/credentials/GotifyApi.credentials.ts +++ b/packages/nodes-base/credentials/GotifyApi.credentials.ts @@ -6,6 +6,7 @@ import { export class GotifyApi implements ICredentialType { name = 'gotifyApi'; displayName = 'Gotify API'; + documentationUrl = 'gotify'; properties = [ { displayName: 'App API Token', From a800a8407801b15e42fa8055bc02f426618bd7b7 Mon Sep 17 00:00:00 2001 From: Harshil Agrawal Date: Wed, 11 Nov 2020 20:38:30 +0530 Subject: [PATCH 37/42] :zap: Added credential documentation URL for Line node (#1150) --- .../nodes-base/credentials/LineNotifyOAuth2Api.credentials.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/nodes-base/credentials/LineNotifyOAuth2Api.credentials.ts b/packages/nodes-base/credentials/LineNotifyOAuth2Api.credentials.ts index f97af2e872..b5bc61fb8f 100644 --- a/packages/nodes-base/credentials/LineNotifyOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/LineNotifyOAuth2Api.credentials.ts @@ -9,6 +9,7 @@ export class LineNotifyOAuth2Api implements ICredentialType { 'oAuth2Api', ]; displayName = 'Line Notify OAuth2 API'; + documentationUrl = 'line'; properties = [ { displayName: 'Authorization URL', From c602803ed73aa026a340d783dc46d536cfbefa98 Mon Sep 17 00:00:00 2001 From: Tanay Pant <7481165+tanay1337@users.noreply.github.com> Date: Thu, 12 Nov 2020 09:14:24 +0100 Subject: [PATCH 38/42] :zap: Add breaking change for 0.93.0 --- packages/cli/BREAKING-CHANGES.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/cli/BREAKING-CHANGES.md b/packages/cli/BREAKING-CHANGES.md index 1e603ca1cb..83b2514ea4 100644 --- a/packages/cli/BREAKING-CHANGES.md +++ b/packages/cli/BREAKING-CHANGES.md @@ -2,6 +2,21 @@ This list shows all the versions which include breaking changes and how to upgrade. +## 0.93.0 + +### What changed? + +Change in naming of the Authentication field for the Pipedrive Trigger node. + +### When is action necessary? + +If you had set "Basic Auth" for the "Authentication" field in the node. + +### How to upgrade: + +The "Authentication" field has been renamed to "Incoming Authentication". Please set the parameter “Incoming Authentication” to “Basic Auth” to activate it again. + + ## 0.90.0 ### What changed? From e327bfcb917f0f72511312eb463de2623e138c55 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 12 Nov 2020 12:42:04 +0100 Subject: [PATCH 39/42] :zap: Do not allow shorter polling-times than 1 minute --- packages/core/src/ActiveWorkflows.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/core/src/ActiveWorkflows.ts b/packages/core/src/ActiveWorkflows.ts index 2e3b6d7c12..86a88c617b 100644 --- a/packages/core/src/ActiveWorkflows.ts +++ b/packages/core/src/ActiveWorkflows.ts @@ -127,7 +127,7 @@ export class ActiveWorkflows { for (const item of pollTimes.item) { cronTime = []; if (item.mode === 'custom') { - cronTimes.push(item.cronExpression as string); + cronTimes.push((item.cronExpression as string).trim()); continue; } if (item.mode === 'everyMinute') { @@ -178,6 +178,11 @@ export class ActiveWorkflows { // Start the cron-jobs const cronJobs: CronJob[] = []; for (const cronTime of cronTimes) { + const cronTimeParts = cronTime.split(' '); + if (cronTimeParts.length > 0 && cronTimeParts[0].includes('*')) { + throw new Error('The polling interval is too short. It has to be at least a minute!'); + } + cronJobs.push(new CronJob(cronTime, executeTrigger, undefined, true, timezone)); } From 042028fb5fa7f3ae5f164317b6c268a20c816dd1 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Thu, 12 Nov 2020 13:23:55 -0500 Subject: [PATCH 40/42] :zap: Add private key and passphrase fields to SFTP credentials (#1136) * :zap: Add private key and passphrase fields to credentials * :zap: Small improvement --- .../credentials/Sftp.credentials.ts | 22 +++++++++++++++++++ packages/nodes-base/nodes/Ftp.node.ts | 2 ++ 2 files changed, 24 insertions(+) diff --git a/packages/nodes-base/credentials/Sftp.credentials.ts b/packages/nodes-base/credentials/Sftp.credentials.ts index 5c7dd2122c..93f1e0dec4 100644 --- a/packages/nodes-base/credentials/Sftp.credentials.ts +++ b/packages/nodes-base/credentials/Sftp.credentials.ts @@ -38,5 +38,27 @@ export class Sftp implements ICredentialType { }, default: '', }, + { + displayName: 'Private Key', + name: 'privateKey', + required: true, + type: 'string' as NodePropertyTypes, + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'String that contains a private key for either key-based or hostbased user authentication (OpenSSH format).', + }, + { + displayName: 'Passphrase', + name: 'passphrase', + typeOptions: { + password: true, + }, + required: true, + type: 'string' as NodePropertyTypes, + default: '', + description: 'For an encrypted private key, this is the passphrase used to decrypt it', + }, ]; } diff --git a/packages/nodes-base/nodes/Ftp.node.ts b/packages/nodes-base/nodes/Ftp.node.ts index c71b096bf7..dacbd40a5d 100644 --- a/packages/nodes-base/nodes/Ftp.node.ts +++ b/packages/nodes-base/nodes/Ftp.node.ts @@ -288,6 +288,8 @@ export class Ftp implements INodeType { port: credentials.port as number, username: credentials.username as string, password: credentials.password as string, + privateKey: credentials.privateKey as string | undefined, + passphrase: credentials.passphrase as string | undefined, }); } else { From 8f8603320a010d004da34931f71e5ea1abf3596c Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 12 Nov 2020 19:26:03 +0100 Subject: [PATCH 41/42] :zap: Minor improvement to SFTP credentials --- packages/nodes-base/credentials/Sftp.credentials.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/nodes-base/credentials/Sftp.credentials.ts b/packages/nodes-base/credentials/Sftp.credentials.ts index 93f1e0dec4..d9eb0f2747 100644 --- a/packages/nodes-base/credentials/Sftp.credentials.ts +++ b/packages/nodes-base/credentials/Sftp.credentials.ts @@ -41,10 +41,9 @@ export class Sftp implements ICredentialType { { displayName: 'Private Key', name: 'privateKey', - required: true, type: 'string' as NodePropertyTypes, typeOptions: { - alwaysOpenEditWindow: true, + alwaysOpenEditWindow: true, }, default: '', description: 'String that contains a private key for either key-based or hostbased user authentication (OpenSSH format).', @@ -55,10 +54,9 @@ export class Sftp implements ICredentialType { typeOptions: { password: true, }, - required: true, type: 'string' as NodePropertyTypes, default: '', - description: 'For an encrypted private key, this is the passphrase used to decrypt it', + description: 'For an encrypted private key, this is the passphrase used to decrypt it.', }, ]; } From f2666e92ffed2c3983d08e73b1e45a2bd516b90d Mon Sep 17 00:00:00 2001 From: Ben Hesseldieck <1849459+BHesseldieck@users.noreply.github.com> Date: Fri, 13 Nov 2020 23:31:27 +0100 Subject: [PATCH 42/42] :zap: Add preExecuteHooks (#1151) * :zap: Save initital data on hook error * :construction: update function interface * :construction: response webhook with error, :bug: fix adding preExecutionHooks to hooks * :fire: remove execute hook * :zap: execute preExecute hooks on integrated workflows Co-authored-by: Jan Oberhauser --- packages/cli/commands/start.ts | 1 - packages/cli/src/WebhookHelpers.ts | 26 ++++++++-------- .../cli/src/WorkflowExecuteAdditionalData.ts | 26 ++++++++++++++-- packages/cli/src/WorkflowRunner.ts | 6 ++-- packages/cli/src/WorkflowRunnerProcess.ts | 13 +++++++- packages/core/src/WorkflowExecute.ts | 30 ++++++++++++++++++- packages/workflow/src/Interfaces.ts | 2 +- packages/workflow/src/WorkflowHooks.ts | 16 ++-------- 8 files changed, 85 insertions(+), 35 deletions(-) diff --git a/packages/cli/commands/start.ts b/packages/cli/commands/start.ts index 8e14b98e2c..d6e16b7cb3 100644 --- a/packages/cli/commands/start.ts +++ b/packages/cli/commands/start.ts @@ -15,7 +15,6 @@ import { Db, ExternalHooks, GenericHelpers, - IExecutionsCurrentSummary, LoadNodesAndCredentials, NodeTypes, Server, diff --git a/packages/cli/src/WebhookHelpers.ts b/packages/cli/src/WebhookHelpers.ts index 114111a821..6d3504963a 100644 --- a/packages/cli/src/WebhookHelpers.ts +++ b/packages/cli/src/WebhookHelpers.ts @@ -222,7 +222,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] { return; } - // Now that we know that the workflow should run we can return the default respons + // Now that we know that the workflow should run we can return the default response // directly if responseMode it set to "onReceived" and a respone should be sent if (responseMode === 'onReceived' && didSendResponse === false) { // Return response directly and do not wait for the workflow to finish @@ -302,6 +302,19 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] { } const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data); + if(data.data.resultData.error || returnData?.error !== undefined) { + if (didSendResponse === false) { + responseCallback(null, { + data: { + message: 'Workflow did error.', + }, + responseCode: 500, + }); + } + didSendResponse = true; + return data; + } + if (returnData === undefined) { if (didSendResponse === false) { responseCallback(null, { @@ -313,17 +326,6 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] { } didSendResponse = true; return data; - } else if (returnData.error !== undefined) { - if (didSendResponse === false) { - responseCallback(null, { - data: { - message: 'Workflow did error.', - }, - responseCode: 500, - }); - } - didSendResponse = true; - return data; } const responseData = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseData'], 'firstEntryJson'); diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index 11c15aee8e..cf755a6eaa 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -202,6 +202,18 @@ function hookFunctionsPush(): IWorkflowExecuteHooks { } +export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowExecuteHooks { + const externalHooks = ExternalHooks(); + + return { + workflowExecuteBefore: [ + async function (this: WorkflowHooks, workflow: Workflow): Promise { + await externalHooks.run('workflow.preExecute', [workflow, this.mode]); + }, + ], + }; +} + /** * Returns hook functions to save workflow execution and call error workflow * @@ -337,7 +349,6 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi const externalHooks = ExternalHooks(); await externalHooks.init(); - await externalHooks.run('workflow.execute', [workflowData, mode]); const nodeTypes = NodeTypes(); @@ -462,6 +473,10 @@ export async function getBase(credentials: IWorkflowCredentials, currentNodePara export function getWorkflowHooksIntegrated(mode: WorkflowExecuteMode, executionId: string, workflowData: IWorkflowBase, optionalParameters?: IWorkflowHooksOptionalParameters): WorkflowHooks { optionalParameters = optionalParameters || {}; const hookFunctions = hookFunctionsSave(optionalParameters.parentProcessMode); + const preExecuteFunctions = hookFunctionsPreExecute(optionalParameters.parentProcessMode); + for (const key of Object.keys(preExecuteFunctions)) { + hookFunctions[key]!.push.apply(hookFunctions[key], preExecuteFunctions[key]); + } return new WorkflowHooks(hookFunctions, mode, executionId, workflowData, optionalParameters); } @@ -474,12 +489,19 @@ export function getWorkflowHooksIntegrated(mode: WorkflowExecuteMode, executionI * @param {string} executionId * @returns {WorkflowHooks} */ -export function getWorkflowHooksMain(data: IWorkflowExecutionDataProcess, executionId: string): WorkflowHooks { +export function getWorkflowHooksMain(data: IWorkflowExecutionDataProcess, executionId: string, isMainProcess = false): WorkflowHooks { const hookFunctions = hookFunctionsSave(); const pushFunctions = hookFunctionsPush(); for (const key of Object.keys(pushFunctions)) { hookFunctions[key]!.push.apply(hookFunctions[key], pushFunctions[key]); } + if (isMainProcess) { + const preExecuteFunctions = hookFunctionsPreExecute(); + for (const key of Object.keys(preExecuteFunctions)) { + hookFunctions[key]!.push.apply(hookFunctions[key], preExecuteFunctions[key]); + } + } + return new WorkflowHooks(hookFunctions, data.executionMode, executionId, data.workflowData, { sessionId: data.sessionId, retryOf: data.retryOf as string}); } diff --git a/packages/cli/src/WorkflowRunner.ts b/packages/cli/src/WorkflowRunner.ts index 3a5e197f1a..3306282a4e 100644 --- a/packages/cli/src/WorkflowRunner.ts +++ b/packages/cli/src/WorkflowRunner.ts @@ -100,9 +100,6 @@ export class WorkflowRunner { * @memberof WorkflowRunner */ async run(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean): Promise { - const externalHooks = ExternalHooks(); - await externalHooks.run('workflow.execute', [data.workflowData, data.executionMode]); - const executionsProcess = config.get('executions.process') as string; let executionId: string; @@ -112,6 +109,7 @@ export class WorkflowRunner { executionId = await this.runSubprocess(data, loadStaticData); } + const externalHooks = ExternalHooks(); if (externalHooks.exists('workflow.postExecute')) { this.activeExecutions.getPostExecutePromise(executionId) .then(async (executionData) => { @@ -148,7 +146,7 @@ export class WorkflowRunner { // Register the active execution const executionId = this.activeExecutions.add(data, undefined); - additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId); + additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId, true); let workflowExecution: PCancelable; if (data.executionData !== undefined) { diff --git a/packages/cli/src/WorkflowRunnerProcess.ts b/packages/cli/src/WorkflowRunnerProcess.ts index 894e110297..8fc749b4b8 100644 --- a/packages/cli/src/WorkflowRunnerProcess.ts +++ b/packages/cli/src/WorkflowRunnerProcess.ts @@ -2,6 +2,7 @@ import { CredentialsOverwrites, CredentialTypes, + ExternalHooks, IWorkflowExecutionDataProcessWithExecution, NodeTypes, WorkflowExecuteAdditionalData, @@ -19,6 +20,7 @@ import { INodeTypeData, IRun, ITaskData, + IWorkflowExecuteHooks, Workflow, WorkflowHooks, } from 'n8n-workflow'; @@ -68,6 +70,10 @@ export class WorkflowRunnerProcess { const credentialsOverwrites = CredentialsOverwrites(); await credentialsOverwrites.init(inputData.credentialsOverwrite); + // Load all external hooks + const externalHooks = ExternalHooks(); + await externalHooks.init(); + this.workflow = new Workflow({ id: this.data.workflowData.id as string | undefined, name: this.data.workflowData.name, nodes: this.data.workflowData!.nodes, connections: this.data.workflowData!.connections, active: this.data.workflowData!.active, nodeTypes, staticData: this.data.workflowData!.staticData, settings: this.data.workflowData!.settings}); const additionalData = await WorkflowExecuteAdditionalData.getBase(this.data.credentials); additionalData.hooks = this.getProcessForwardHooks(); @@ -121,7 +127,7 @@ export class WorkflowRunnerProcess { * @returns */ getProcessForwardHooks(): WorkflowHooks { - const hookFunctions = { + const hookFunctions: IWorkflowExecuteHooks = { nodeExecuteBefore: [ async (nodeName: string): Promise => { this.sendHookToParentProcess('nodeExecuteBefore', [nodeName]); @@ -144,6 +150,11 @@ export class WorkflowRunnerProcess { ], }; + const preExecuteFunctions = WorkflowExecuteAdditionalData.hookFunctionsPreExecute(); + for (const key of Object.keys(preExecuteFunctions)) { + hookFunctions[key]!.push.apply(hookFunctions[key], preExecuteFunctions[key]); + } + return new WorkflowHooks(hookFunctions, this.data!.executionMode, this.data!.executionId, this.data!.workflowData, { sessionId: this.data!.sessionId, retryOf: this.data!.retryOf as string }); } diff --git a/packages/core/src/WorkflowExecute.ts b/packages/core/src/WorkflowExecute.ts index 4133db0d9f..4ed49770a3 100644 --- a/packages/core/src/WorkflowExecute.ts +++ b/packages/core/src/WorkflowExecute.ts @@ -468,7 +468,6 @@ export class WorkflowExecute { this.runExecutionData.startData = {}; } - this.executeHook('workflowExecuteBefore', []); let currentExecutionTry = ''; let lastExecutionTry = ''; @@ -482,6 +481,35 @@ export class WorkflowExecute { }); const returnPromise = (async () => { + try { + await this.executeHook('workflowExecuteBefore', [workflow]); + } catch (error) { + // Set the error that it can be saved correctly + executionError = { + message: error.message, + stack: error.stack, + }; + + // Set the incoming data of the node that it can be saved correctly + executionData = this.runExecutionData.executionData!.nodeExecutionStack[0] as IExecuteData; + this.runExecutionData.resultData = { + runData: { + [executionData.node.name]: [ + { + startTime, + executionTime: (new Date().getTime()) - startTime, + data: ({ + 'main': executionData.data.main, + } as ITaskDataConnections), + }, + ], + }, + lastNodeExecuted: executionData.node.name, + error: executionError, + }; + + throw error; + } executionLoop: while (this.runExecutionData.executionData!.nodeExecutionStack.length !== 0) { diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 492810cf2c..33524176bb 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -716,7 +716,7 @@ export interface IWorkflowExecuteHooks { nodeExecuteAfter?: Array<((nodeName: string, data: ITaskData) => Promise)>; nodeExecuteBefore?: Array<((nodeName: string) => Promise)>; workflowExecuteAfter?: Array<((data: IRun, newStaticData: IDataObject) => Promise)>; - workflowExecuteBefore?: Array<(() => Promise)>; + workflowExecuteBefore?: Array<((workflow: Workflow, data: IRunExecutionData) => Promise)>; } export interface IWorkflowExecuteAdditionalData { diff --git a/packages/workflow/src/WorkflowHooks.ts b/packages/workflow/src/WorkflowHooks.ts index 94a02abddc..92efb468a0 100644 --- a/packages/workflow/src/WorkflowHooks.ts +++ b/packages/workflow/src/WorkflowHooks.ts @@ -28,19 +28,9 @@ export class WorkflowHooks { async executeHookFunctions(hookName: string, parameters: any[]) { // tslint:disable-line:no-any if (this.hookFunctions[hookName] !== undefined && Array.isArray(this.hookFunctions[hookName])) { for (const hookFunction of this.hookFunctions[hookName]!) { - await hookFunction.apply(this, parameters) - .catch((error: Error) => { - // Catch all errors here because when "executeHook" gets called - // we have the most time no "await" and so the errors would so - // not be uncaught by anything. - - // TODO: Add proper logging - console.error(`There was a problem executing hook: "${hookName}"`); - console.error('Parameters:'); - console.error(parameters); - console.error('Error:'); - console.error(error); - }); + // TODO: As catch got removed we should make sure that we catch errors + // where hooks get called + await hookFunction.apply(this, parameters); } } }