diff --git a/packages/cli/package.json b/packages/cli/package.json
index ea41423761994..14968d585f04a 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",
@@ -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",
diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts
index 4d3a9e1aec666..4b38988b712a5 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.`);
}
diff --git a/packages/core/package.json b/packages/core/package.json
index dad78b0bcd9cd..bb7dd473aa624 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",
@@ -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",
diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json
index 805de89a55962..4e5f0d91d016c 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",
@@ -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",
diff --git a/packages/editor-ui/src/components/CredentialsEdit.vue b/packages/editor-ui/src/components/CredentialsEdit.vue
index 49e3a12acda03..6d01db51ac4f8 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 6eb2b90ea363e..c694173540a01 100644
--- a/packages/editor-ui/src/components/DataDisplay.vue
+++ b/packages/editor-ui/src/components/DataDisplay.vue
@@ -9,7 +9,7 @@
-
@@ -65,6 +65,17 @@ export default Vue.extend({
};
},
computed: {
+ documentationUrl (): string {
+ if (!this.nodeType) {
+ return '';
+ }
+
+ 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/editor-ui/src/components/ParameterInput.vue b/packages/editor-ui/src/components/ParameterInput.vue
index 893c61aa33b0c..336e27041a407 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/credentials/GetResponseApi.credentials.ts b/packages/nodes-base/credentials/GetResponseApi.credentials.ts
new file mode 100644
index 0000000000000..1494a01930492
--- /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 0000000000000..76ce3acb0fa64
--- /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/credentials/GotifyApi.credentials.ts b/packages/nodes-base/credentials/GotifyApi.credentials.ts
new file mode 100644
index 0000000000000..5f90e6726b1fb
--- /dev/null
+++ b/packages/nodes-base/credentials/GotifyApi.credentials.ts
@@ -0,0 +1,32 @@
+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: '',
+ description: 'The URL of the Gotify host.',
+ },
+ ];
+}
diff --git a/packages/nodes-base/credentials/LineNotifyOAuth2Api.credentials.ts b/packages/nodes-base/credentials/LineNotifyOAuth2Api.credentials.ts
new file mode 100644
index 0000000000000..f97af2e872568
--- /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/credentials/MicrosoftSql.credentials.ts b/packages/nodes-base/credentials/MicrosoftSql.credentials.ts
index 598c9ded21cab..1b14ec5778d76 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/credentials/StrapiApi.credentials.ts b/packages/nodes-base/credentials/StrapiApi.credentials.ts
new file mode 100644
index 0000000000000..801503193c7b8
--- /dev/null
+++ b/packages/nodes-base/credentials/StrapiApi.credentials.ts
@@ -0,0 +1,33 @@
+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,
+ typeOptions: {
+ password: true,
+ },
+ default: '',
+ },
+ {
+ displayName: 'URL',
+ name: 'url',
+ type: 'string' as NodePropertyTypes,
+ default: '',
+ placeholder: 'https://api.example.com',
+ },
+ ];
+}
diff --git a/packages/nodes-base/nodes/Amqp/Amqp.node.ts b/packages/nodes-base/nodes/Amqp/Amqp.node.ts
index 6cbdcf4b4d73f..3fd8133711151 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,
@@ -69,17 +69,15 @@ 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,35 +107,43 @@ export class Amqp implements INodeType {
connectOptions.transport = credentials.transportType;
}
- const allSent = new Promise(( resolve ) => {
- container.on('sendable', (context: any) => { // tslint:disable-line:no-any
+ const conn = container.connect(connectOptions);
+ const sender = conn.open_sender(sink);
- let body: IDataObject | string = item.json;
- const sendOnlyProperty = options.sendOnlyProperty as string;
+ const responseData: IDataObject[] = await new Promise((resolve) => {
+ container.once('sendable', (context: any) => { // tslint:disable-line:no-any
+ const returnData = [];
- if (sendOnlyProperty) {
- body = body[sendOnlyProperty] as string;
- }
+ const items = this.getInputData();
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
- if (options.dataAsObject !== true) {
- body = JSON.stringify(body);
- }
+ let body: IDataObject | string = item.json;
+ const sendOnlyProperty = options.sendOnlyProperty as string;
+
+ if (sendOnlyProperty) {
+ body = body[sendOnlyProperty] as string;
+ }
- const message = {
- application_properties: headerProperties,
- body,
- };
+ if (options.dataAsObject !== true) {
+ body = JSON.stringify(body);
+ }
- const sendResult = context.sender.send(message);
+ const result = context.sender.send({
+ application_properties: headerProperties,
+ body,
+ });
- resolve(sendResult);
+ returnData.push({ id: result.id });
+ }
+
+ resolve(returnData);
});
});
- container.connect(connectOptions).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;
+ 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 1b1e679f13055..40af50c4f2ab6 100644
--- a/packages/nodes-base/nodes/Amqp/AmqpTrigger.node.ts
+++ b/packages/nodes-base/nodes/Amqp/AmqpTrigger.node.ts
@@ -82,6 +82,20 @@ 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.',
+ },
],
},
],
@@ -99,6 +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 = options.pullMessagesNumber || 100;
if (sink === '') {
throw new Error('Queue or Topic required!');
@@ -130,10 +145,13 @@ export class AmqpTrigger implements INodeType {
connectOptions.transport = credentials.transportType;
}
-
let lastMsgId: number | undefined = undefined;
const self = this;
+ container.on('receiver_open', (context: any) => { // tslint:disable-line:no-any
+ 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) {
@@ -143,6 +161,12 @@ export class AmqpTrigger implements INodeType {
let data = context.message;
+ if (options.jsonConvertByteArrayToString === true && data.body.content !== undefined) {
+ // The buffer is not ready... Stringify and parse back to load it.
+ 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) {
// The buffer is not ready... Stringify and parse back to load it.
const content = JSON.stringify(data.body.content);
@@ -158,6 +182,12 @@ export class AmqpTrigger implements INodeType {
self.emit([self.helpers.returnJsonArray([data])]);
+
+ if (context.receiver.credit === 0) {
+ setTimeout(() => {
+ context.receiver.add_credit(pullMessagesNumber);
+ }, options.sleepTime as number || 10);
+ }
});
const connection = container.connect(connectOptions);
@@ -170,14 +200,14 @@ export class AmqpTrigger implements INodeType {
durable: 2,
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,6 +216,8 @@ 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();
}
diff --git a/packages/nodes-base/nodes/EditImage.node.ts b/packages/nodes-base/nodes/EditImage.node.ts
index 9e10000ffda70..5c3b9c53aad1e 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/nodes-base/nodes/GetResponse/ContactDescription.ts b/packages/nodes-base/nodes/GetResponse/ContactDescription.ts
new file mode 100644
index 0000000000000..0a8e60cdfef4c
--- /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: '',
+ },
+ {
+ 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: '',
+ },
+ {
+ 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 0000000000000..f07d130da45e5
--- /dev/null
+++ b/packages/nodes-base/nodes/GetResponse/GenericFunctions.ts
@@ -0,0 +1,70 @@
+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 0000000000000..09e772e1ac864
--- /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 0000000000000..f533a87a64ce9
Binary files /dev/null and b/packages/nodes-base/nodes/GetResponse/getResponse.png differ
diff --git a/packages/nodes-base/nodes/Gotify/GenericFunctions.ts b/packages/nodes-base/nodes/Gotify/GenericFunctions.ts
new file mode 100644
index 0000000000000..ab694fe577c56
--- /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 0000000000000..1c6179efc62b0
--- /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 0000000000000..f38a25e88af63
Binary files /dev/null and b/packages/nodes-base/nodes/Gotify/gotify.png differ
diff --git a/packages/nodes-base/nodes/Hubspot/DealDescription.ts b/packages/nodes-base/nodes/Hubspot/DealDescription.ts
index 837c37a2388a0..28e2261fedfc9 100644
--- a/packages/nodes-base/nodes/Hubspot/DealDescription.ts
+++ b/packages/nodes-base/nodes/Hubspot/DealDescription.ts
@@ -101,16 +101,28 @@ export const dealFields = [
},
options: [
{
- displayName: 'Deal Name',
- name: 'dealName',
+ displayName: 'Amount',
+ name: 'amount',
type: 'string',
default: '',
},
{
- displayName: 'Pipeline',
- name: 'pipeline',
- type: 'string',
- default: '',
+ displayName: 'Associated Company',
+ name: 'associatedCompany',
+ type: 'multiOptions',
+ typeOptions: {
+ loadOptionsMethod:'getCompanies' ,
+ },
+ default: [],
+ },
+ {
+ displayName: 'Associated Vids',
+ name: 'associatedVids',
+ type: 'multiOptions',
+ typeOptions: {
+ loadOptionsMethod:'getContacts' ,
+ },
+ default: [],
},
{
displayName: 'Close Date',
@@ -119,8 +131,43 @@ export const dealFields = [
default: '',
},
{
- displayName: 'Amount',
- name: 'amount',
+ 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',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ displayName: 'Deal Name',
+ name: 'dealName',
type: 'string',
default: '',
},
@@ -134,22 +181,10 @@ export const dealFields = [
default: '',
},
{
- displayName: 'Associated Company',
- name: 'associatedCompany',
- type: 'multiOptions',
- typeOptions: {
- loadOptionsMethod:'getCompanies' ,
- },
- default: [],
- },
- {
- displayName: 'Associated Vids',
- name: 'associatedVids',
- type: 'multiOptions',
- typeOptions: {
- loadOptionsMethod:'getContacts' ,
- },
- default: [],
+ displayName: 'Pipeline',
+ name: 'pipeline',
+ type: 'string',
+ default: '',
},
],
},
@@ -191,6 +226,53 @@ export const dealFields = [
},
},
options: [
+ {
+ displayName: 'Amount',
+ name: 'amount',
+ type: 'string',
+ default: '',
+ },
+ {
+ displayName: 'Close Date',
+ name: 'closeDate',
+ type: 'dateTime',
+ 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',
+ },
+ ],
+ },
+ ],
+ },
{
displayName: 'Deal Name',
name: 'dealName',
@@ -208,24 +290,6 @@ export const dealFields = [
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',
- type: 'string',
- default: '',
- },
- {
- displayName: 'Close Date',
- name: 'closeDate',
- type: 'dateTime',
- default: '',
- },
- {
- displayName: 'Amount',
- name: 'amount',
- type: 'string',
- default: '',
- },
{
displayName: 'Deal Type',
name: 'dealType',
@@ -235,6 +299,12 @@ export const dealFields = [
},
default: '',
},
+ {
+ displayName: 'Pipeline',
+ name: 'pipeline',
+ type: 'string',
+ default: '',
+ },
],
},
/* -------------------------------------------------------------------------- */
diff --git a/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts
index aaae8c5895e82..e04a829d935a4 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);
}
diff --git a/packages/nodes-base/nodes/Jira/GenericFunctions.ts b/packages/nodes-base/nodes/Jira/GenericFunctions.ts
index 93cbdd5cc18dc..3d2198d34f57f 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 0418f0b42cd3f..98ecc8757f829 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;
@@ -130,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;
},
@@ -165,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;
},
@@ -184,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;
},
@@ -203,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;
},
@@ -241,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;
},
@@ -260,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;
},
@@ -277,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;
},
},
diff --git a/packages/nodes-base/nodes/Line/GenericFunctions.ts b/packages/nodes-base/nodes/Line/GenericFunctions.ts
new file mode 100644
index 0000000000000..cc95fadd37087
--- /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 0000000000000..8c50baa35481a
--- /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 0000000000000..f26dd68068b3c
--- /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 0000000000000..da40b65f5a6e7
Binary files /dev/null and b/packages/nodes-base/nodes/Line/line.png differ
diff --git a/packages/nodes-base/nodes/Mattermost/Mattermost.node.ts b/packages/nodes-base/nodes/Mattermost/Mattermost.node.ts
index e6ff3b86b2e1d..e4618f5919d9b 100644
--- a/packages/nodes-base/nodes/Mattermost/Mattermost.node.ts
+++ b/packages/nodes-base/nodes/Mattermost/Mattermost.node.ts
@@ -1520,11 +1520,11 @@ export class Mattermost implements INodeType {
const returnData: INodePropertyOptions[] = [];
let name: string;
for (const data of responseData) {
- if (data.delete_at !== 0) {
+ if (data.delete_at !== 0 || (!data.display_name || !data.name)) {
continue;
}
- name = `${data.name} (${data.type === 'O' ? 'public' : 'private'})`;
+ name = `${data.team_display_name} - ${data.display_name || data.name} (${data.type === 'O' ? 'public' : 'private'})`;
returnData.push({
name,
@@ -1532,6 +1532,12 @@ export class Mattermost implements INodeType {
});
}
+ returnData.sort((a, b) => {
+ if (a.name < b.name) { return -1; }
+ if (a.name > b.name) { return 1; }
+ return 0;
+ });
+
return returnData;
},
@@ -1548,17 +1554,18 @@ export class Mattermost implements INodeType {
const returnData: INodePropertyOptions[] = [];
let name: string;
for (const data of responseData) {
- if (data.delete_at !== 0) {
+ if (data.delete_at !== 0 || (!data.display_name || !data.name)) {
continue;
}
const channelTypes: IDataObject = {
+ 'D': 'direct',
+ 'G': 'group',
'O': 'public',
'P': 'private',
- 'D': 'direct',
};
- name = `${data.name} (${channelTypes[data.type as string]})`;
+ name = `${data.display_name} (${channelTypes[data.type as string]})`;
returnData.push({
name,
@@ -1566,6 +1573,12 @@ export class Mattermost implements INodeType {
});
}
+ returnData.sort((a, b) => {
+ if (a.name < b.name) { return -1; }
+ if (a.name > b.name) { return 1; }
+ return 0;
+ });
+
return returnData;
},
@@ -1593,6 +1606,12 @@ export class Mattermost implements INodeType {
});
}
+ returnData.sort((a, b) => {
+ if (a.name < b.name) { return -1; }
+ if (a.name > b.name) { return 1; }
+ return 0;
+ });
+
return returnData;
},
async getUsers(this: ILoadOptionsFunctions): Promise {
@@ -1616,6 +1635,12 @@ export class Mattermost implements INodeType {
});
}
+ returnData.sort((a, b) => {
+ if (a.name < b.name) { return -1; }
+ if (a.name > b.name) { return 1; }
+ return 0;
+ });
+
return returnData;
},
},
diff --git a/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts b/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts
index 46128fa8954ed..39516f94f11af 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);
diff --git a/packages/nodes-base/nodes/Pipedrive/PipedriveTrigger.node.ts b/packages/nodes-base/nodes/Pipedrive/PipedriveTrigger.node.ts
index e212fb0fed6a0..eb1b10f7470c3 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: 'httpBasicAuth',
+ name: 'pipedriveOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: [
+ 'oAuth2',
+ ],
+ },
+ },
+ },
+ {
+ name: 'httpBasicAuth',
+ required: true,
+ displayOptions: {
+ show: {
+ 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');
diff --git a/packages/nodes-base/nodes/Segment/IdentifyDescription.ts b/packages/nodes-base/nodes/Segment/IdentifyDescription.ts
index 44b2c60b4809b..72b78fef35bbd 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 b4ad23e9c1400..954193969e802 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 f32b03278cb5e..1e49c5e579a6c 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: '',
+ },
+ ],
+ },
+ ],
+ },
],
},
],
diff --git a/packages/nodes-base/nodes/Shopify/GenericFunctions.ts b/packages/nodes-base/nodes/Shopify/GenericFunctions.ts
index e808846a25e6d..6a7205e860229 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 f0d627d500268..e6c49d4cf04a7 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 {
diff --git a/packages/nodes-base/nodes/Strapi/EntryDescription.ts b/packages/nodes-base/nodes/Strapi/EntryDescription.ts
new file mode 100644
index 0000000000000..7305aa6ce7ebd
--- /dev/null
+++ b/packages/nodes-base/nodes/Strapi/EntryDescription.ts
@@ -0,0 +1,350 @@
+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 delete.',
+ },
+
+ /* -------------------------------------------------------------------------- */
+ /* 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 0000000000000..fbd7c57ff0a63
--- /dev/null
+++ b/packages/nodes-base/nodes/Strapi/GenericFunctions.ts
@@ -0,0 +1,103 @@
+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: {},
+ 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;
+ }
+
+ //@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 = {}, headers: 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, undefined, headers);
+ 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 0000000000000..74849e618756a
--- /dev/null
+++ b/packages/nodes-base/nodes/Strapi/Strapi.node.ts
@@ -0,0 +1,192 @@
+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 = {};
+ 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);
+
+ headers.Authorization = `Bearer ${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, undefined, headers);
+
+ 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, undefined, headers);
+
+ 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, headers);
+ } else {
+ qs._limit = this.getNodeParameter('limit', i) as number;
+
+ responseData = await strapiApiRequest.call(this, 'GET', `/${contentType}`, {}, qs, undefined, headers);
+ }
+ 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, undefined, headers);
+
+ 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, undefined, headers);
+
+ 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 0000000000000..bf9f95847a50b
--- /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 29a0b90bb7130..7214127df7b1b 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",
@@ -71,6 +71,8 @@
"dist/credentials/FileMaker.credentials.js",
"dist/credentials/FlowApi.credentials.js",
"dist/credentials/Ftp.credentials.js",
+ "dist/credentials/GetResponseApi.credentials.js",
+ "dist/credentials/GetResponseOAuth2Api.credentials.js",
"dist/credentials/GithubApi.credentials.js",
"dist/credentials/GithubOAuth2Api.credentials.js",
"dist/credentials/GitlabApi.credentials.js",
@@ -86,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",
@@ -105,6 +108,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",
@@ -176,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",
@@ -276,6 +281,7 @@
"dist/nodes/Flow/FlowTrigger.node.js",
"dist/nodes/Function.node.js",
"dist/nodes/FunctionItem.node.js",
+ "dist/nodes/GetResponse/GetResponse.node.js",
"dist/nodes/Github/Github.node.js",
"dist/nodes/Github/GithubTrigger.node.js",
"dist/nodes/Gitlab/Gitlab.node.js",
@@ -290,6 +296,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",
@@ -312,6 +319,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",
@@ -380,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",
@@ -448,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"
@@ -478,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",
diff --git a/packages/workflow/package.json b/packages/workflow/package.json
index bcaed2e5d45c7..cfe35eac2acad 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",
diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts
index bf0fc63e13115..492810cf2c921 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[];
}
@@ -535,6 +536,7 @@ export interface INodeTypeDescription {
version: number;
description: string;
defaults: INodeParameters;
+ documentationUrl?: string;
inputs: string[];
inputNames?: string[];
outputs: string[];