From c6aa73cc2f4598737e332f489db1a3a6abbbb81e Mon Sep 17 00:00:00 2001 From: Chris Lonardo Date: Sun, 5 Apr 2020 21:06:00 -0400 Subject: [PATCH 01/29] upgrade mongodb client to support mongodb 4.2 and allow conn string override --- package.json | 38 +-- packages/cli/package.json | 2 +- packages/editor-ui/src/main.ts | 2 +- .../nodes-base/nodes/MongoDb/MongoDb.node.ts | 245 ++++-------------- .../nodes/MongoDb/mongo.node.options.ts | 156 +++++++++++ .../nodes/MongoDb/mongo.node.utils.ts | 47 ++++ packages/nodes-base/package.json | 6 +- 7 files changed, 279 insertions(+), 217 deletions(-) create mode 100644 packages/nodes-base/nodes/MongoDb/mongo.node.options.ts create mode 100644 packages/nodes-base/nodes/MongoDb/mongo.node.utils.ts diff --git a/package.json b/package.json index b045830f51..e14010e7a8 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,22 @@ { - "name": "n8n", - "private": true, - "homepage": "https://n8n.io", - "scripts": { - "bootstrap": "lerna bootstrap --hoist --no-ci", - "build": "lerna exec npm run build", - "dev": "lerna exec npm run dev --parallel", - "start": "run-script-os", - "start:default": "cd packages/cli/bin && ./n8n", - "start:windows": "cd packages/cli/bin && n8n", - "test": "lerna run test", - "watch": "lerna run --parallel watch" - }, - "devDependencies": { - "lerna": "^3.13.1", - "run-script-os": "^1.0.7" - }, - "postcss": {} + "name": "n8n", + "private": true, + "homepage": "https://n8n.io", + "scripts": { + "bootstrap": "lerna bootstrap --hoist --no-ci", + "build": "lerna exec npm run build", + "dev": "lerna exec npm run dev --parallel", + "clean:dist": "lerna exec -- rimraf ./dist", + "start": "run-script-os", + "start:default": "cd packages/cli/bin && ./n8n", + "start:windows": "cd packages/cli/bin && n8n", + "test": "lerna run test", + "watch": "lerna run --parallel watch" + }, + "devDependencies": { + "lerna": "^3.13.1", + "rimraf": "^3.0.2", + "run-script-os": "^1.0.7" + }, + "postcss": {} } diff --git a/packages/cli/package.json b/packages/cli/package.json index aabfbb7f7a..386fce5dc1 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -93,7 +93,7 @@ "jwks-rsa": "^1.6.0", "localtunnel": "^2.0.0", "lodash.get": "^4.4.2", - "mongodb": "^3.2.3", + "mongodb": "^3.5.5", "mysql2": "^2.0.1", "n8n-core": "~0.29.0", "n8n-editor-ui": "~0.40.0", diff --git a/packages/editor-ui/src/main.ts b/packages/editor-ui/src/main.ts index f9a7f879a0..e1b30cd166 100644 --- a/packages/editor-ui/src/main.ts +++ b/packages/editor-ui/src/main.ts @@ -179,7 +179,7 @@ if (process.env.NODE_ENV !== 'production') { // not do anything about it anyway return; } - console.error('error cought in main.ts'); // eslint-disable-line no-console + console.error('error caught in main.ts'); // eslint-disable-line no-console console.error(message); // eslint-disable-line no-console console.error(error); // eslint-disable-line no-console }; diff --git a/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts b/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts index 0aa6d603ac..d15fd0112e 100644 --- a/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts +++ b/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts @@ -1,227 +1,85 @@ -import { IExecuteFunctions } from 'n8n-core'; +import { IExecuteFunctions } from "n8n-core"; import { IDataObject, INodeExecutionData, INodeType, INodeTypeDescription, -} from 'n8n-workflow'; - -import { MongoClient } from 'mongodb'; - - -/** - * Returns of copy of the items which only contains the json data and - * of that only the define properties - * - * @param {INodeExecutionData[]} items The items to copy - * @param {string[]} properties The properties it should include - * @returns - */ -function getItemCopy(items: INodeExecutionData[], properties: string[]): IDataObject[] { - // Prepare the data to insert and copy it to be returned - let newItem: IDataObject; - return items.map((item) => { - newItem = {}; - for (const property of properties) { - if (item.json[property] === undefined) { - newItem[property] = null; - } else { - newItem[property] = JSON.parse(JSON.stringify(item.json[property])); - } - } - return newItem; - }); -} - +} from "n8n-workflow"; +import { nodeDescription } from "./mongo.node.options"; +import { MongoClient } from "mongodb"; +import { getItemCopy, buildParameterizedConnString } from "./mongo.node.utils"; export class MongoDb implements INodeType { - description: INodeTypeDescription = { - displayName: 'MongoDB', - name: 'mongoDb', - icon: 'file:mongoDb.png', - group: ['input'], - version: 1, - description: 'Find, insert and update documents in MongoDB.', - defaults: { - name: 'MongoDB', - color: '#13AA52', - }, - inputs: ['main'], - outputs: ['main'], - credentials: [ - { - name: 'mongoDb', - required: true, - } - ], - properties: [ - { - displayName: 'Operation', - name: 'operation', - type: 'options', - options: [ - { - name: 'Find', - value: 'find', - description: 'Find documents.', - }, - { - name: 'Insert', - value: 'insert', - description: 'Insert documents.', - }, - { - name: 'Update', - value: 'update', - description: 'Updates documents.', - }, - ], - default: 'find', - description: 'The operation to perform.', - }, - - { - displayName: 'Collection', - name: 'collection', - type: 'string', - required: true, - default: '', - description: 'MongoDB Collection' - }, - - // ---------------------------------- - // find - // ---------------------------------- - { - displayName: 'Query (JSON format)', - name: 'query', - type: 'string', - typeOptions: { - rows: 5, - }, - displayOptions: { - show: { - operation: [ - 'find' - ], - }, - }, - default: '{}', - placeholder: `{ "birth": { "$gt": "1950-01-01" } }`, - required: true, - description: 'MongoDB Find query.', - }, - - - // ---------------------------------- - // insert - // ---------------------------------- - { - displayName: 'Fields', - name: 'fields', - type: 'string', - displayOptions: { - show: { - operation: [ - 'insert' - ], - }, - }, - default: '', - placeholder: 'name,description', - description: 'Comma separated list of the fields to be included into the new document.', - }, - - - // ---------------------------------- - // update - // ---------------------------------- - { - displayName: 'Update Key', - name: 'updateKey', - type: 'string', - displayOptions: { - show: { - 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: 'Fields', - name: 'fields', - type: 'string', - displayOptions: { - show: { - operation: [ - 'update' - ], - }, - }, - default: '', - placeholder: 'name,description', - description: 'Comma separated list of the fields to be included into the new document.', - }, - - ] - }; - + description: INodeTypeDescription = nodeDescription; async execute(this: IExecuteFunctions): Promise { + const credentials = this.getCredentials("mongoDb"); - const credentials = this.getCredentials('mongoDb'); + // user can optionally override connection string + const useOverriddenConnString = this.getNodeParameter( + "shouldOverrideConnString", + 0 + ) as boolean; if (credentials === undefined) { - throw new Error('No credentials got returned!'); + throw new Error("No credentials got returned!"); } - let connectionUri = ''; - - if (credentials.port) { - connectionUri = `mongodb://${credentials.user}:${credentials.password}@${credentials.host}:${credentials.port}`; + let connectionUri = ""; + if (useOverriddenConnString === true) { + const connStringInput = this.getNodeParameter( + "connStringOverrideVal", + 0 + ) as string; + if (connStringInput && connStringInput.length > 0) { + connectionUri = connStringInput; + } else { + throw new Error( + "Cannot override credentials: valid MongoDB connection string not provided " + ); + } } else { - connectionUri = `mongodb+srv://${credentials.user}:${credentials.password}@${credentials.host}`; + connectionUri = buildParameterizedConnString(credentials); } - const client = await MongoClient.connect(connectionUri, { useNewUrlParser: true, useUnifiedTopology: true }); + const client: MongoClient = await MongoClient.connect(connectionUri, { + useNewUrlParser: true, + useUnifiedTopology: true, + }); + const mdb = client.db(credentials.database as string); let returnItems = []; const items = this.getInputData(); - const operation = this.getNodeParameter('operation', 0) as string; + const operation = this.getNodeParameter("operation", 0) as string; - if (operation === 'find') { + if (operation === "find") { // ---------------------------------- // find // ---------------------------------- const queryResult = await mdb - .collection(this.getNodeParameter('collection', 0) as string) - .find(JSON.parse(this.getNodeParameter('query', 0) as string)) + .collection(this.getNodeParameter("collection", 0) as string) + .find(JSON.parse(this.getNodeParameter("query", 0) as string)) .toArray(); returnItems = this.helpers.returnJsonArray(queryResult as IDataObject[]); - - } else if (operation === 'insert') { + } else if (operation === "insert") { // ---------------------------------- // insert // ---------------------------------- // Prepare the data to insert and copy it to be returned - const fields = (this.getNodeParameter('fields', 0) as string) - .split(',') - .map(f => f.trim()) - .filter(f => !!f); + const fields = (this.getNodeParameter("fields", 0) as string) + .split(",") + .map((f) => f.trim()) + .filter((f) => !!f); const insertItems = getItemCopy(items, fields); const { insertedIds } = await mdb - .collection(this.getNodeParameter('collection', 0) as string) + .collection(this.getNodeParameter("collection", 0) as string) .insertMany(insertItems); // Add the id to the data @@ -230,20 +88,20 @@ export class MongoDb implements INodeType { json: { ...insertItems[parseInt(i, 10)], id: insertedIds[parseInt(i, 10)] as string, - } + }, }); } - } else if (operation === 'update') { + } else if (operation === "update") { // ---------------------------------- // update // ---------------------------------- - const fields = (this.getNodeParameter('fields', 0) as string) - .split(',') - .map(f => f.trim()) - .filter(f => !!f); + const fields = (this.getNodeParameter("fields", 0) as string) + .split(",") + .map((f) => f.trim()) + .filter((f) => !!f); - let updateKey = this.getNodeParameter('updateKey', 0) as string; + let updateKey = this.getNodeParameter("updateKey", 0) as string; updateKey = updateKey.trim(); if (!fields.includes(updateKey)) { @@ -258,16 +116,15 @@ export class MongoDb implements INodeType { continue; } - const filter: { [key: string] :string } = {}; + const filter: { [key: string]: string } = {}; filter[updateKey] = item[updateKey] as string; await mdb - .collection(this.getNodeParameter('collection', 0) as string) + .collection(this.getNodeParameter("collection", 0) as string) .updateOne(filter, { $set: item }); } returnItems = this.helpers.returnJsonArray(updateItems as IDataObject[]); - } else { throw new Error(`The operation "${operation}" is not supported!`); } diff --git a/packages/nodes-base/nodes/MongoDb/mongo.node.options.ts b/packages/nodes-base/nodes/MongoDb/mongo.node.options.ts new file mode 100644 index 0000000000..19f18f4ab7 --- /dev/null +++ b/packages/nodes-base/nodes/MongoDb/mongo.node.options.ts @@ -0,0 +1,156 @@ +import { INodeTypeDescription } from "n8n-workflow"; + +/** + * Options to be displayed + */ +export const nodeDescription: INodeTypeDescription = { + displayName: "MongoDB", + name: "mongoDb", + icon: "file:mongoDb.png", + group: ["input"], + version: 1, + description: "Find, insert and update documents in MongoDB.", + defaults: { + name: "MongoDB", + color: "#13AA52", + }, + inputs: ["main"], + outputs: ["main"], + credentials: [ + { + name: "mongoDb", + required: true, + }, + ], + properties: [ + { + displayName: "Override conn string", + name: "shouldOverrideConnString", + type: "boolean", + default: false, + description: + "Whether to override the generated connection string. Credentials will also be ignored in this case.", + }, + { + displayName: "Conn string", + name: "connStringOverrideVal", + type: "string", + typeOptions: { + rows: 1, + }, + displayOptions: { + show: { + shouldOverrideConnString: [true], + }, + }, + default: "", + placeholder: `mongodb://USERNAMEHERE:PASSWORDHERE@localhost:27017/?authSource=admin&readPreference=primary&appname=n8n&ssl=false`, + required: false, + description: `If "Override conn string" is checked, the value here will be used as a MongoDB connection string, and the MongoDB credentials will be ignored`, + }, + { + displayName: "Operation", + name: "operation", + type: "options", + options: [ + { + name: "Find", + value: "find", + description: "Find documents.", + }, + { + name: "Insert", + value: "insert", + description: "Insert documents.", + }, + { + name: "Update", + value: "update", + description: "Updates documents.", + }, + ], + default: "find", + description: "The operation to perform.", + }, + + { + displayName: "Collection", + name: "collection", + type: "string", + required: true, + default: "", + description: "MongoDB Collection", + }, + + // ---------------------------------- + // find + // ---------------------------------- + { + displayName: "Query (JSON format)", + name: "query", + type: "string", + typeOptions: { + rows: 5, + }, + displayOptions: { + show: { + operation: ["find"], + }, + }, + default: "{}", + placeholder: `{ "birth": { "$gt": "1950-01-01" } }`, + required: true, + description: "MongoDB Find query.", + }, + + // ---------------------------------- + // insert + // ---------------------------------- + { + displayName: "Fields", + name: "fields", + type: "string", + displayOptions: { + show: { + operation: ["insert"], + }, + }, + default: "", + placeholder: "name,description", + description: + "Comma separated list of the fields to be included into the new document.", + }, + + // ---------------------------------- + // update + // ---------------------------------- + { + displayName: "Update Key", + name: "updateKey", + type: "string", + displayOptions: { + show: { + 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: "Fields", + name: "fields", + type: "string", + displayOptions: { + show: { + operation: ["update"], + }, + }, + default: "", + placeholder: "name,description", + description: + "Comma separated list of the fields to be included into the new document.", + }, + ], +}; diff --git a/packages/nodes-base/nodes/MongoDb/mongo.node.utils.ts b/packages/nodes-base/nodes/MongoDb/mongo.node.utils.ts new file mode 100644 index 0000000000..7de725f08a --- /dev/null +++ b/packages/nodes-base/nodes/MongoDb/mongo.node.utils.ts @@ -0,0 +1,47 @@ +import { + IDataObject, + INodeExecutionData, + ICredentialDataDecryptedObject, +} from "n8n-workflow"; + +/** + * Standard way of building the MongoDB connection string, unless overridden with a provided string + * + * @param {ICredentialDataDecryptedObject} credentials MongoDB credentials to use, unless conn string is overridden + */ +export function buildParameterizedConnString( + credentials: ICredentialDataDecryptedObject +): string { + if (credentials.port) { + return `mongodb://${credentials.user}:${credentials.password}@${credentials.host}:${credentials.port}`; + } else { + return `mongodb+srv://${credentials.user}:${credentials.password}@${credentials.host}`; + } +} + +/** + * Returns of copy of the items which only contains the json data and + * of that only the define properties + * + * @param {INodeExecutionData[]} items The items to copy + * @param {string[]} properties The properties it should include + * @returns + */ +export function getItemCopy( + items: INodeExecutionData[], + properties: string[] +): IDataObject[] { + // Prepare the data to insert and copy it to be returned + let newItem: IDataObject; + return items.map((item) => { + newItem = {}; + for (const property of properties) { + if (item.json[property] === undefined) { + newItem[property] = null; + } else { + newItem[property] = JSON.parse(JSON.stringify(item.json[property])); + } + } + return newItem; + }); +} diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index e197bd6ca4..b1a3cbda7b 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -75,7 +75,7 @@ "dist/credentials/MandrillApi.credentials.js", "dist/credentials/MattermostApi.credentials.js", "dist/credentials/MauticApi.credentials.js", - "dist/credentials/MoceanApi.credentials.js", + "dist/credentials/MoceanApi.credentials.js", "dist/credentials/MondayComApi.credentials.js", "dist/credentials/MongoDb.credentials.js", "dist/credentials/Msg91Api.credentials.js", @@ -255,7 +255,7 @@ "@types/jest": "^24.0.18", "@types/lodash.set": "^4.3.6", "@types/moment-timezone": "^0.5.12", - "@types/mongodb": "^3.3.6", + "@types/mongodb": "^3.5.4", "@types/node": "^10.10.1", "@types/nodemailer": "^4.6.5", "@types/redis": "^2.8.11", @@ -284,7 +284,7 @@ "lodash.get": "^4.4.2", "lodash.set": "^4.3.2", "lodash.unset": "^4.5.2", - "mongodb": "^3.3.2", + "mongodb": "^3.5.5", "mysql2": "^2.0.1", "n8n-core": "~0.29.0", "nodemailer": "^5.1.1", From 00fc6e598025022edca6b9461e1d5c9caaf85060 Mon Sep 17 00:00:00 2001 From: Chris Lonardo Date: Mon, 6 Apr 2020 14:04:59 -0400 Subject: [PATCH 02/29] enhance .editorconfig and apply formatting cleanup to last commit --- .editorconfig | 7 + package.json | 40 ++--- .../nodes-base/nodes/MongoDb/MongoDb.node.ts | 64 +++---- .../nodes/MongoDb/mongo.node.options.ts | 156 +++++++++--------- .../nodes/MongoDb/mongo.node.utils.ts | 6 +- 5 files changed, 140 insertions(+), 133 deletions(-) diff --git a/.editorconfig b/.editorconfig index bec7553240..5d02a5688b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,3 +6,10 @@ indent_style = tab end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true + +[package.json] +indent_style = space +indent_size = 2 + +[*.ts] +quote_type = single \ No newline at end of file diff --git a/package.json b/package.json index e14010e7a8..0c7d8dac15 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,22 @@ { - "name": "n8n", - "private": true, - "homepage": "https://n8n.io", - "scripts": { - "bootstrap": "lerna bootstrap --hoist --no-ci", - "build": "lerna exec npm run build", - "dev": "lerna exec npm run dev --parallel", - "clean:dist": "lerna exec -- rimraf ./dist", - "start": "run-script-os", - "start:default": "cd packages/cli/bin && ./n8n", - "start:windows": "cd packages/cli/bin && n8n", - "test": "lerna run test", - "watch": "lerna run --parallel watch" - }, - "devDependencies": { - "lerna": "^3.13.1", - "rimraf": "^3.0.2", - "run-script-os": "^1.0.7" - }, - "postcss": {} + "name": "n8n", + "private": true, + "homepage": "https://n8n.io", + "scripts": { + "bootstrap": "lerna bootstrap --hoist --no-ci", + "build": "lerna exec npm run build", + "dev": "lerna exec npm run dev --parallel", + "clean:dist": "lerna exec -- rimraf ./dist", + "start": "run-script-os", + "start:default": "cd packages/cli/bin && ./n8n", + "start:windows": "cd packages/cli/bin && n8n", + "test": "lerna run test", + "watch": "lerna run --parallel watch" + }, + "devDependencies": { + "lerna": "^3.13.1", + "rimraf": "^3.0.2", + "run-script-os": "^1.0.7" + }, + "postcss": {} } diff --git a/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts b/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts index d15fd0112e..7dc8daa957 100644 --- a/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts +++ b/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts @@ -1,41 +1,41 @@ -import { IExecuteFunctions } from "n8n-core"; +import { IExecuteFunctions } from 'n8n-core'; import { IDataObject, INodeExecutionData, INodeType, - INodeTypeDescription, -} from "n8n-workflow"; -import { nodeDescription } from "./mongo.node.options"; -import { MongoClient } from "mongodb"; -import { getItemCopy, buildParameterizedConnString } from "./mongo.node.utils"; + INodeTypeDescription +} from 'n8n-workflow'; +import { nodeDescription } from './mongo.node.options'; +import { MongoClient } from 'mongodb'; +import { getItemCopy, buildParameterizedConnString } from './mongo.node.utils'; export class MongoDb implements INodeType { description: INodeTypeDescription = nodeDescription; async execute(this: IExecuteFunctions): Promise { - const credentials = this.getCredentials("mongoDb"); + const credentials = this.getCredentials('mongoDb'); // user can optionally override connection string const useOverriddenConnString = this.getNodeParameter( - "shouldOverrideConnString", + 'shouldOverrideConnString', 0 ) as boolean; if (credentials === undefined) { - throw new Error("No credentials got returned!"); + throw new Error('No credentials got returned!'); } - let connectionUri = ""; + let connectionUri = ''; if (useOverriddenConnString === true) { const connStringInput = this.getNodeParameter( - "connStringOverrideVal", + 'connStringOverrideVal', 0 ) as string; if (connStringInput && connStringInput.length > 0) { connectionUri = connStringInput; } else { throw new Error( - "Cannot override credentials: valid MongoDB connection string not provided " + 'Cannot override credentials: valid MongoDB connection string not provided ' ); } } else { @@ -44,7 +44,7 @@ export class MongoDb implements INodeType { const client: MongoClient = await MongoClient.connect(connectionUri, { useNewUrlParser: true, - useUnifiedTopology: true, + useUnifiedTopology: true }); const mdb = client.db(credentials.database as string); @@ -52,34 +52,34 @@ export class MongoDb implements INodeType { let returnItems = []; const items = this.getInputData(); - const operation = this.getNodeParameter("operation", 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; - if (operation === "find") { + if (operation === 'find') { // ---------------------------------- // find // ---------------------------------- const queryResult = await mdb - .collection(this.getNodeParameter("collection", 0) as string) - .find(JSON.parse(this.getNodeParameter("query", 0) as string)) + .collection(this.getNodeParameter('collection', 0) as string) + .find(JSON.parse(this.getNodeParameter('query', 0) as string)) .toArray(); returnItems = this.helpers.returnJsonArray(queryResult as IDataObject[]); - } else if (operation === "insert") { + } else if (operation === 'insert') { // ---------------------------------- // insert // ---------------------------------- // Prepare the data to insert and copy it to be returned - const fields = (this.getNodeParameter("fields", 0) as string) - .split(",") - .map((f) => f.trim()) - .filter((f) => !!f); + const fields = (this.getNodeParameter('fields', 0) as string) + .split(',') + .map(f => f.trim()) + .filter(f => !!f); const insertItems = getItemCopy(items, fields); const { insertedIds } = await mdb - .collection(this.getNodeParameter("collection", 0) as string) + .collection(this.getNodeParameter('collection', 0) as string) .insertMany(insertItems); // Add the id to the data @@ -87,21 +87,21 @@ export class MongoDb implements INodeType { returnItems.push({ json: { ...insertItems[parseInt(i, 10)], - id: insertedIds[parseInt(i, 10)] as string, - }, + id: insertedIds[parseInt(i, 10)] as string + } }); } - } else if (operation === "update") { + } else if (operation === 'update') { // ---------------------------------- // update // ---------------------------------- - const fields = (this.getNodeParameter("fields", 0) as string) - .split(",") - .map((f) => f.trim()) - .filter((f) => !!f); + const fields = (this.getNodeParameter('fields', 0) as string) + .split(',') + .map(f => f.trim()) + .filter(f => !!f); - let updateKey = this.getNodeParameter("updateKey", 0) as string; + let updateKey = this.getNodeParameter('updateKey', 0) as string; updateKey = updateKey.trim(); if (!fields.includes(updateKey)) { @@ -120,7 +120,7 @@ export class MongoDb implements INodeType { filter[updateKey] = item[updateKey] as string; await mdb - .collection(this.getNodeParameter("collection", 0) as string) + .collection(this.getNodeParameter('collection', 0) as string) .updateOne(filter, { $set: item }); } diff --git a/packages/nodes-base/nodes/MongoDb/mongo.node.options.ts b/packages/nodes-base/nodes/MongoDb/mongo.node.options.ts index 19f18f4ab7..4a98eeabd5 100644 --- a/packages/nodes-base/nodes/MongoDb/mongo.node.options.ts +++ b/packages/nodes-base/nodes/MongoDb/mongo.node.options.ts @@ -1,156 +1,156 @@ -import { INodeTypeDescription } from "n8n-workflow"; +import { INodeTypeDescription } from 'n8n-workflow'; /** * Options to be displayed */ export const nodeDescription: INodeTypeDescription = { - displayName: "MongoDB", - name: "mongoDb", - icon: "file:mongoDb.png", - group: ["input"], + displayName: 'MongoDB', + name: 'mongoDb', + icon: 'file:mongoDb.png', + group: ['input'], version: 1, - description: "Find, insert and update documents in MongoDB.", + description: 'Find, insert and update documents in MongoDB.', defaults: { - name: "MongoDB", - color: "#13AA52", + name: 'MongoDB', + color: '#13AA52' }, - inputs: ["main"], - outputs: ["main"], + inputs: ['main'], + outputs: ['main'], credentials: [ { - name: "mongoDb", - required: true, - }, + name: 'mongoDb', + required: true + } ], properties: [ { - displayName: "Override conn string", - name: "shouldOverrideConnString", - type: "boolean", + displayName: 'Override conn string', + name: 'shouldOverrideConnString', + type: 'boolean', default: false, description: - "Whether to override the generated connection string. Credentials will also be ignored in this case.", + 'Whether to override the generated connection string. Credentials will also be ignored in this case.' }, { - displayName: "Conn string", - name: "connStringOverrideVal", - type: "string", + displayName: 'Conn string', + name: 'connStringOverrideVal', + type: 'string', typeOptions: { - rows: 1, + rows: 1 }, displayOptions: { show: { - shouldOverrideConnString: [true], - }, + shouldOverrideConnString: [true] + } }, - default: "", + default: '', placeholder: `mongodb://USERNAMEHERE:PASSWORDHERE@localhost:27017/?authSource=admin&readPreference=primary&appname=n8n&ssl=false`, required: false, - description: `If "Override conn string" is checked, the value here will be used as a MongoDB connection string, and the MongoDB credentials will be ignored`, + description: `If "Override conn string" is checked, the value here will be used as a MongoDB connection string, and the MongoDB credentials will be ignored` }, { - displayName: "Operation", - name: "operation", - type: "options", + displayName: 'Operation', + name: 'operation', + type: 'options', options: [ { - name: "Find", - value: "find", - description: "Find documents.", + name: 'Find', + value: 'find', + description: 'Find documents.' }, { - name: "Insert", - value: "insert", - description: "Insert documents.", + name: 'Insert', + value: 'insert', + description: 'Insert documents.' }, { - name: "Update", - value: "update", - description: "Updates documents.", - }, + name: 'Update', + value: 'update', + description: 'Updates documents.' + } ], - default: "find", - description: "The operation to perform.", + default: 'find', + description: 'The operation to perform.' }, { - displayName: "Collection", - name: "collection", - type: "string", + displayName: 'Collection', + name: 'collection', + type: 'string', required: true, - default: "", - description: "MongoDB Collection", + default: '', + description: 'MongoDB Collection' }, // ---------------------------------- // find // ---------------------------------- { - displayName: "Query (JSON format)", - name: "query", - type: "string", + displayName: 'Query (JSON format)', + name: 'query', + type: 'string', typeOptions: { - rows: 5, + rows: 5 }, displayOptions: { show: { - operation: ["find"], - }, + operation: ['find'] + } }, - default: "{}", + default: '{}', placeholder: `{ "birth": { "$gt": "1950-01-01" } }`, required: true, - description: "MongoDB Find query.", + description: 'MongoDB Find query.' }, // ---------------------------------- // insert // ---------------------------------- { - displayName: "Fields", - name: "fields", - type: "string", + displayName: 'Fields', + name: 'fields', + type: 'string', displayOptions: { show: { - operation: ["insert"], - }, + operation: ['insert'] + } }, - default: "", - placeholder: "name,description", + default: '', + placeholder: 'name,description', description: - "Comma separated list of the fields to be included into the new document.", + 'Comma separated list of the fields to be included into the new document.' }, // ---------------------------------- // update // ---------------------------------- { - displayName: "Update Key", - name: "updateKey", - type: "string", + displayName: 'Update Key', + name: 'updateKey', + type: 'string', displayOptions: { show: { - operation: ["update"], - }, + operation: ['update'] + } }, - default: "id", + default: 'id', required: true, description: - 'Name of the property which decides which rows in the database should be updated. Normally that would be "id".', + 'Name of the property which decides which rows in the database should be updated. Normally that would be "id".' }, { - displayName: "Fields", - name: "fields", - type: "string", + displayName: 'Fields', + name: 'fields', + type: 'string', displayOptions: { show: { - operation: ["update"], - }, + operation: ['update'] + } }, - default: "", - placeholder: "name,description", + default: '', + placeholder: 'name,description', description: - "Comma separated list of the fields to be included into the new document.", - }, - ], + 'Comma separated list of the fields to be included into the new document.' + } + ] }; diff --git a/packages/nodes-base/nodes/MongoDb/mongo.node.utils.ts b/packages/nodes-base/nodes/MongoDb/mongo.node.utils.ts index 7de725f08a..54cae7d53c 100644 --- a/packages/nodes-base/nodes/MongoDb/mongo.node.utils.ts +++ b/packages/nodes-base/nodes/MongoDb/mongo.node.utils.ts @@ -1,8 +1,8 @@ import { IDataObject, INodeExecutionData, - ICredentialDataDecryptedObject, -} from "n8n-workflow"; + ICredentialDataDecryptedObject +} from 'n8n-workflow'; /** * Standard way of building the MongoDB connection string, unless overridden with a provided string @@ -33,7 +33,7 @@ export function getItemCopy( ): IDataObject[] { // Prepare the data to insert and copy it to be returned let newItem: IDataObject; - return items.map((item) => { + return items.map(item => { newItem = {}; for (const property of properties) { if (item.json[property] === undefined) { From d80deda34fc8271f08256cba8c0bf5928c5691fd Mon Sep 17 00:00:00 2001 From: Chris Lonardo Date: Mon, 6 Apr 2020 21:07:54 -0400 Subject: [PATCH 03/29] Moved MongoDb connection string override into credentials (#1) * interim work on credentials refactor * move conn string override for mongodb into credentials and enhance credentials typings --- .../credentials/MongoDb.credentials.ts | 39 +++++++++--- .../nodes-base/nodes/MongoDb/MongoDb.node.ts | 40 +++--------- .../nodes/MongoDb/mongo.node.options.ts | 25 -------- .../nodes/MongoDb/mongo.node.types.ts | 52 ++++++++++++++++ .../nodes/MongoDb/mongo.node.utils.ts | 61 ++++++++++++++++++- 5 files changed, 149 insertions(+), 68 deletions(-) create mode 100644 packages/nodes-base/nodes/MongoDb/mongo.node.types.ts diff --git a/packages/nodes-base/credentials/MongoDb.credentials.ts b/packages/nodes-base/credentials/MongoDb.credentials.ts index b39c7016de..436e9f2e4f 100644 --- a/packages/nodes-base/credentials/MongoDb.credentials.ts +++ b/packages/nodes-base/credentials/MongoDb.credentials.ts @@ -1,8 +1,4 @@ -import { - ICredentialType, - NodePropertyTypes, -} from 'n8n-workflow'; - +import { ICredentialType, NodePropertyTypes } from 'n8n-workflow'; export class MongoDb implements ICredentialType { name = 'mongoDb'; @@ -12,34 +8,57 @@ export class MongoDb implements ICredentialType { displayName: 'Host', name: 'host', type: 'string' as NodePropertyTypes, - default: 'localhost', + default: 'localhost' }, { displayName: 'Database', name: 'database', type: 'string' as NodePropertyTypes, default: '', + description: + 'Note: the database should still be provided even if using an override connection string' }, { displayName: 'User', name: 'user', type: 'string' as NodePropertyTypes, - default: '', + default: '' }, { displayName: 'Password', name: 'password', type: 'string' as NodePropertyTypes, typeOptions: { - password: true, + password: true }, - default: '', + default: '' }, { displayName: 'Port', name: 'port', type: 'number' as NodePropertyTypes, - default: 27017, + default: 27017 }, + { + displayName: 'Override conn string', + name: 'shouldOverrideConnString', + type: 'boolean' as NodePropertyTypes, + default: false, + required: false, + description: + 'Whether to override the generated connection string. Credentials will also be ignored in this case.' + }, + { + displayName: 'Conn string override', + name: 'connStringOverrideVal', + type: 'string' as NodePropertyTypes, + typeOptions: { + rows: 1 + }, + default: '', + placeholder: `mongodb://USERNAMEHERE:PASSWORDHERE@localhost:27017/?authSource=admin&readPreference=primary&appname=n8n&ssl=false`, + required: false, + description: `If provided, the value here will be used as a MongoDB connection string, and the MongoDB credentials will be ignored` + } ]; } diff --git a/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts b/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts index 7dc8daa957..dbd7ab2929 100644 --- a/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts +++ b/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts @@ -7,47 +7,25 @@ import { } from 'n8n-workflow'; import { nodeDescription } from './mongo.node.options'; import { MongoClient } from 'mongodb'; -import { getItemCopy, buildParameterizedConnString } from './mongo.node.utils'; +import { + getItemCopy, + validateAndResolveMongoCredentials +} from './mongo.node.utils'; export class MongoDb implements INodeType { description: INodeTypeDescription = nodeDescription; async execute(this: IExecuteFunctions): Promise { - const credentials = this.getCredentials('mongoDb'); + const { database, connectionString } = validateAndResolveMongoCredentials( + this.getCredentials('mongoDb') + ); - // user can optionally override connection string - const useOverriddenConnString = this.getNodeParameter( - 'shouldOverrideConnString', - 0 - ) as boolean; - - if (credentials === undefined) { - throw new Error('No credentials got returned!'); - } - - let connectionUri = ''; - if (useOverriddenConnString === true) { - const connStringInput = this.getNodeParameter( - 'connStringOverrideVal', - 0 - ) as string; - if (connStringInput && connStringInput.length > 0) { - connectionUri = connStringInput; - } else { - throw new Error( - 'Cannot override credentials: valid MongoDB connection string not provided ' - ); - } - } else { - connectionUri = buildParameterizedConnString(credentials); - } - - const client: MongoClient = await MongoClient.connect(connectionUri, { + const client: MongoClient = await MongoClient.connect(connectionString, { useNewUrlParser: true, useUnifiedTopology: true }); - const mdb = client.db(credentials.database as string); + const mdb = client.db(database as string); let returnItems = []; diff --git a/packages/nodes-base/nodes/MongoDb/mongo.node.options.ts b/packages/nodes-base/nodes/MongoDb/mongo.node.options.ts index 4a98eeabd5..190ff3de88 100644 --- a/packages/nodes-base/nodes/MongoDb/mongo.node.options.ts +++ b/packages/nodes-base/nodes/MongoDb/mongo.node.options.ts @@ -23,31 +23,6 @@ export const nodeDescription: INodeTypeDescription = { } ], properties: [ - { - displayName: 'Override conn string', - name: 'shouldOverrideConnString', - type: 'boolean', - default: false, - description: - 'Whether to override the generated connection string. Credentials will also be ignored in this case.' - }, - { - displayName: 'Conn string', - name: 'connStringOverrideVal', - type: 'string', - typeOptions: { - rows: 1 - }, - displayOptions: { - show: { - shouldOverrideConnString: [true] - } - }, - default: '', - placeholder: `mongodb://USERNAMEHERE:PASSWORDHERE@localhost:27017/?authSource=admin&readPreference=primary&appname=n8n&ssl=false`, - required: false, - description: `If "Override conn string" is checked, the value here will be used as a MongoDB connection string, and the MongoDB credentials will be ignored` - }, { displayName: 'Operation', name: 'operation', diff --git a/packages/nodes-base/nodes/MongoDb/mongo.node.types.ts b/packages/nodes-base/nodes/MongoDb/mongo.node.types.ts new file mode 100644 index 0000000000..2995fdd2a2 --- /dev/null +++ b/packages/nodes-base/nodes/MongoDb/mongo.node.types.ts @@ -0,0 +1,52 @@ +import { CredentialInformation } from 'n8n-workflow'; + +/** + * Credentials object for Mongo, if using individual parameters + */ +export interface IMongoParametricCredentials { + /** + * Whether to allow overriding the parametric credentials with a connection string + */ + shouldOverrideConnString: false | undefined | null; + host: string; + database: string; + user: string; + password: string; + port?: number; +} + +/** + * Credentials object for Mongo, if using override connection string + */ +export interface IMongoOverrideCredentials { + /** + * Whether to allow overriding the parametric credentials with a connection string + */ + shouldOverrideConnString: true; + /** + * If using an override connection string, this is where it will be. + */ + connStringOverrideVal: string; + database: string; +} + +/** + * Unified credential object type (whether params are overridden with a connection string or not) + */ +export type IMongoCredentialsType = + | IMongoParametricCredentials + | IMongoOverrideCredentials; + +/** + * Resolve the database and connection string from input credentials + */ +export type IMongoCredentials = { + /** + * Database name (used to create the Mongo client) + */ + database: string; + /** + * Generated connection string (after validating and figuring out overrides) + */ + connectionString: string; +}; diff --git a/packages/nodes-base/nodes/MongoDb/mongo.node.utils.ts b/packages/nodes-base/nodes/MongoDb/mongo.node.utils.ts index 54cae7d53c..1d682a450a 100644 --- a/packages/nodes-base/nodes/MongoDb/mongo.node.utils.ts +++ b/packages/nodes-base/nodes/MongoDb/mongo.node.utils.ts @@ -3,14 +3,19 @@ import { INodeExecutionData, ICredentialDataDecryptedObject } from 'n8n-workflow'; +import { + IMongoCredentialsType, + IMongoParametricCredentials, + IMongoCredentials +} from './mongo.node.types'; /** * Standard way of building the MongoDB connection string, unless overridden with a provided string * * @param {ICredentialDataDecryptedObject} credentials MongoDB credentials to use, unless conn string is overridden */ -export function buildParameterizedConnString( - credentials: ICredentialDataDecryptedObject +function buildParameterizedConnString( + credentials: IMongoParametricCredentials ): string { if (credentials.port) { return `mongodb://${credentials.user}:${credentials.password}@${credentials.host}:${credentials.port}`; @@ -19,6 +24,58 @@ export function buildParameterizedConnString( } } +/** + * Build mongoDb connection string and resolve database name. + * If a connection string override value is provided, that will be used in place of individual args + * + * @param {ICredentialDataDecryptedObject} credentials raw/input MongoDB credentials to use + */ +function buildMongoConnectionParams( + credentials: IMongoCredentialsType +): IMongoCredentials { + const sanitizedDbName = + credentials.database && credentials.database.trim().length > 0 + ? credentials.database.trim() + : ''; + if (credentials.shouldOverrideConnString) { + if ( + credentials.connStringOverrideVal && + credentials.connStringOverrideVal.trim().length > 0 + ) { + return { + connectionString: credentials.connStringOverrideVal.trim(), + database: sanitizedDbName + }; + } else { + throw new Error( + 'Cannot override credentials: valid MongoDB connection string not provided ' + ); + } + } else { + return { + connectionString: buildParameterizedConnString(credentials), + database: sanitizedDbName + }; + } +} + +/** + * Verify credentials. If ok, build mongoDb connection string and resolve database name. + * + * @param {ICredentialDataDecryptedObject} credentials raw/input MongoDB credentials to use + */ +export function validateAndResolveMongoCredentials( + credentials?: ICredentialDataDecryptedObject +): IMongoCredentials { + if (credentials == undefined) { + throw new Error('No credentials got returned!'); + } else { + return buildMongoConnectionParams( + (credentials as any) as IMongoCredentialsType + ); + } +} + /** * Returns of copy of the items which only contains the json data and * of that only the define properties From d57d45714836a5688a3fc9f1f28b3e90a401d264 Mon Sep 17 00:00:00 2001 From: ricardo Date: Sun, 26 Apr 2020 12:51:20 -0500 Subject: [PATCH 04/29] :sparkles: SurveyMonkey trigger --- packages/cli/src/Server.ts | 43 +- .../SurveyMonkeyApi.credentials.ts | 35 ++ .../nodes/SurveyMonkey/GenericFunctions.ts | 82 ++++ .../SurveyMonkey/SurveyMonkeyTrigger.node.ts | 433 ++++++++++++++++++ .../nodes/SurveyMonkey/surveyMonkey.png | Bin 0 -> 3327 bytes packages/nodes-base/package.json | 2 + packages/workflow/src/Interfaces.ts | 2 +- 7 files changed, 593 insertions(+), 4 deletions(-) create mode 100644 packages/nodes-base/credentials/SurveyMonkeyApi.credentials.ts create mode 100644 packages/nodes-base/nodes/SurveyMonkey/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts create mode 100644 packages/nodes-base/nodes/SurveyMonkey/surveyMonkey.png diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index a98def2cbe..b6a07aab44 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -1083,7 +1083,6 @@ class App { return returnData; })); - // Forces the execution to stop this.app.post('/rest/executions-current/:id/stop', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { const executionId = req.params.id; @@ -1151,6 +1150,26 @@ class App { // Webhooks // ---------------------------------------- + // HEAD webhook requests + this.app.head(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => { + // Cut away the "/webhook/" to get the registred part of the url + const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhook.length + 2); + + let response; + try { + response = await this.activeWorkflowRunner.executeWebhook('HEAD', requestUrl, req, res); + } catch (error) { + ResponseHelper.sendErrorResponse(res, error); + return; + } + + if (response.noWebhookResponse === true) { + // Nothing else to do as the response got already sent + return; + } + + ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); + }); // GET webhook requests this.app.get(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => { @@ -1173,7 +1192,6 @@ class App { ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); }); - // POST webhook requests this.app.post(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => { // Cut away the "/webhook/" to get the registred part of the url @@ -1195,6 +1213,26 @@ class App { ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); }); + // HEAD webhook requests (test for UI) + this.app.head(`/${this.endpointWebhookTest}/*`, async (req: express.Request, res: express.Response) => { + // Cut away the "/webhook-test/" to get the registred part of the url + const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhookTest.length + 2); + + let response; + try { + response = await this.testWebhooks.callTestWebhook('HEAD', requestUrl, req, res); + } catch (error) { + ResponseHelper.sendErrorResponse(res, error); + return; + } + + if (response.noWebhookResponse === true) { + // Nothing else to do as the response got already sent + return; + } + + ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); + }); // GET webhook requests (test for UI) this.app.get(`/${this.endpointWebhookTest}/*`, async (req: express.Request, res: express.Response) => { @@ -1217,7 +1255,6 @@ class App { ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); }); - // POST webhook requests (test for UI) this.app.post(`/${this.endpointWebhookTest}/*`, async (req: express.Request, res: express.Response) => { // Cut away the "/webhook-test/" to get the registred part of the url diff --git a/packages/nodes-base/credentials/SurveyMonkeyApi.credentials.ts b/packages/nodes-base/credentials/SurveyMonkeyApi.credentials.ts new file mode 100644 index 0000000000..130690d423 --- /dev/null +++ b/packages/nodes-base/credentials/SurveyMonkeyApi.credentials.ts @@ -0,0 +1,35 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class SurveyMonkeyApi implements ICredentialType { + name = 'surveyMonkeyApi'; + displayName = 'SurveyMonkey API'; + properties = [ + { + displayName: 'Access Token', + name: 'accessToken', + type: 'string' as NodePropertyTypes, + default: '', + description: `The access token must have the following scopes:
+ - Create/modify webhooks
+ - View webhooks
+ - View surveys
+ - View collectors
+ - View response details`, + }, + { + displayName: 'Client ID', + name: 'clientId', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Client Secret', + name: 'clientSecret', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/SurveyMonkey/GenericFunctions.ts b/packages/nodes-base/nodes/SurveyMonkey/GenericFunctions.ts new file mode 100644 index 0000000000..2d2cf7995d --- /dev/null +++ b/packages/nodes-base/nodes/SurveyMonkey/GenericFunctions.ts @@ -0,0 +1,82 @@ +import { + OptionsWithUri, + } from 'request'; + +import { + IExecuteFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, + IHookFunctions, + IWebhookFunctions +} from 'n8n-workflow'; + +export async function surveyMonkeyApiRequest(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const credentials = this.getCredentials('surveyMonkeyApi'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const endpoint = 'https://api.surveymonkey.com/v3'; + + let options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `bearer ${credentials.accessToken}`, + }, + method, + body, + qs: query, + uri: uri || `${endpoint}${resource}`, + json: true + }; + if (!Object.keys(body).length) { + delete options.body; + } + if (!Object.keys(query).length) { + delete options.qs; + } + options = Object.assign({}, options, option); + try { + return await this.helpers.request!(options); + } catch (error) { + const errorMessage = error.response.body.error.message; + if (errorMessage !== undefined) { + throw new Error(`SurveyMonkey error response [${error.statusCode}]: ${errorMessage}`); + } + throw error; + } +} + +export async function surveyMonkeyRequestAllItems(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + query.page = 1; + query.per_page = 100; + let uri: string | undefined; + + do { + responseData = await surveyMonkeyApiRequest.call(this, method, endpoint, body, query, uri); + uri = responseData.links.next; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData.links.next + ); + + return returnData; +} + +export function idsExist(ids: string[], surveyIds: string[]) { + for (const surveyId of surveyIds) { + if (!ids.includes(surveyId)) { + return false; + } + } + return true; +} diff --git a/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts b/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts new file mode 100644 index 0000000000..51d101697a --- /dev/null +++ b/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts @@ -0,0 +1,433 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + idsExist, + surveyMonkeyApiRequest, + surveyMonkeyRequestAllItems, +} from './GenericFunctions'; + +import { + createHmac, + } from 'crypto'; + +export class SurveyMonkeyTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'SurveyMonkey Trigger', + name: 'surveyMonkeyTrigger', + icon: 'file:surveyMonkey.png', + group: ['trigger'], + version: 1, + description: 'Starts the workflow when Survey Monkey events occure.', + defaults: { + name: 'SurveyMonkey Trigger', + color: '#53b675', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'surveyMonkeyApi', + required: true, + }, + ], + webhooks: [ + { + name: 'setup', + httpMethod: 'HEAD', + responseMode: 'onReceived', + path: 'webhook', + }, + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Event', + name: 'event', + type: 'options', + options: [ + { + name: 'Collector Created', + value: 'collector_created', + description: 'A collector is created', + }, + { + name: 'Collector Updated', + value: 'collector_updated', + description: 'A collector is updated', + }, + { + name: 'Collector Deleted', + value: 'collector_deleted', + description: 'A collector is deleted', + }, + { + name: 'Response Completed', + value: 'response_completed', + description: 'A survey response is completed', + }, + { + name: 'Response Created', + value: 'response_created', + description: 'A respondent begins a survey', + }, + { + name: 'Response Deleted', + value: 'response_deleted', + description: 'A response is deleted', + }, + { + name: 'Response Desqualified', + value: 'response_desqualified', + description: 'A survey response is disqualified ', + }, + { + name: 'Response Overquota', + value: 'response_overquota', + description: `A response is over a survey’s quota`, + }, + { + name: 'Response Updated', + value: 'response_updated', + description: 'A survey response is updated', + }, + { + name: 'Survey Created', + value: 'survey_created', + description: 'A survey is created', + }, + { + name: 'Survey Deleted', + value: 'survey_deleted', + description: 'A survey is deleted', + }, + { + name: 'Survey Updated', + value: 'survey_updated', + description: 'A survey is updated', + }, + ], + default: '', + required: true, + }, + { + displayName: 'Type', + name: 'objectType', + type: 'options', + options: [ + { + name: 'Collector', + value: 'collector', + }, + { + name: 'Survey', + value: 'survey', + }, + ], + default: '', + required: true, + }, + { + displayName: 'Survey IDs', + name: 'surveyIds', + type: 'multiOptions', + displayOptions: { + show: { + objectType: [ + 'survey', + ], + }, + hide: { + event: [ + 'survey_created', + ], + }, + }, + typeOptions: { + loadOptionsMethod: 'getSurveys', + }, + options: [], + default: [], + required: true, + }, + { + displayName: 'Survey ID', + name: 'surveyId', + type: 'options', + displayOptions: { + show: { + objectType: [ + 'collector', + ], + }, + }, + typeOptions: { + loadOptionsMethod: 'getSurveys', + }, + default: [], + required: true, + }, + { + displayName: 'Collector IDs', + name: 'collectorIds', + type: 'multiOptions', + displayOptions: { + show: { + objectType: [ + 'collector', + ], + }, + }, + typeOptions: { + loadOptionsMethod: 'getCollectors', + loadOptionsDependsOn: [ + 'surveyId', + ], + }, + options: [], + default: [], + required: true, + }, + { + displayName: 'Resolve Data', + name: 'resolveData', + type: 'boolean', + displayOptions: { + show: { + event: [ + 'response_completed', + ], + }, + }, + default: true, + description: 'By default the webhook-data only contain the IDs. If this option gets activated it
will resolve the data automatically.', + }, + ], + }; + + methods = { + loadOptions: { + // Get all the survey's collectors to display them to user so that he can + // select them easily + async getCollectors(this: ILoadOptionsFunctions): Promise { + const surveyId = this.getCurrentNodeParameter('surveyId'); + const returnData: INodePropertyOptions[] = []; + const collectors = await surveyMonkeyRequestAllItems.call(this, 'data', 'GET', `/surveys/${surveyId}/collectors`); + for (const collector of collectors) { + const collectorName = collector.name; + const collectorId = collector.id; + returnData.push({ + name: collectorName, + value: collectorId, + }); + } + return returnData; + }, + + // Get all the surveys to display them to user so that he can + // select them easily + async getSurveys(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const surveys = await surveyMonkeyRequestAllItems.call(this, 'data', 'GET', '/surveys'); + for (const survey of surveys) { + const surveyName = survey.title; + const surveyId = survey.id; + returnData.push({ + name: surveyName, + value: surveyId, + }); + } + return returnData; + }, + }, + }; + + // @ts-ignore (because of request) + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const objectType = this.getNodeParameter('objectType') as string; + const event = this.getNodeParameter('event') as string; + // Check all the webhooks which exist already if it is identical to the + // one that is supposed to get created. + const endpoint = '/webhooks'; + + const responseData = await surveyMonkeyRequestAllItems.call(this, 'data', 'GET', endpoint, {}); + + const webhookUrl = this.getNodeWebhookUrl('default'); + + const ids: string[] = []; + + if (objectType === 'survey') { + const surveyIds = this.getNodeParameter('surveyIds') as string[]; + ids.push.apply(ids, surveyIds); + } else if (objectType === 'collector') { + const collectorIds = this.getNodeParameter('collectorIds') as string[]; + ids.push.apply(ids, collectorIds); + } + + for (const webhook of responseData) { + const webhookDetails = await surveyMonkeyApiRequest.call(this, 'GET', `/webhooks/${webhook.id}`); + if (webhookDetails.subscription_url === webhookUrl + && idsExist(webhookDetails.object_ids as string[], ids as string[]) + && webhookDetails.event_type === event) { + // Set webhook-id to be sure that it can be deleted + const webhookData = this.getWorkflowStaticData('node'); + webhookData.webhookId = webhook.id as string; + return true; + } + } + + return false; + }, + + async create(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default'); + const event = this.getNodeParameter('event') as string; + const objectType = this.getNodeParameter('objectType') as string; + const endpoint = '/webhooks'; + const ids: string[] = []; + + if (objectType === 'survey') { + const surveyIds = this.getNodeParameter('surveyIds') as string[]; + ids.push.apply(ids, surveyIds); + } else if (objectType === 'collector') { + const collectorIds = this.getNodeParameter('collectorIds') as string[]; + ids.push.apply(ids, collectorIds); + } + + const body: IDataObject = { + name: `n8n - Webhook [${event}]`, + object_type: objectType, + object_ids: ids, + subscription_url: webhookUrl, + event_type: event, + }; + + if (objectType === 'collector' && event === 'collector_created') { + throw new Error('Type collector cannot be used with collector created event'); + } + + if (objectType === 'survey' && event === 'survey_created') { + delete body.object_type; + } + + let responseData: IDataObject = {}; + + responseData = await surveyMonkeyApiRequest.call(this, 'POST', endpoint, body); + + if (responseData.id === undefined) { + // Required data is missing so was not successful + return false; + } + + const webhookData = this.getWorkflowStaticData('node'); + webhookData.webhookId = responseData.id as string; + + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId !== undefined) { + + const endpoint = `/webhooks/${webhookData.webhookId}`; + + try { + await surveyMonkeyApiRequest.call(this, 'DELETE', endpoint); + } catch (e) { + return false; + } + + // Remove from the static workflow data so that it is clear + // that no webhooks are registred anymore + delete webhookData.webhookId; + } + + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const event = this.getNodeParameter('event') as string; + const objectType = this.getNodeParameter('objectType') as string; + const credentials = this.getCredentials('surveyMonkeyApi') as IDataObject; + const headerData = this.getHeaderData() as IDataObject; + const req = this.getRequestObject(); + const webhookName = this.getWebhookName(); + + if (webhookName === 'setup') { + // It is a create webhook confirmation request + return {}; + } + + if (headerData['sm-signature'] === undefined) { + return {}; + } + + return new Promise((resolve, reject) => { + const data: Buffer[] = []; + + req.on('data', (chunk) => { + data.push(chunk); + }); + + req.on('end', async () => { + + const computedSignature = createHmac('sha1', `${credentials.clientId}&${credentials.clientSecret}`).update(data.join('')).digest('base64'); + if (headerData['sm-signature'] !== computedSignature) { + // Signature is not valid so ignore call + return {}; + } + + let responseData = JSON.parse(data.join('')); + let endpoint = ''; + + if (event === 'response_completed') { + const resolveData = this.getNodeParameter('resolveData') as boolean; + if (resolveData) { + if (objectType === 'survey') { + endpoint = `/surveys/${responseData.resources.survey_id}/responses/${responseData.object_id}/details`; + } else { + endpoint = `/collectors/${responseData.resources.collector_id}/responses/${responseData.object_id}/details`; + } + responseData = await surveyMonkeyApiRequest.call(this, 'GET', endpoint); + } + } + + const returnItem: INodeExecutionData = { + json: responseData, + }; + + return resolve({ + workflowData: [ + [ + returnItem, + ], + ], + }); + }); + + req.on('error', (err) => { + throw new Error(err.message); + }); + }); + } +} diff --git a/packages/nodes-base/nodes/SurveyMonkey/surveyMonkey.png b/packages/nodes-base/nodes/SurveyMonkey/surveyMonkey.png new file mode 100644 index 0000000000000000000000000000000000000000..80e09c3afeba2230fbd4ce7364820bb5811c7cfd GIT binary patch literal 3327 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 ow4Hb{6MGU;`FwUW=l;^s9G6arP3(`x Date: Mon, 27 Apr 2020 01:03:01 +0200 Subject: [PATCH 05/29] :zap: Improvements to SurveyMonkey --- .../SurveyMonkeyApi.credentials.ts | 11 ++++++----- .../nodes/SurveyMonkey/GenericFunctions.ts | 2 +- .../SurveyMonkey/SurveyMonkeyTrigger.node.ts | 2 +- .../nodes/SurveyMonkey/surveyMonkey.png | Bin 3327 -> 1052 bytes 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/nodes-base/credentials/SurveyMonkeyApi.credentials.ts b/packages/nodes-base/credentials/SurveyMonkeyApi.credentials.ts index 130690d423..66614595eb 100644 --- a/packages/nodes-base/credentials/SurveyMonkeyApi.credentials.ts +++ b/packages/nodes-base/credentials/SurveyMonkeyApi.credentials.ts @@ -12,11 +12,12 @@ export class SurveyMonkeyApi implements ICredentialType { name: 'accessToken', type: 'string' as NodePropertyTypes, default: '', - description: `The access token must have the following scopes:
- - Create/modify webhooks
- - View webhooks
- - View surveys
- - View collectors
+ description: `The access token must have the following scopes:
+ - Create/modify webhooks
+ - View webhooks
+ - View surveys
+ - View collectors
+ - View responses
- View response details`, }, { diff --git a/packages/nodes-base/nodes/SurveyMonkey/GenericFunctions.ts b/packages/nodes-base/nodes/SurveyMonkey/GenericFunctions.ts index 2d2cf7995d..49ecef2912 100644 --- a/packages/nodes-base/nodes/SurveyMonkey/GenericFunctions.ts +++ b/packages/nodes-base/nodes/SurveyMonkey/GenericFunctions.ts @@ -1,6 +1,6 @@ import { OptionsWithUri, - } from 'request'; +} from 'request'; import { IExecuteFunctions, diff --git a/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts b/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts index 51d101697a..ee31ff95c3 100644 --- a/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts +++ b/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts @@ -21,7 +21,7 @@ import { import { createHmac, - } from 'crypto'; +} from 'crypto'; export class SurveyMonkeyTrigger implements INodeType { description: INodeTypeDescription = { diff --git a/packages/nodes-base/nodes/SurveyMonkey/surveyMonkey.png b/packages/nodes-base/nodes/SurveyMonkey/surveyMonkey.png index 80e09c3afeba2230fbd4ce7364820bb5811c7cfd..e5337accd626afbe0bfe893e301bf769a61e7ec0 100644 GIT binary patch literal 1052 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKw3=&b&bO2Ik0(?STfi%O)B8EK$4Eu{1_U19{ z&SRJ!z_16%C}7xIz_30YB(*1>VP`Ig+zUhnAT>bto?M1)*$f+#!Gb_;K1j=Updnyo zAO%1Xuz|aBKvEzsP!Om$k6}kHNEyf$uu(hnKvr+h1u27yWP?QZf|UVnJW$ARqKx5K zDZ`;+h9e~mr^*=)7BQTvU^rXJaHfLcRwKjtDu&B7+nO&W1HEHf666=m!2Fl>_g}Uj zKfiHZn&|N1me1D+WqFa$uTQ!NJYK%IB`MI_`0SQ>oq9XhPRoilm3(>cfcc&B`EM6+ zFfcHk@^oGpV;QqU7&&)Fx z)8@@r@lGx|Ci!2zi{q%c{R7di4-21tDE?<0%Wk^pyW*U}ReD_RtWRR<+!&A48M(a= zd|#L7Br->CWAmZ!L7JhaAsQCTMQ^4>UUmwwt=e?z_+@T~xh+e@7Edt@)V(`(^WMb_ zd3VDVLr>{ctP;A|sW|KLuSMn|lY)JWjnrC5ENb#t)I+|y;Z~alH#-r$V z{@vrKA9DgY4!N}NWR#L_lbpKU|HPs{i$eC#)}77VsVE%je9*3Ln|VXe-X~(K-dXrD ze)ZBno5O2v<71%yNv-&7F!$x6?duIMzEu}Vspfn2^-WL8RgPPlX^SrD+DF}&ednw) z!a#;fFWhQ`SucczC%3bK{WU(sK1cH8P(lYTiJ zIvKL#`tOsx;Q|E{4lQ5ZtvlWOsr2^8#cribH`X)F%I#fvUN3stDzBrxS6;DnzhdDH z%X2H^o49h$!sA@epHwy5h`xIyy6|GVcT2g#n_cP}zV8Hf1*+)gYZ?dYE?&)QvdJ#Q zAo+KCpW1>R$4ce&m$R~#r=}+vZfdjn`TbPf)ccv4@wet&pY~B`x093QPIc?w^E>*s zm~&n&`{=iIAuG?f9SJ?1W3zZ}12sW1EeY0-wVyBn?j8l=`b z=D&ZFVdQ&MBb@0FIyC AF#rGn literal 3327 zcmY*cc|4SD7am(#Qpg?}V@cVUFk|d8VeHY6eJNwl3}dp(G6vb#gtBB>$eyAxs9xDq zCbDmZ*A}vmZ}j%{e&7B4&U3E&JlDCd``rIM31%iRR%U)?006*ha8=KOn$4&o!gzxE zrhA;+Moly*3z!a|vR7b%x`}Z|7~C{A23(?IMgRj12Y~LFLVW-<`~Zeu7yvM&Irj%! z&`A8|&;kJQNC5qBjx9AGFDGhHyMJf8Vw!&yi)sI`&5G&%U=b=Ckg;_=lbV?PuiBsh zfYTkvL6aqU`W#iw26+vEMi|2(u6{l;&Tf7#?lQqX{>NPaXfT8decaK`z+fM5Ulb%* z9rTL)9gFMh^ ze~7Fs27{5oD9ZQ++>(_CgTb}b*+1IUR_JjRVj6&Sr#c_& zYsf=?G5@dj+XpIpZ2muo`DfB!RqCi3%uv}s*QUV?Z@XXv05De>=xJZ`VCb<8^R~Lh zchvH2e7vfm&O2N4OD^iZ$$NpkAdWy8NomVdZZ<9??*X^GVOVB3TycK7O-$a<1;i+C}64kL?Q79ccZ_r=;$u9s)7$xK7H1F2Eu16m$TH0t+>6ZC1xy7_qXXZ8i+SIA2Cz*?jPUJ{Y+_`b)I-Fj%3nG^gl+V2nftF@ zqM%OucS|$dkv6bFlfyUq$-4^rrmU`?f6Cv(aJ8-s@mjBneyD{hp6~4VHm2n9(%&>^ zY+~Iedf>y78X=Ip@hXn|qkF9gfs38t4c3TKe80N^2qm7w_#Dy|(TpXHKTjm2Y?jn& zDY6vjI$Dl=4*VI^(E8;9!3P8EME8|Qq7!Ziw{F=ZaD1=?cO{Mj#cPm0&8u3(#Q7d| z$j|qmzQ^bO?3U{hh#W)a-+)xnw9OWRJhs`P6-Bk#6?~uyFwi_79#@{E)h=qPgah)i zoJgK4f{jPeJC5Gb-?-W-cq{ehiB`5|N`om!$waY6;N`gY&^;9zX~5ryCm3`yxNBx2 zhaQ7?JDYp7P-pTZg8j?23&H`H{W9>;;w-Ou2$xRg>BMotEJh1}<{X6Cy>RXX1TFYd zVfEAgPxbf1Z@-I9R9#pQSvPXAI9xWOiO_Umj~ACr5G0+w#NNDSCAoT^zNR3m*cnB zf+uVf-W~V}-YXZFK4VnTxr1M^q;i z+3)=&iHpj{qY}R>@`Lat5muc^MLJ+AatOT%NR`=&|V!Uqy5*admH5N8gT{cL1i+T4&{l zl9rrDmmjhOCvHC_yfDJP;n|pGQRS((u15^bEY8o4y7KOA!xEX)&cjby)nt|km9hpE zrCWdEJpoyr(#Zy9@C=^1JXTVEWf=H{VPgNp%H9CMAaSHzyY;fE2-kk9M%S#Vf-1B% z20Kw&-8WTMKQUu>qhikSrvIZnMKJ|&{IjJ;;z4u=v0gj9r<>QDnB#eFR%=N2iup@t zh)ZtW+U1u3Eqe!!*A5;E_B|A@j$_HRy^cSE){E);V1=aZ9WA273}x~0leF=bQXXDj z#U+J8E5Lf?s9Yi^M$Ukn-%TOhMIsfe-76b(b5E^M47WWM+FPddh*L%dh=;FiBZlgA zCRoqd?k#6_G=sFVPZocwpQ^+=v0k4rtbT&@w34GJZkn^CZFGe4!M|@Xudo$->G9Yj zA<-D|fmavzD`OG8WW{T5pMc`WQo3RGf@@95?WuarS;g>5OqryJYs5Z6+B4uid$RHi zPEl3#`839*TYkCE$f+k)20Gga`>5(nPKS(dEZT)_O=v*7eudoG_~qOSZ1O4`5-Xan zaYJ?5!JmrFbJ&)@8d`b}Zcg{s?L1HPidrteSa}*h-T&CZ)7&|0@ar4)4a+4&7Cry@ zc@+-y6JD5}5LwLxQAb7AW)@g#tU>rV!#DB|`^OrsfT+e(~_v8lF8 zk+jg9dQYfyDz7W+k`Sabu^8$4Xq5(OsA{pognf^G?+I-81hxUwN|gf~WOQ;`YtiNb z@JA(Su9tS4fE4!>1>(?$96^SIKUqIjszg2IRML3a?MUE-PI!u^&Zm zO>v%iDAqCktDMYl$5r)?y)+7Io!f~yn|V=y0VPw(}gRj z>mq$!5;I_5Z~U#PpYjJP;TbhP2N)H&SGV~-n18-` zzU1CDg{{43N$sC#po+Y?Me#@Gh~W>a#xWQ6FB(VFIZwZ0Gjf#Zwd3YW>0sQcC(SSm z&22=QbvWGB%M5+KLv*rtd3Sp5?8rp8T8a^$8KYEKV3hXT?f5MlzIXy1um0O?V?T}p zB9QBG(Zkim>tzzdzTtpxCNsi^0!S$zT&$+6a1lzLJ)dL22eP})RzmCo$8b;8)@ASy zr{+`?MoKttJ!|4=5v&^M`P*5|SPEET%P2rv?`C%wymW;r04}KI48q!KO*}ROxxq`k zI}GHf$Ml__E_CEM{H0R?{21+`ObT(Fj40`Rc1<=5*m)tVdn|v+reXi?#1Hn!k*h~` zXT3_{^&{*YiD}LKJNGxw;gv#l%NQLBK33OViNV@-%{#ifB{6xY$60XY_IYSjX_#8@ zv&MR^o98!`@u^to-niCghAJ0bDJ9heuh)j9gvV(t5AuqfLE17JA$MVyMepWmI~c2J z^DSLm8iw=aHy+rjpj0%UXKR9P2IaRV$o~ek% zwhUle&0$c%jaSY#Ob7620(ZX-znp}}l?gvqLWjGw6-H#s%_GG?8M>@ceH-FC!&2R0 zLqNKw12C%#RlI%g%{{K!b#4c2_w?G?Ha}Pzxg*i2 z6LPzovr@8{c4oBA1b+a(W92cTjBiPi(fO3qmYi&+KVRM>qAj9b5P~gN?9IwHunbSD zH3}4s2<5!pXue^sxZBLvb0cL2_*z5c;}|fxFsO@(_e!B+0Q+8Ju?rKzP*b+q(LlIE ow4Hb{6MGU;`FwUW=l;^s9G6arP3(`x Date: Wed, 29 Apr 2020 16:51:49 +0200 Subject: [PATCH 06/29] added node sms77 --- .../credentials/Sms77.credentials.ts | 17 +++ .../nodes/Sms77/GenericFunctions.ts | 47 ++++++ packages/nodes-base/nodes/Sms77/Sms77.node.ts | 144 ++++++++++++++++++ packages/nodes-base/nodes/Sms77/sms77.png | Bin 0 -> 3738 bytes packages/nodes-base/package.json | 1 + 5 files changed, 209 insertions(+) create mode 100644 packages/nodes-base/credentials/Sms77.credentials.ts create mode 100644 packages/nodes-base/nodes/Sms77/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Sms77/Sms77.node.ts create mode 100644 packages/nodes-base/nodes/Sms77/sms77.png diff --git a/packages/nodes-base/credentials/Sms77.credentials.ts b/packages/nodes-base/credentials/Sms77.credentials.ts new file mode 100644 index 0000000000..f098297dff --- /dev/null +++ b/packages/nodes-base/credentials/Sms77.credentials.ts @@ -0,0 +1,17 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class Sms77 implements ICredentialType { + name = 'Sms77'; + displayName = 'Sms77'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Sms77/GenericFunctions.ts b/packages/nodes-base/nodes/Sms77/GenericFunctions.ts new file mode 100644 index 0000000000..68e9fb281b --- /dev/null +++ b/packages/nodes-base/nodes/Sms77/GenericFunctions.ts @@ -0,0 +1,47 @@ +import {IExecuteFunctions, IHookFunctions,} from 'n8n-core'; +import {IDataObject,} from 'n8n-workflow'; + +/** + * Make an API request to MSG91 + * + * @param {IHookFunctions | IExecuteFunctions} this + * @param {string} method + * @param {string} endpoint + * @param {object} form + * @param {object | undefined} qs + * @returns {Promise} + */ +export async function sms77ApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, form: IDataObject, qs?: IDataObject): Promise { // tslint:disable-line:no-any + const setPayload = (o?: IDataObject) => { + if (!o) { + o = {}; + } + + o.p = credentials!.apiKey as string; + o.json = 1; + o.sendwith = 'n8n'; + + return o; + }; + + const credentials = this.getCredentials('Sms77'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + 'GET' === method ? qs = setPayload(qs) : form = setPayload(form); + + const res = await this.helpers.request({ + form, + json: true, + method, + qs, + uri: `https://gateway.sms77.io/api/${endpoint}`, + }); + + if ('100' !== res.success) { + throw new Error('Invalid sms77 credentials or API error!'); + } + + return res; +} diff --git a/packages/nodes-base/nodes/Sms77/Sms77.node.ts b/packages/nodes-base/nodes/Sms77/Sms77.node.ts new file mode 100644 index 0000000000..c483253d42 --- /dev/null +++ b/packages/nodes-base/nodes/Sms77/Sms77.node.ts @@ -0,0 +1,144 @@ +import {IExecuteFunctions,} from 'n8n-core'; +import {IDataObject, INodeExecutionData, INodeType, INodeTypeDescription,} from 'n8n-workflow'; +import {sms77ApiRequest} from './GenericFunctions'; + +export class Sms77 implements INodeType { + description: INodeTypeDescription = { + displayName: 'Sms77', + name: 'sms77', + icon: 'file:sms77.png', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Send SMS', + defaults: { + name: 'Sms77', + color: '#18D46A', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'Sms77', + required: true, + } + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'SMS', + value: 'sms', + }, + ], + default: 'sms', + description: 'The resource to operate on.', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'sms', + ], + }, + }, + options: [ + { + name: 'Send', + value: 'send', + description: 'Send SMS', + }, + ], + default: 'send', + description: 'The operation to perform.', + }, + { + displayName: 'From', + name: 'from', + type: 'string', + default: '', + placeholder: '+4901234567890', + required: false, + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'sms', + ], + }, + }, + description: 'The number from which to send the message.', + }, + { + displayName: 'To', + name: 'to', + type: 'string', + default: '', + placeholder: '+49876543210', + required: true, + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'sms', + ], + }, + }, + description: 'The number, with coutry code, to which to send the message.', + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'sms', + ], + }, + }, + description: 'The message to send', + }, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const returnData: IDataObject[] = []; + + for (let i = 0; i < this.getInputData().length; i++) { + const resource = this.getNodeParameter('resource', i); + if ('sms' !== resource) { + throw new Error(`The resource "${resource}" is not known!`); + } + + const operation = this.getNodeParameter('operation', i); + if ('send' !== operation) { + throw new Error(`The operation "${operation}" is not known!`); + } + + returnData.push({ + requestId: await sms77ApiRequest.call(this, 'POST', 'sms', {}, { + from: this.getNodeParameter('from', i), + to: this.getNodeParameter('to', i), + text: this.getNodeParameter('message', i), + } as IDataObject) + }); + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Sms77/sms77.png b/packages/nodes-base/nodes/Sms77/sms77.png new file mode 100644 index 0000000000000000000000000000000000000000..9ee5756efe882c1cf59152a73071072ce8b34295 GIT binary patch literal 3738 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 literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index ca58495864..6bfdcac069 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -92,6 +92,7 @@ "dist/credentials/RundeckApi.credentials.js", "dist/credentials/ShopifyApi.credentials.js", "dist/credentials/SlackApi.credentials.js", + "dist/credentials/Sms77.credentials.js", "dist/credentials/Smtp.credentials.js", "dist/credentials/StripeApi.credentials.js", "dist/credentials/SalesmateApi.credentials.js", From dd91a2c3ae5b141a7475f98f290997ed4b0dbd9a Mon Sep 17 00:00:00 2001 From: Pablo Estevez Date: Wed, 29 Apr 2020 21:57:36 -0400 Subject: [PATCH 07/29] Add Facebook Graph API node --- .../credentials/GraphApi.credentials.ts | 18 + .../nodes/Facebook/GraphApi.node.ts | 416 ++++++++++++++++++ .../nodes-base/nodes/Facebook/facebook.png | Bin 0 -> 24576 bytes packages/nodes-base/package.json | 2 + 4 files changed, 436 insertions(+) create mode 100644 packages/nodes-base/credentials/GraphApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Facebook/GraphApi.node.ts create mode 100644 packages/nodes-base/nodes/Facebook/facebook.png diff --git a/packages/nodes-base/credentials/GraphApi.credentials.ts b/packages/nodes-base/credentials/GraphApi.credentials.ts new file mode 100644 index 0000000000..132ba38e77 --- /dev/null +++ b/packages/nodes-base/credentials/GraphApi.credentials.ts @@ -0,0 +1,18 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + + +export class GraphApi implements ICredentialType { + name = 'graphApi'; + displayName = 'Graph API'; + properties = [ + { + displayName: 'Access Token', + name: 'accessToken', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Facebook/GraphApi.node.ts b/packages/nodes-base/nodes/Facebook/GraphApi.node.ts new file mode 100644 index 0000000000..a81894c138 --- /dev/null +++ b/packages/nodes-base/nodes/Facebook/GraphApi.node.ts @@ -0,0 +1,416 @@ +import { + BINARY_ENCODING, + IExecuteFunctions, +} from 'n8n-core'; +import { + IBinaryData, + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { OptionsWithUri } from 'request'; + +export class GraphApi implements INodeType { + description: INodeTypeDescription = { + displayName: 'Graph API', + name: 'graphApi', + icon: 'file:facebook.png', + group: ['transform'], + version: 1, + description: 'Interacts with Facebook using the Graph API', + defaults: { + name: 'Graph API', + color: '#772244', + }, + inputs: ['main'], + outputs: ['main', 'main'], + outputNames: ['success', 'failure'], + credentials: [ + { + name: 'graphApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Host URL', + name: 'hostUrl', + type: 'options', + options: [ + { + name: 'Default', + value: 'graph.facebook.com', + }, + { + name: 'Video Uploads', + value: 'graph-video.facebook.com', + } + ], + default: 'graph.facebook.com', + description: 'The Host URL of the request. Almost all requests are passed to the graph.facebook.com host URL. The single exception is video uploads, which use graph-video.facebook.com.', + required: true, + }, + { + displayName: 'HTTP Request Method', + name: 'httpRequestMethod', + type: 'options', + options: [ + { + name: 'GET', + value: 'GET', + }, + { + name: 'POST', + value: 'POST', + }, + { + name: 'DELETE', + value: 'DELETE', + }, + ], + default: 'GET', + description: 'The HTTP Method to be used for the request.', + required: true, + }, + { + displayName: 'Graph API Version', + name: 'graphApiVersion', + type: 'options', + options: [ + { + name: 'Latest', + value: '', + }, + { + name: 'v6.0', + value: 'v6.0/', + }, + { + name: 'v5.0', + value: 'v5.0/', + }, + { + name: 'v4.0', + value: 'v4.0/', + }, + { + name: 'v3.3', + value: 'v3.3/', + }, + { + name: 'v3.2', + value: 'v3.2/', + }, + { + name: 'v3.1', + value: 'v3.1/', + }, + { + name: 'v3.0', + value: 'v3.0/', + }, + { + name: 'v2.12', + value: 'v2.12/', + }, + ], + default: '', + description: 'The version of the Graph API to be used in the request.', + required: true, + }, + { + displayName: 'Node', + name: 'node', + type: 'string', + default: '', + description: 'The node on which to operate. A node is an individual object with a unique ID. For example, there are many User node objects, each with a unique ID representing a person on Facebook.', + placeholder: 'me', + required: true, + }, + { + displayName: 'Edge', + name: 'edge', + type: 'string', + default: '', + description: 'Edge of the node on which to operate. Edges represent collections of objects wich are attached to the node.', + placeholder: 'videos', + required: false, + }, + { + displayName: 'Send Binary Data', + name: 'sendBinaryData', + type: 'boolean', + displayOptions: { + show: { + httpRequestMethod: [ + 'POST', + 'PUT', + ], + }, + }, + default: false, + required: true, + description: 'If binary data should be send as body.', + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + required: false, + default: '', + placeholder: 'file:data', + displayOptions: { + hide: { + sendBinaryData: [ + false, + ], + }, + show: { + httpRequestMethod: [ + 'POST', + 'PUT', + ], + }, + }, + description: `Name of the binary property which contains the data for the file to be uploaded.
+ For Form-Data Multipart, multiple can be provided in the format:
+ "sendKey1:binaryProperty1,sendKey2:binaryProperty2`, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Fields', + name: 'fields', + placeholder: 'Add Field', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + '/httpRequestMethod': [ + 'GET', + ], + }, + }, + description: 'The list of fields to request in the GET request.', + default: {}, + options: [ + { + name: 'field', + displayName: 'Field', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'Name of the field.', + }, + ], + }, + ], + }, + { + displayName: 'Query Parameters', + name: 'queryParameters', + placeholder: 'Add Parameter', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + description: 'The query parameters to send', + default: {}, + options: [ + { + name: 'parameter', + displayName: 'Parameter', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'Name of the parameter.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the parameter.', + }, + ], + }, + ], + }, + { + displayName: 'Query Parameters JSON', + name: 'queryParametersJson', + type: 'json', + default: '{}', + placeholder: '{\"field_name\": \"field_value\"}', + description: 'The query parameters to send, defined as a JSON object', + required: false, + } + ], + }, + ], + }; + + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + + let response: any; // tslint:disable-line:no-any + const successItems: INodeExecutionData[] = []; + const failureItems: INodeExecutionData[] = []; + + for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { + const graphApiCredentials = this.getCredentials('graphApi'); + + const hostUrl = this.getNodeParameter('hostUrl', itemIndex) as string; + const httpRequestMethod = this.getNodeParameter('httpRequestMethod', itemIndex) as string; + const graphApiVersion = this.getNodeParameter('graphApiVersion', itemIndex) as string; + const node = this.getNodeParameter('node', itemIndex) as string; + const edge = this.getNodeParameter('edge', itemIndex) as string; + const options = this.getNodeParameter('options', itemIndex, {}) as IDataObject; + + let uri = `https://${hostUrl}/${graphApiVersion}${node}`; + if (edge) { + uri = `${uri}/${edge}`; + } + + const requestOptions : OptionsWithUri = { + headers: { + accept: 'application/json,text/*;q=0.99', + }, + method: httpRequestMethod, + uri, + json: true, + gzip: true, + qs: { + access_token: graphApiCredentials!.accessToken, + }, + }; + + if (options !== undefined) { + // Build fields query parameter as a comma separated list + if (options.fields !== undefined) { + const fields = options.fields as IDataObject; + if (fields.field !== undefined) { + const fieldsCsv = (fields.field as IDataObject[]).map(field => field.name).join(','); + requestOptions.qs.fields = fieldsCsv; + } + } + + // Add the query parameters defined in the UI + if (options.queryParameters !== undefined) { + const queryParameters = options.queryParameters as IDataObject; + + if (queryParameters.parameter != undefined) { + for (const queryParameter of queryParameters.parameter as IDataObject[]) { + requestOptions.qs[queryParameter.name as string] = queryParameter.value; + } + } + } + + // Add the query parameters defined as a JSON object + if (options.queryParametersJson) { + let queryParametersJsonObj = {}; + try + { + queryParametersJsonObj = JSON.parse(options.queryParametersJson as string); + } catch { /* Do nothing, at least for now */} + const qs = requestOptions.qs; + requestOptions.qs = { + ...qs, + ...queryParametersJsonObj, + }; + } + } + + const sendBinaryData = this.getNodeParameter('sendBinaryData', itemIndex, false) as boolean; + if (sendBinaryData) { + const item = items[itemIndex]; + if (item.binary === undefined) { + throw new Error('No binary data exists on item!'); + } + + const binaryPropertyNameFull = this.getNodeParameter('binaryPropertyName', itemIndex) as string; + + let propertyName = 'file'; + let binaryPropertyName = binaryPropertyNameFull; + if (binaryPropertyNameFull.includes(':')) { + const binaryPropertyNameParts = binaryPropertyNameFull.split(':'); + propertyName = binaryPropertyNameParts[0]; + binaryPropertyName = binaryPropertyNameParts[1]; + } + + if (item.binary[binaryPropertyName] === undefined) { + throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`); + } + + const binaryProperty = item.binary[binaryPropertyName] as IBinaryData; + + requestOptions.formData = { + [propertyName]: { + value: Buffer.from(binaryProperty.data, BINARY_ENCODING), + options: { + filename: binaryProperty.fileName, + contentType: binaryProperty.mimeType, + }, + }, + }; + } + + try { + // Now that the options are all set make the actual http request + response = await this.helpers.request(requestOptions); + } catch (error) { + if (this.continueOnFail() === false) { + throw error; + } + + let errorItem; + if (error.response !== undefined) { + // Since this is a Graph API node and we already know the request was + // not successful, we'll go straight to the error details. + const graphApiErrors = error.response.body?.error ?? {}; + + errorItem = { + statusCode: error.statusCode, + ...graphApiErrors, + headers: error.response.headers, + }; + } else { + // Unknown Graph API response, we'll dump everything in the response item + errorItem = error; + } + failureItems.push({ json: { ...errorItem }}); + + continue; + } + + if (typeof response === 'string') { + if (this.continueOnFail() === false) { + throw new Error('Response body is not valid JSON.'); + } + + failureItems.push({json: {message: response}}); + continue; + } + + successItems.push({json: response}); + } + + return [successItems, failureItems]; + } +} diff --git a/packages/nodes-base/nodes/Facebook/facebook.png b/packages/nodes-base/nodes/Facebook/facebook.png new file mode 100644 index 0000000000000000000000000000000000000000..b6cba78f88d9da3882f20f1a6d9a212c4fcaf62c GIT binary patch literal 24576 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 Date: Fri, 1 May 2020 10:02:34 +0200 Subject: [PATCH 08/29] :bug: Fix issue with push messages that get received to fast --- .../src/components/mixins/pushConnection.ts | 80 ++++++++++++++----- 1 file changed, 62 insertions(+), 18 deletions(-) diff --git a/packages/editor-ui/src/components/mixins/pushConnection.ts b/packages/editor-ui/src/components/mixins/pushConnection.ts index f32423f6c9..d9e77d929c 100644 --- a/packages/editor-ui/src/components/mixins/pushConnection.ts +++ b/packages/editor-ui/src/components/mixins/pushConnection.ts @@ -22,6 +22,8 @@ export const pushConnection = mixins( return { eventSource: null as EventSource | null, reconnectTimeout: null as NodeJS.Timeout | null, + retryTimeout: null as NodeJS.Timeout | null, + pushMessageQueue: [] as Array<{ event: Event, retriesLeft: number }>, }; }, computed: { @@ -96,47 +98,84 @@ export const pushConnection = mixins( * @param {number} retryAttempts * @returns */ - retryPushMessage (event: Event, retryAttempts: number) { - retryAttempts = retryAttempts - 1; + queuePushMessage (event: Event, retryAttempts: number) { + this.pushMessageQueue.push({ event, retriesLeft: retryAttempts }); - if (retryAttempts <= 0) { - return; + if (this.retryTimeout === null) { + this.retryTimeout = setTimeout(this.processWaitingPushMessages, 20); + } + }, + + + /** + * Process the push messages which are waiting in the queue + */ + processWaitingPushMessages () { + if (this.retryTimeout !== null) { + clearTimeout(this.retryTimeout); + this.retryTimeout = null; } - setTimeout(() => { - this.pushMessageReceived(event, retryAttempts); - }, 200); + const queueLength = this.pushMessageQueue.length; + for (let i = 0; i < queueLength; i++) { + const messageData = this.pushMessageQueue.shift(); + + if (this.pushMessageReceived(messageData!.event, true) === false) { + // Was not successful + messageData!.retriesLeft -= 1; + + if (messageData!.retriesLeft > 0) { + // If still retries are left add it back and stop execution + this.pushMessageQueue.unshift(messageData!); + } + break; + } + } + + if (this.pushMessageQueue.length !== 0 && this.retryTimeout === null) { + this.retryTimeout = setTimeout(this.processWaitingPushMessages, 25); + } }, + /** * Process a newly received message * * @param {Event} event The event data with the message data - * @returns {void} + * @param {boolean} [isRetry] If it is a retry + * @returns {boolean} If message could be processed */ - pushMessageReceived (event: Event, retryAttempts?: number): void { - retryAttempts = retryAttempts || 5; + pushMessageReceived (event: Event, isRetry?: boolean): boolean { + const retryAttempts = 5; let receivedData: IPushData; try { // @ts-ignore receivedData = JSON.parse(event.data); } catch (error) { - console.error('The received push data is not valid JSON.'); // eslint-disable-line no-console - return; + return false; + } + + if (!['testWebhookReceived'].includes(receivedData.type) && isRetry !== true && this.pushMessageQueue.length) { + // If there are already messages in the queue add the new one that all of them + // get executed in order + this.queuePushMessage(event, retryAttempts); + return false; } if (['nodeExecuteAfter', 'nodeExecuteBefore'].includes(receivedData.type)) { if (this.$store.getters.isActionActive('workflowRunning') === false) { // No workflow is running so ignore the messages - return; + return false; } const pushData = receivedData.data as IPushDataNodeExecuteBefore; if (this.$store.getters.activeExecutionId !== pushData.executionId) { // The data is not for the currently active execution or // we do not have the execution id yet. - this.retryPushMessage(event, retryAttempts); - return; + if (isRetry !== true) { + this.queuePushMessage(event, retryAttempts); + } + return false; } } @@ -148,14 +187,16 @@ export const pushConnection = mixins( if (this.$store.getters.isActionActive('workflowRunning') === false) { // No workflow is running so ignore the messages - return; + return false; } if (this.$store.getters.activeExecutionId !== pushData.executionIdActive) { // The workflow which did finish execution did either not get started // by this session or we do not have the execution id yet. - this.retryPushMessage(event, retryAttempts); - return; + if (isRetry !== true) { + this.queuePushMessage(event, retryAttempts); + } + return false; } const runDataExecuted = pushData.data; @@ -231,7 +272,10 @@ export const pushConnection = mixins( this.$store.commit('setExecutionWaitingForWebhook', false); this.$store.commit('setActiveExecutionId', pushData.executionId); } + + this.processWaitingPushMessages(); } + return true; }, }, }); From 88b904fd1c4c7d77944590835551215e32591353 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Fri, 1 May 2020 18:27:38 +0200 Subject: [PATCH 09/29] :zap: Add moment manually with old version to not use their broke version --- packages/nodes-base/package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index fcdbde4675..5372239695 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -290,6 +290,8 @@ "lodash.get": "^4.4.2", "lodash.set": "^4.3.2", "lodash.unset": "^4.5.2", + "moment": "2.24.0", + "moment-timezone": "^0.5.28", "mongodb": "^3.3.2", "mysql2": "^2.0.1", "n8n-core": "~0.32.0", From 384efade9fd7150c8a1da55f029a5b1944b6fd5a Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Fri, 1 May 2020 20:24:20 +0200 Subject: [PATCH 10/29] :zap: Small improvements to Mongo connection string support --- .../src/components/CredentialsInput.vue | 35 +++++--- .../credentials/MongoDb.credentials.ts | 88 ++++++++++++++----- .../nodes/MongoDb/mongo.node.types.ts | 7 +- .../nodes/MongoDb/mongo.node.utils.ts | 12 +-- 4 files changed, 98 insertions(+), 44 deletions(-) diff --git a/packages/editor-ui/src/components/CredentialsInput.vue b/packages/editor-ui/src/components/CredentialsInput.vue index 0830b2a2a0..064d88fc71 100644 --- a/packages/editor-ui/src/components/CredentialsInput.vue +++ b/packages/editor-ui/src/components/CredentialsInput.vue @@ -21,18 +21,20 @@ - - - {{parameter.displayName}}: - -
- -
-
- - - -
+ + + + {{parameter.displayName}}: + +
+ +
+
+ + + +
+
@@ -85,6 +87,7 @@ import { ICredentialType, ICredentialNodeAccess, INodeCredentialDescription, + INodeProperties, INodeTypeDescription, } from 'n8n-workflow'; @@ -162,6 +165,14 @@ export default mixins( tempValue[name] = parameterData.value; Vue.set(this, 'propertyValue', tempValue); }, + displayCredentialParameter (parameter: INodeProperties): boolean { + if (parameter.displayOptions === undefined) { + // If it is not defined no need to do a proper check + return true; + } + + return this.displayParameter(this.propertyValue, parameter, ''); + }, async createCredentials (): Promise { const nodesAccess = this.nodesAccess.map((nodeType) => { return { diff --git a/packages/nodes-base/credentials/MongoDb.credentials.ts b/packages/nodes-base/credentials/MongoDb.credentials.ts index 436e9f2e4f..a6de302172 100644 --- a/packages/nodes-base/credentials/MongoDb.credentials.ts +++ b/packages/nodes-base/credentials/MongoDb.credentials.ts @@ -4,10 +4,53 @@ export class MongoDb implements ICredentialType { name = 'mongoDb'; displayName = 'MongoDB'; properties = [ + { + displayName: 'Configuration Type', + name: 'configurationType', + type: 'options' as NodePropertyTypes, + options: [ + { + name: 'Connection String', + value: 'connectionString', + description: 'Provide connection data via string', + }, + { + name: 'Values', + value: 'values', + description: 'Provide connection data via values', + }, + ], + default: 'values', + description: 'The operation to perform.', + }, + { + displayName: 'Connection String', + name: 'connectionString', + type: 'string' as NodePropertyTypes, + displayOptions: { + show: { + configurationType: [ + 'connectionString', + ], + }, + }, + default: '', + placeholder: 'mongodb://:@localhost:27017/?authSource=admin&readPreference=primary&appname=n8n&ssl=false', + required: false, + description: `If provided, the value here will be used as a MongoDB connection string,
+ and the MongoDB credentials will be ignored` + }, { displayName: 'Host', name: 'host', type: 'string' as NodePropertyTypes, + displayOptions: { + show: { + configurationType: [ + 'values', + ], + }, + }, default: 'localhost' }, { @@ -15,13 +58,19 @@ export class MongoDb implements ICredentialType { name: 'database', type: 'string' as NodePropertyTypes, default: '', - description: - 'Note: the database should still be provided even if using an override connection string' + description: 'Note: the database should still be provided even if using an override connection string' }, { displayName: 'User', name: 'user', type: 'string' as NodePropertyTypes, + displayOptions: { + show: { + configurationType: [ + 'values', + ], + }, + }, default: '' }, { @@ -31,34 +80,27 @@ export class MongoDb implements ICredentialType { typeOptions: { password: true }, + displayOptions: { + show: { + configurationType: [ + 'values', + ], + }, + }, default: '' }, { displayName: 'Port', name: 'port', type: 'number' as NodePropertyTypes, + displayOptions: { + show: { + configurationType: [ + 'values', + ], + }, + }, default: 27017 }, - { - displayName: 'Override conn string', - name: 'shouldOverrideConnString', - type: 'boolean' as NodePropertyTypes, - default: false, - required: false, - description: - 'Whether to override the generated connection string. Credentials will also be ignored in this case.' - }, - { - displayName: 'Conn string override', - name: 'connStringOverrideVal', - type: 'string' as NodePropertyTypes, - typeOptions: { - rows: 1 - }, - default: '', - placeholder: `mongodb://USERNAMEHERE:PASSWORDHERE@localhost:27017/?authSource=admin&readPreference=primary&appname=n8n&ssl=false`, - required: false, - description: `If provided, the value here will be used as a MongoDB connection string, and the MongoDB credentials will be ignored` - } ]; } diff --git a/packages/nodes-base/nodes/MongoDb/mongo.node.types.ts b/packages/nodes-base/nodes/MongoDb/mongo.node.types.ts index 2995fdd2a2..212ebc159d 100644 --- a/packages/nodes-base/nodes/MongoDb/mongo.node.types.ts +++ b/packages/nodes-base/nodes/MongoDb/mongo.node.types.ts @@ -7,7 +7,8 @@ export interface IMongoParametricCredentials { /** * Whether to allow overriding the parametric credentials with a connection string */ - shouldOverrideConnString: false | undefined | null; + configurationType: 'values'; + host: string; database: string; user: string; @@ -22,11 +23,11 @@ export interface IMongoOverrideCredentials { /** * Whether to allow overriding the parametric credentials with a connection string */ - shouldOverrideConnString: true; + configurationType: 'connectionString'; /** * If using an override connection string, this is where it will be. */ - connStringOverrideVal: string; + connectionString: string; database: string; } diff --git a/packages/nodes-base/nodes/MongoDb/mongo.node.utils.ts b/packages/nodes-base/nodes/MongoDb/mongo.node.utils.ts index 1d682a450a..17ccc0dc4a 100644 --- a/packages/nodes-base/nodes/MongoDb/mongo.node.utils.ts +++ b/packages/nodes-base/nodes/MongoDb/mongo.node.utils.ts @@ -37,13 +37,13 @@ function buildMongoConnectionParams( credentials.database && credentials.database.trim().length > 0 ? credentials.database.trim() : ''; - if (credentials.shouldOverrideConnString) { + if (credentials.configurationType === 'connectionString') { if ( - credentials.connStringOverrideVal && - credentials.connStringOverrideVal.trim().length > 0 + credentials.connectionString && + credentials.connectionString.trim().length > 0 ) { return { - connectionString: credentials.connStringOverrideVal.trim(), + connectionString: credentials.connectionString.trim(), database: sanitizedDbName }; } else { @@ -67,11 +67,11 @@ function buildMongoConnectionParams( export function validateAndResolveMongoCredentials( credentials?: ICredentialDataDecryptedObject ): IMongoCredentials { - if (credentials == undefined) { + if (credentials === undefined) { throw new Error('No credentials got returned!'); } else { return buildMongoConnectionParams( - (credentials as any) as IMongoCredentialsType + credentials as unknown as IMongoCredentialsType, ); } } From cb476069bd00ee519dcb6eee2401b19eb9ca1c7b Mon Sep 17 00:00:00 2001 From: ricardo Date: Fri, 1 May 2020 19:46:02 -0400 Subject: [PATCH 11/29] :zap: Improvements --- .../SurveyMonkeyApi.credentials.ts | 11 +- .../nodes/Asana/GenericFunctions.ts | 2 +- .../nodes/SurveyMonkey/GenericFunctions.ts | 4 +- .../nodes/SurveyMonkey/Interfaces.ts | 47 +++ .../SurveyMonkey/SurveyMonkeyTrigger.node.ts | 316 ++++++++++++++++-- 5 files changed, 349 insertions(+), 31 deletions(-) create mode 100644 packages/nodes-base/nodes/SurveyMonkey/Interfaces.ts diff --git a/packages/nodes-base/credentials/SurveyMonkeyApi.credentials.ts b/packages/nodes-base/credentials/SurveyMonkeyApi.credentials.ts index 130690d423..66614595eb 100644 --- a/packages/nodes-base/credentials/SurveyMonkeyApi.credentials.ts +++ b/packages/nodes-base/credentials/SurveyMonkeyApi.credentials.ts @@ -12,11 +12,12 @@ export class SurveyMonkeyApi implements ICredentialType { name: 'accessToken', type: 'string' as NodePropertyTypes, default: '', - description: `The access token must have the following scopes:
- - Create/modify webhooks
- - View webhooks
- - View surveys
- - View collectors
+ description: `The access token must have the following scopes:
+ - Create/modify webhooks
+ - View webhooks
+ - View surveys
+ - View collectors
+ - View responses
- View response details`, }, { diff --git a/packages/nodes-base/nodes/Asana/GenericFunctions.ts b/packages/nodes-base/nodes/Asana/GenericFunctions.ts index 83bdfe01a2..1f6a1a5252 100644 --- a/packages/nodes-base/nodes/Asana/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Asana/GenericFunctions.ts @@ -41,7 +41,7 @@ export async function asanaApiRequest(this: IHookFunctions | IExecuteFunctions | // Return a clear error throw new Error('The Asana credentials are not valid!'); } - + console.log(error); if (error.response && error.response.body && error.response.body.errors) { // Try to return the error prettier const errorMessages = error.response.body.errors.map((errorData: { message: string }) => { diff --git a/packages/nodes-base/nodes/SurveyMonkey/GenericFunctions.ts b/packages/nodes-base/nodes/SurveyMonkey/GenericFunctions.ts index 2d2cf7995d..86f999b578 100644 --- a/packages/nodes-base/nodes/SurveyMonkey/GenericFunctions.ts +++ b/packages/nodes-base/nodes/SurveyMonkey/GenericFunctions.ts @@ -1,6 +1,6 @@ import { OptionsWithUri, - } from 'request'; +} from 'request'; import { IExecuteFunctions, @@ -52,7 +52,7 @@ export async function surveyMonkeyApiRequest(this: IExecuteFunctions | IWebhookF } } -export async function surveyMonkeyRequestAllItems(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any +export async function surveyMonkeyRequestAllItems(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any const returnData: IDataObject[] = []; diff --git a/packages/nodes-base/nodes/SurveyMonkey/Interfaces.ts b/packages/nodes-base/nodes/SurveyMonkey/Interfaces.ts new file mode 100644 index 0000000000..56c4e03a29 --- /dev/null +++ b/packages/nodes-base/nodes/SurveyMonkey/Interfaces.ts @@ -0,0 +1,47 @@ +import { + IDataObject, + } from 'n8n-workflow'; + +export interface IImage { + url: string; +} + +export interface IChoice { + position: number; + visible: boolean; + text: string; + id: string; + weight: number; + description: string; + image?: IImage; +} + +export interface IRow { + position: number; + visible: boolean; + text: string; + id: string; +} + +export interface IOther { + text: string; + visible: boolean; + is_answer_choice: boolean; + id: string; +} + +export interface IQuestion { + id: string; + family?: string; + subtype?: string; + headings?: IDataObject[]; + answers: IDataObject; + rows?: IDataObject; +} + +export interface IAnswer { + choice_id: string; + row_id?: string; + text?: string; + other_id?: string; +} diff --git a/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts b/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts index 51d101697a..49c44c9ac0 100644 --- a/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts +++ b/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts @@ -19,9 +19,17 @@ import { surveyMonkeyRequestAllItems, } from './GenericFunctions'; +import { + IAnswer, + IChoice, + IQuestion, + IRow, + IOther, +} from './Interfaces'; + import { createHmac, - } from 'crypto'; +} from 'crypto'; export class SurveyMonkeyTrigger implements INodeType { description: INodeTypeDescription = { @@ -58,9 +66,33 @@ export class SurveyMonkeyTrigger implements INodeType { }, ], properties: [ + { + displayName: 'Type', + name: 'objectType', + type: 'options', + options: [ + { + name: 'Collector', + value: 'collector', + }, + { + name: 'Survey', + value: 'survey', + }, + ], + default: '', + required: true, + }, { displayName: 'Event', name: 'event', + displayOptions: { + show: { + objectType: [ + 'survey' + ], + }, + }, type: 'options', options: [ { @@ -94,8 +126,8 @@ export class SurveyMonkeyTrigger implements INodeType { description: 'A response is deleted', }, { - name: 'Response Desqualified', - value: 'response_desqualified', + name: 'Response Disqualified', + value: 'response_disqualified', description: 'A survey response is disqualified ', }, { @@ -128,17 +160,56 @@ export class SurveyMonkeyTrigger implements INodeType { required: true, }, { - displayName: 'Type', - name: 'objectType', + displayName: 'Event', + name: 'event', type: 'options', + displayOptions: { + show: { + objectType: [ + 'collector', + ], + }, + }, options: [ { - name: 'Collector', - value: 'collector', + name: 'Collector Updated', + value: 'collector_updated', + description: 'A collector is updated', }, { - name: 'Survey', - value: 'survey', + name: 'Collector Deleted', + value: 'collector_deleted', + description: 'A collector is deleted', + }, + { + name: 'Response Completed', + value: 'response_completed', + description: 'A survey response is completed', + }, + { + name: 'Response Created', + value: 'response_created', + description: 'A respondent begins a survey', + }, + { + name: 'Response Deleted', + value: 'response_deleted', + description: 'A response is deleted', + }, + { + name: 'Response Disqualified', + value: 'response_disqualified', + description: 'A survey response is disqualified ', + }, + { + name: 'Response Overquota', + value: 'response_overquota', + description: `A response is over a survey’s quota`, + }, + { + name: 'Response Updated', + value: 'response_updated', + description: 'A survey response is updated', }, ], default: '', @@ -219,6 +290,23 @@ export class SurveyMonkeyTrigger implements INodeType { default: true, description: 'By default the webhook-data only contain the IDs. If this option gets activated it
will resolve the data automatically.', }, + { + displayName: 'Only Answers', + name: 'onlyAnswers', + displayOptions: { + show: { + resolveData: [ + true, + ], + event: [ + 'response_completed', + ], + }, + }, + type: 'boolean', + default: true, + description: 'Returns only the answers of the form and not any of the other data.', + }, ], }; @@ -275,7 +363,7 @@ export class SurveyMonkeyTrigger implements INodeType { const ids: string[] = []; - if (objectType === 'survey') { + if (objectType === 'survey' && event !== 'survey_created') { const surveyIds = this.getNodeParameter('surveyIds') as string[]; ids.push.apply(ids, surveyIds); } else if (objectType === 'collector') { @@ -305,7 +393,7 @@ export class SurveyMonkeyTrigger implements INodeType { const endpoint = '/webhooks'; const ids: string[] = []; - if (objectType === 'survey') { + if (objectType === 'survey' && event !== 'survey_created') { const surveyIds = this.getNodeParameter('surveyIds') as string[]; ids.push.apply(ids, surveyIds); } else if (objectType === 'collector') { @@ -321,12 +409,9 @@ export class SurveyMonkeyTrigger implements INodeType { event_type: event, }; - if (objectType === 'collector' && event === 'collector_created') { - throw new Error('Type collector cannot be used with collector created event'); - } - if (objectType === 'survey' && event === 'survey_created') { delete body.object_type; + delete body.object_ids; } let responseData: IDataObject = {}; @@ -390,7 +475,6 @@ export class SurveyMonkeyTrigger implements INodeType { }); req.on('end', async () => { - const computedSignature = createHmac('sha1', `${credentials.clientId}&${credentials.clientSecret}`).update(data.join('')).digest('base64'); if (headerData['sm-signature'] !== computedSignature) { // Signature is not valid so ignore call @@ -400,6 +484,12 @@ export class SurveyMonkeyTrigger implements INodeType { let responseData = JSON.parse(data.join('')); let endpoint = ''; + let returnItem: INodeExecutionData[] = [ + { + json: responseData, + } + ]; + if (event === 'response_completed') { const resolveData = this.getNodeParameter('resolveData') as boolean; if (resolveData) { @@ -409,18 +499,198 @@ export class SurveyMonkeyTrigger implements INodeType { endpoint = `/collectors/${responseData.resources.collector_id}/responses/${responseData.object_id}/details`; } responseData = await surveyMonkeyApiRequest.call(this, 'GET', endpoint); + const surveyId = responseData.survey_id; + + const questions: IQuestion[] = []; + const answers = new Map(); + + const { pages } = await surveyMonkeyApiRequest.call(this, 'GET', `/surveys/${surveyId}/details`); + + for (const page of pages) { + questions.push.apply(questions, page.questions); + } + + for (const page of responseData.pages as IDataObject[]) { + for (const question of page.questions as IDataObject[]) { + answers.set(question.id as string, question.answers as IAnswer[]); + } + } + + const responseQuestions = new Map(); + + for (const question of questions) { + + /* + TODO add support for premiun companents + - File Upload + - Matrix of dropdowm menus + */ + + // if question does not have an answer ignore it + if (!answers.get(question.id)) { + continue; + } + + const heading = question.headings![0].heading as string; + + if (question.family === 'open_ended' || question.family === 'datetime') { + if (question.subtype !== 'multi') { + responseQuestions.set(heading, answers.get(question.id)![0].text as string); + } else { + + const results: IDataObject = {}; + const keys = (question.answers.rows as IRow[]).map(e => e.text) as string[]; + const values = answers.get(question.id)?.map(e => e.text) as string[]; + for (let i = 0; i < keys.length; i++) { + // if for some reason there are questions texts repeted add the index to the key + if (results[keys[i]] !== undefined) { + results[`${keys[i]}(${i})`] = values[i] || ''; + } else { + results[keys[i]] = values[i] || ''; + } + } + responseQuestions.set(heading, results); + } + } + + if (question.family === 'single_choice') { + const other = question.answers.other as IOther; + if (other && other.visible && other.is_answer_choice && answers.get(question.id)![0].other_id) { + responseQuestions.set(heading, answers.get(question.id)![0].text as string); + + } else if (other && other.visible && !other.is_answer_choice){ + const choiceId = answers.get(question.id)![0].choice_id; + + const choice = (question.answers.choices as IChoice[]) + .filter(e => e.id === choiceId)[0]; + + const comment = answers.get(question.id) + ?.find(e => e.other_id === other.id)?.text as string; + responseQuestions.set(heading, { value: choice.text, comment }); + + } else { + const choiceId = answers.get(question.id)![0].choice_id; + const choice = (question.answers.choices as IChoice[]) + .filter(e => e.id === choiceId)[0]; + responseQuestions.set(heading, choice.text); + } + } + + if (question.family === 'multiple_choice') { + const other = question.answers.other as IOther; + const choiceIds = answers.get(question.id)?.map((e) => e.choice_id); + const value = (question.answers.choices as IChoice[]) + .filter(e => choiceIds?.includes(e.id)) + .map(e => e.text) as string[]; + // if "Add an "Other" Answer Option for Comments" is active and was selected + if (other && other.is_answer_choice && other.visible) { + const text = answers.get(question.id) + ?.find(e => e.other_id === other.id)?.text as string; + value.push(text); + } + responseQuestions.set(heading, value); + } + + if (question.family === 'matrix') { + // if more than one row it's a matrix/rating-scale + const rows = question.answers.rows as IRow[]; + + if (rows.length > 1) { + + const results: IDataObject = {}; + const choiceIds = answers.get(question.id)?.map(e => e.choice_id) as string[]; + const rowIds = answers.get(question.id)?.map(e => e.row_id) as string[]; + + const rowsValues = (question.answers.rows as IRow[]) + .filter(e => rowIds!.includes(e.id as string)) + .map(e => e.text); + + const choicesValues = (question.answers.choices as IChoice[]) + .filter(e => choiceIds!.includes(e.id as string)) + .map(e => e.text); + + for (let i = 0; i < rowsValues.length; i++) { + results[rowsValues[i]] = choicesValues[i] || ''; + } + + // add the rows that were not answered + for (const row of question.answers.rows as IDataObject[]) { + if (!rowIds.includes(row.id as string)) { + results[row.text as string] = ''; + } + } + // the comment then add the comment + const other = question.answers.other as IOther; + if (other !== undefined && other.visible) { + results.comment = answers.get(question.id)?.filter((e) => e.other_id)[0].text; + } + + responseQuestions.set(heading, results); + + } else { + const choiceIds = answers.get(question.id)?.map((e) => e.choice_id); + const value = (question.answers.choices as IChoice[]) + .filter(e => choiceIds!.includes(e.id as string)) + .map(e => (e.text === '') ? e.weight : e.text)[0]; + responseQuestions.set(heading, value); + + // if "Add an Other Answer Option for Comments" is active then add comment to the answer + const other = question.answers.other as IOther; + if (other !== undefined && other.visible) { + const response: IDataObject = {}; + //const questionName = (question.answers.other as IOther).text as string; + const text = answers.get(question.id)?.filter((e) => e.other_id)[0].text; + response.value = value; + response.comment = text; + responseQuestions.set(heading, response); + } + } + } + + if (question.family === 'demographic') { + const rows: IDataObject = {}; + for (const row of answers.get(question.id) as IAnswer[]) { + rows[row.row_id as string] = row.text; + } + const addressInfo: IDataObject = {}; + for (const answer of question.answers.rows as IDataObject[]) { + addressInfo[answer.type as string] = rows[answer.id as string] || ''; + } + responseQuestions.set(heading, addressInfo); + } + + if (question.family === 'presentation') { + if (question.subtype === 'image') { + const { url } = question.headings![0].image as IDataObject; + responseQuestions.set(heading, url as string); + } + } + } + delete responseData.pages; + responseData.questions = {}; + + // Map the "Map" to JSON + const tuples = JSON.parse(JSON.stringify([...responseQuestions])); + for (const [key, value] of tuples) { + responseData.questions[key] = value; + } + + const onlyAnswers = this.getNodeParameter('onlyAnswers') as boolean; + if (onlyAnswers) { + responseData = responseData.questions; + } + + returnItem = [ + { + json: responseData, + } + ]; } } - const returnItem: INodeExecutionData = { - json: responseData, - }; - return resolve({ workflowData: [ - [ - returnItem, - ], + returnItem, ], }); }); From 0b438e27388595c9d901d67bae262754780f8380 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sat, 2 May 2020 12:30:23 +0200 Subject: [PATCH 12/29] :zap: Small fixes --- packages/nodes-base/nodes/Asana/GenericFunctions.ts | 2 +- .../nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nodes-base/nodes/Asana/GenericFunctions.ts b/packages/nodes-base/nodes/Asana/GenericFunctions.ts index 1f6a1a5252..83bdfe01a2 100644 --- a/packages/nodes-base/nodes/Asana/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Asana/GenericFunctions.ts @@ -41,7 +41,7 @@ export async function asanaApiRequest(this: IHookFunctions | IExecuteFunctions | // Return a clear error throw new Error('The Asana credentials are not valid!'); } - console.log(error); + if (error.response && error.response.body && error.response.body.errors) { // Try to return the error prettier const errorMessages = error.response.body.errors.map((errorData: { message: string }) => { diff --git a/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts b/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts index 49c44c9ac0..efdc8dba5a 100644 --- a/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts +++ b/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts @@ -521,7 +521,7 @@ export class SurveyMonkeyTrigger implements INodeType { for (const question of questions) { /* - TODO add support for premiun companents + TODO: add support for premium components - File Upload - Matrix of dropdowm menus */ From 4f83d29afe7a8f0f09141ec3d43629e1be289e08 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sat, 2 May 2020 16:54:21 +0200 Subject: [PATCH 13/29] :zap: Small changes to FacebookGraphAPI-Node --- ...als.ts => FacebookGraphApi.credentials.ts} | 6 +- ...phApi.node.ts => FacebookGraphApi.node.ts} | 65 ++++++++----------- packages/nodes-base/package.json | 4 +- 3 files changed, 31 insertions(+), 44 deletions(-) rename packages/nodes-base/credentials/{GraphApi.credentials.ts => FacebookGraphApi.credentials.ts} (63%) rename packages/nodes-base/nodes/Facebook/{GraphApi.node.ts => FacebookGraphApi.node.ts} (86%) diff --git a/packages/nodes-base/credentials/GraphApi.credentials.ts b/packages/nodes-base/credentials/FacebookGraphApi.credentials.ts similarity index 63% rename from packages/nodes-base/credentials/GraphApi.credentials.ts rename to packages/nodes-base/credentials/FacebookGraphApi.credentials.ts index 132ba38e77..b779ca96e6 100644 --- a/packages/nodes-base/credentials/GraphApi.credentials.ts +++ b/packages/nodes-base/credentials/FacebookGraphApi.credentials.ts @@ -4,9 +4,9 @@ import { } from 'n8n-workflow'; -export class GraphApi implements ICredentialType { - name = 'graphApi'; - displayName = 'Graph API'; +export class FacebookGraphApi implements ICredentialType { + name = 'facebookGraphApi'; + displayName = 'Facebook Graph API'; properties = [ { displayName: 'Access Token', diff --git a/packages/nodes-base/nodes/Facebook/GraphApi.node.ts b/packages/nodes-base/nodes/Facebook/FacebookGraphApi.node.ts similarity index 86% rename from packages/nodes-base/nodes/Facebook/GraphApi.node.ts rename to packages/nodes-base/nodes/Facebook/FacebookGraphApi.node.ts index a81894c138..2117defb31 100644 --- a/packages/nodes-base/nodes/Facebook/GraphApi.node.ts +++ b/packages/nodes-base/nodes/Facebook/FacebookGraphApi.node.ts @@ -12,24 +12,23 @@ import { import { OptionsWithUri } from 'request'; -export class GraphApi implements INodeType { +export class FacebookGraphApi implements INodeType { description: INodeTypeDescription = { - displayName: 'Graph API', - name: 'graphApi', + displayName: 'Facebook Graph API', + name: 'facebookGraphApi', icon: 'file:facebook.png', group: ['transform'], version: 1, description: 'Interacts with Facebook using the Graph API', defaults: { - name: 'Graph API', + name: 'Facebook Graph API', color: '#772244', }, inputs: ['main'], - outputs: ['main', 'main'], - outputNames: ['success', 'failure'], + outputs: ['main'], credentials: [ { - name: 'graphApi', + name: 'facebookGraphApi', required: true, }, ], @@ -85,35 +84,35 @@ export class GraphApi implements INodeType { }, { name: 'v6.0', - value: 'v6.0/', + value: 'v6.0', }, { name: 'v5.0', - value: 'v5.0/', + value: 'v5.0', }, { name: 'v4.0', - value: 'v4.0/', + value: 'v4.0', }, { name: 'v3.3', - value: 'v3.3/', + value: 'v3.3', }, { name: 'v3.2', - value: 'v3.2/', + value: 'v3.2', }, { name: 'v3.1', - value: 'v3.1/', + value: 'v3.1', }, { name: 'v3.0', - value: 'v3.0/', + value: 'v3.0', }, { name: 'v2.12', - value: 'v2.12/', + value: 'v2.12', }, ], default: '', @@ -270,19 +269,22 @@ export class GraphApi implements INodeType { const items = this.getInputData(); let response: any; // tslint:disable-line:no-any - const successItems: INodeExecutionData[] = []; - const failureItems: INodeExecutionData[] = []; + const returnItems: INodeExecutionData[] = []; for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { - const graphApiCredentials = this.getCredentials('graphApi'); + const graphApiCredentials = this.getCredentials('facebookGraphApi'); const hostUrl = this.getNodeParameter('hostUrl', itemIndex) as string; const httpRequestMethod = this.getNodeParameter('httpRequestMethod', itemIndex) as string; - const graphApiVersion = this.getNodeParameter('graphApiVersion', itemIndex) as string; + let graphApiVersion = this.getNodeParameter('graphApiVersion', itemIndex) as string; const node = this.getNodeParameter('node', itemIndex) as string; const edge = this.getNodeParameter('edge', itemIndex) as string; const options = this.getNodeParameter('options', itemIndex, {}) as IDataObject; + if (graphApiVersion !== '') { + graphApiVersion += '/'; + } + let uri = `https://${hostUrl}/${graphApiVersion}${node}`; if (edge) { uri = `${uri}/${edge}`; @@ -315,7 +317,7 @@ export class GraphApi implements INodeType { if (options.queryParameters !== undefined) { const queryParameters = options.queryParameters as IDataObject; - if (queryParameters.parameter != undefined) { + if (queryParameters.parameter !== undefined) { for (const queryParameter of queryParameters.parameter as IDataObject[]) { requestOptions.qs[queryParameter.name as string] = queryParameter.value; } @@ -379,22 +381,7 @@ export class GraphApi implements INodeType { throw error; } - let errorItem; - if (error.response !== undefined) { - // Since this is a Graph API node and we already know the request was - // not successful, we'll go straight to the error details. - const graphApiErrors = error.response.body?.error ?? {}; - - errorItem = { - statusCode: error.statusCode, - ...graphApiErrors, - headers: error.response.headers, - }; - } else { - // Unknown Graph API response, we'll dump everything in the response item - errorItem = error; - } - failureItems.push({ json: { ...errorItem }}); + returnItems.push(items[itemIndex]); continue; } @@ -404,13 +391,13 @@ export class GraphApi implements INodeType { throw new Error('Response body is not valid JSON.'); } - failureItems.push({json: {message: response}}); + returnItems.push(items[itemIndex]); continue; } - successItems.push({json: response}); + returnItems.push({json: response}); } - return [successItems, failureItems]; + return [returnItems]; } } diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 0870305d90..dcc39d4ba0 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -48,13 +48,13 @@ "dist/credentials/DriftApi.credentials.js", "dist/credentials/DropboxApi.credentials.js", "dist/credentials/EventbriteApi.credentials.js", + "dist/credentials/FacebookGraphApi.credentials.js", "dist/credentials/FreshdeskApi.credentials.js", "dist/credentials/FileMaker.credentials.js", "dist/credentials/FlowApi.credentials.js", "dist/credentials/GithubApi.credentials.js", "dist/credentials/GitlabApi.credentials.js", "dist/credentials/GoogleApi.credentials.js", - "dist/credentials/GraphApi.credentials.js", "dist/credentials/GumroadApi.credentials.js", "dist/credentials/HarvestApi.credentials.js", "dist/credentials/HttpBasicAuth.credentials.js", @@ -155,6 +155,7 @@ "dist/nodes/Eventbrite/EventbriteTrigger.node.js", "dist/nodes/ExecuteCommand.node.js", "dist/nodes/ExecuteWorkflow.node.js", + "dist/nodes/Facebook/FacebookGraphApi.node.js", "dist/nodes/FileMaker/FileMaker.node.js", "dist/nodes/Freshdesk/Freshdesk.node.js", "dist/nodes/Flow/Flow.node.js", @@ -167,7 +168,6 @@ "dist/nodes/Gitlab/GitlabTrigger.node.js", "dist/nodes/Google/GoogleDrive.node.js", "dist/nodes/Google/GoogleSheets.node.js", - "dist/nodes/Facebook/GraphApi.node.js", "dist/nodes/GraphQL/GraphQL.node.js", "dist/nodes/Gumroad/GumroadTrigger.node.js", "dist/nodes/Harvest/Harvest.node.js", From ae853ce602feb18ede10be7d1e97ba4d6fddaf19 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sat, 2 May 2020 13:09:16 +0200 Subject: [PATCH 14/29] :zap: Small changes to Sms77 node --- ...credentials.ts => Sms77Api.credentials.ts} | 6 +-- .../nodes/Sms77/GenericFunctions.ts | 51 +++++++++++------- packages/nodes-base/nodes/Sms77/Sms77.node.ts | 14 ++--- packages/nodes-base/nodes/Sms77/sms77.png | Bin 3738 -> 1188 bytes packages/nodes-base/package.json | 3 +- 5 files changed, 43 insertions(+), 31 deletions(-) rename packages/nodes-base/credentials/{Sms77.credentials.ts => Sms77Api.credentials.ts} (67%) diff --git a/packages/nodes-base/credentials/Sms77.credentials.ts b/packages/nodes-base/credentials/Sms77Api.credentials.ts similarity index 67% rename from packages/nodes-base/credentials/Sms77.credentials.ts rename to packages/nodes-base/credentials/Sms77Api.credentials.ts index f098297dff..e560a9f124 100644 --- a/packages/nodes-base/credentials/Sms77.credentials.ts +++ b/packages/nodes-base/credentials/Sms77Api.credentials.ts @@ -3,9 +3,9 @@ import { NodePropertyTypes, } from 'n8n-workflow'; -export class Sms77 implements ICredentialType { - name = 'Sms77'; - displayName = 'Sms77'; +export class Sms77Api implements ICredentialType { + name = 'sms77Api'; + displayName = 'Sms77 API'; properties = [ { displayName: 'API Key', diff --git a/packages/nodes-base/nodes/Sms77/GenericFunctions.ts b/packages/nodes-base/nodes/Sms77/GenericFunctions.ts index 68e9fb281b..30499b3b29 100644 --- a/packages/nodes-base/nodes/Sms77/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Sms77/GenericFunctions.ts @@ -1,5 +1,12 @@ -import {IExecuteFunctions, IHookFunctions,} from 'n8n-core'; -import {IDataObject,} from 'n8n-workflow'; +import { + IExecuteFunctions, + IHookFunctions, +} from 'n8n-core'; + +import { + ICredentialDataDecryptedObject, + IDataObject, +} from 'n8n-workflow'; /** * Make an API request to MSG91 @@ -12,26 +19,17 @@ import {IDataObject,} from 'n8n-workflow'; * @returns {Promise} */ export async function sms77ApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, form: IDataObject, qs?: IDataObject): Promise { // tslint:disable-line:no-any - const setPayload = (o?: IDataObject) => { - if (!o) { - o = {}; - } - - o.p = credentials!.apiKey as string; - o.json = 1; - o.sendwith = 'n8n'; - - return o; - }; - - const credentials = this.getCredentials('Sms77'); + const credentials = this.getCredentials('sms77Api'); if (credentials === undefined) { throw new Error('No credentials got returned!'); } - 'GET' === method ? qs = setPayload(qs) : form = setPayload(form); - - const res = await this.helpers.request({ + if ('GET' === method) { + qs = setPayload(credentials, qs); + } else { + form = setPayload(credentials, form); + } + const response = await this.helpers.request({ form, json: true, method, @@ -39,9 +37,22 @@ export async function sms77ApiRequest(this: IHookFunctions | IExecuteFunctions, uri: `https://gateway.sms77.io/api/${endpoint}`, }); - if ('100' !== res.success) { + if ('100' !== response.success) { throw new Error('Invalid sms77 credentials or API error!'); } - return res; + return response; +} + + +function setPayload(credentials: ICredentialDataDecryptedObject, o?: IDataObject) { + if (!o) { + o = {}; + } + + o.p = credentials!.apiKey as string; + o.json = 1; + o.sendwith = 'n8n'; + + return o; } diff --git a/packages/nodes-base/nodes/Sms77/Sms77.node.ts b/packages/nodes-base/nodes/Sms77/Sms77.node.ts index c483253d42..ac8a10f4b6 100644 --- a/packages/nodes-base/nodes/Sms77/Sms77.node.ts +++ b/packages/nodes-base/nodes/Sms77/Sms77.node.ts @@ -19,7 +19,7 @@ export class Sms77 implements INodeType { outputs: ['main'], credentials: [ { - name: 'Sms77', + name: 'sms77Api', required: true, } ], @@ -131,13 +131,13 @@ export class Sms77 implements INodeType { throw new Error(`The operation "${operation}" is not known!`); } - returnData.push({ - requestId: await sms77ApiRequest.call(this, 'POST', 'sms', {}, { - from: this.getNodeParameter('from', i), - to: this.getNodeParameter('to', i), - text: this.getNodeParameter('message', i), - } as IDataObject) + const responseData = await sms77ApiRequest.call(this, 'POST', 'sms', {}, { + from: this.getNodeParameter('from', i), + to: this.getNodeParameter('to', i), + text: this.getNodeParameter('message', i), }); + + returnData.push(responseData); } return [this.helpers.returnJsonArray(returnData)]; } diff --git a/packages/nodes-base/nodes/Sms77/sms77.png b/packages/nodes-base/nodes/Sms77/sms77.png index 9ee5756efe882c1cf59152a73071072ce8b34295..500ba005ae607319db55ee5faf60847d2bc4bf87 100644 GIT binary patch delta 1179 zcmV;M1Z4Y~9i$168Gi!+000dlDL?=K0K-sBR7C;PYXH(|0MKLr(Psbu{{YZp1kz~z z{{6!9*%8!n`ThG#-jN&Db@Tl97u9nN(`}{h$SBx-@cj1J_vfnb%VpxBAl7&g)NcsV zYUTRx+xY3w_TtO+-?#D8lB>iY7$^47EP(XjB&Sm2r@*LvXi?8o%nn(V<*-<1^9a-r?T zi|M#M+=@BchmY#IT;ZG3_T)5k5X}Gp1Dr`jK~zY`&6d}i+CU72qcS#Y<4P~4_uh6X z3%hy$r;2SL1AnH)xyTn?2{ZrbSR-lt`#&q?C@tO zqN%sM_Zpu}t(Vs$M?rWk7IPcB&O9xrzuMi*C^KyB5)Eb}F`8TTS?m^{Dy7U!9RyZU!5uE z<#bR#1%Je7I!vqjNhx5{a41f*<3UTGl#)4JAjE+|Ner^baMhmu@>Enfg75aKvo^4_W;u2bSuK>^$_` zmSS3P{@*Is{r0?%SY%iIxg=khE_m{3pq))rmWkg-8bN-W>)8a_`X zz->jX4P3)hu&b#pf}=LkZ}6uA&tUMwrnu=Ov)MNQT)Mor2z(YthT2BSM&M%|n|~Gc zy@v!4{uKKnd&!`mTmCCZHUig!$Ur?mtiM>^=BpW(sv@`JhAw>eF)Xq>_QPJ+U#`iG zr5EfV(OVdmOv4W=q7_6fW}ZtqcaQ4eaq%*&ls+nc=1H=*aKkWoeSdqPHy?chZ|hv# t;G^vUo?k(>3>(`(p*ZNRvVUd202IcBM1Rmln|A;J002ovPDHLkV1h(LVyOTC literal 3738 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 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 82e0c02223..93ac08a158 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -92,7 +92,7 @@ "dist/credentials/RundeckApi.credentials.js", "dist/credentials/ShopifyApi.credentials.js", "dist/credentials/SlackApi.credentials.js", - "dist/credentials/Sms77.credentials.js", + "dist/credentials/Sms77Api.credentials.js", "dist/credentials/Smtp.credentials.js", "dist/credentials/StripeApi.credentials.js", "dist/credentials/SalesmateApi.credentials.js", @@ -220,6 +220,7 @@ "dist/nodes/Shopify/Shopify.node.js", "dist/nodes/Shopify/ShopifyTrigger.node.js", "dist/nodes/Slack/Slack.node.js", + "dist/nodes/Sms77/Sms77.node.js", "dist/nodes/SplitInBatches.node.js", "dist/nodes/SpreadsheetFile.node.js", "dist/nodes/SseTrigger.node.js", From cb7d4d86d43d0cfb1d0b6584d174bdda79c1077a Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sat, 2 May 2020 19:21:53 +0200 Subject: [PATCH 15/29] :zap: Optimize Facebook icon --- .../nodes-base/nodes/Facebook/facebook.png | Bin 24576 -> 1371 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/packages/nodes-base/nodes/Facebook/facebook.png b/packages/nodes-base/nodes/Facebook/facebook.png index b6cba78f88d9da3882f20f1a6d9a212c4fcaf62c..e0cc04460903fb05de99b1c4189d5170f44e6a02 100644 GIT binary patch literal 1371 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 Date: Sat, 2 May 2020 23:17:34 +0200 Subject: [PATCH 16/29] :pencil: Change licensor --- LICENSE.md | 2 +- packages/cli/LICENSE.md | 2 +- packages/core/LICENSE.md | 2 +- packages/editor-ui/LICENSE.md | 2 +- packages/node-dev/LICENSE.md | 2 +- packages/nodes-base/LICENSE.md | 2 +- packages/workflow/LICENSE.md | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index b3aadc2a0f..aac54547eb 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -21,7 +21,7 @@ Software: n8n License: Apache 2.0 -Licensor: Jan Oberhauser +Licensor: n8n GmbH --------------------------------------------------------------------- diff --git a/packages/cli/LICENSE.md b/packages/cli/LICENSE.md index b3aadc2a0f..aac54547eb 100644 --- a/packages/cli/LICENSE.md +++ b/packages/cli/LICENSE.md @@ -21,7 +21,7 @@ Software: n8n License: Apache 2.0 -Licensor: Jan Oberhauser +Licensor: n8n GmbH --------------------------------------------------------------------- diff --git a/packages/core/LICENSE.md b/packages/core/LICENSE.md index b3aadc2a0f..aac54547eb 100644 --- a/packages/core/LICENSE.md +++ b/packages/core/LICENSE.md @@ -21,7 +21,7 @@ Software: n8n License: Apache 2.0 -Licensor: Jan Oberhauser +Licensor: n8n GmbH --------------------------------------------------------------------- diff --git a/packages/editor-ui/LICENSE.md b/packages/editor-ui/LICENSE.md index b3aadc2a0f..aac54547eb 100644 --- a/packages/editor-ui/LICENSE.md +++ b/packages/editor-ui/LICENSE.md @@ -21,7 +21,7 @@ Software: n8n License: Apache 2.0 -Licensor: Jan Oberhauser +Licensor: n8n GmbH --------------------------------------------------------------------- diff --git a/packages/node-dev/LICENSE.md b/packages/node-dev/LICENSE.md index b3aadc2a0f..aac54547eb 100644 --- a/packages/node-dev/LICENSE.md +++ b/packages/node-dev/LICENSE.md @@ -21,7 +21,7 @@ Software: n8n License: Apache 2.0 -Licensor: Jan Oberhauser +Licensor: n8n GmbH --------------------------------------------------------------------- diff --git a/packages/nodes-base/LICENSE.md b/packages/nodes-base/LICENSE.md index b3aadc2a0f..aac54547eb 100644 --- a/packages/nodes-base/LICENSE.md +++ b/packages/nodes-base/LICENSE.md @@ -21,7 +21,7 @@ Software: n8n License: Apache 2.0 -Licensor: Jan Oberhauser +Licensor: n8n GmbH --------------------------------------------------------------------- diff --git a/packages/workflow/LICENSE.md b/packages/workflow/LICENSE.md index b3aadc2a0f..aac54547eb 100644 --- a/packages/workflow/LICENSE.md +++ b/packages/workflow/LICENSE.md @@ -21,7 +21,7 @@ Software: n8n License: Apache 2.0 -Licensor: Jan Oberhauser +Licensor: n8n GmbH --------------------------------------------------------------------- From c2b1d28072428d1d48f36e58a7a6c9d52f01b155 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=A4fer?= Date: Sun, 3 May 2020 12:28:15 +0200 Subject: [PATCH 17/29] Fix typo --- packages/nodes-base/nodes/Github/Github.node.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/Github/Github.node.ts b/packages/nodes-base/nodes/Github/Github.node.ts index 59df672290..b61c6d373f 100644 --- a/packages/nodes-base/nodes/Github/Github.node.ts +++ b/packages/nodes-base/nodes/Github/Github.node.ts @@ -212,7 +212,7 @@ export class Github implements INodeType { { name: 'Get Emails', value: 'getEmails', - description: 'Returns the repositories of a user', + description: 'Returns the email addresses of a user', }, { name: 'Get Repositories', From 42f6f4af2c8e02748ab35cbd6cd83ddf1f9eb0cb Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 3 May 2020 17:55:14 +0200 Subject: [PATCH 18/29] :zap: Display an error when unsaved workflow gets executed with webhook --- packages/cli/src/Server.ts | 7 ++----- packages/cli/src/TestWebhooks.ts | 4 ++++ packages/workflow/src/NodeHelpers.ts | 16 ++++++---------- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index b6a07aab44..ad2a3c1de5 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -512,11 +512,8 @@ class App { const sessionId = GenericHelpers.getSessionId(req); - // Check if workflow is saved as webhooks can only be tested with saved workflows. - // If that is the case check if any webhooks calls are present we have to wait for and - // if that is the case wait till we receive it. - if (WorkflowHelpers.isWorkflowIdValid(workflowData.id) === true && (runData === undefined || startNodes === undefined || startNodes.length === 0 || destinationNode === undefined)) { - // Webhooks can only be tested with saved workflows + // If webhooks nodes exist and are active we have to wait for till we receive a call + if (runData === undefined || startNodes === undefined || startNodes.length === 0 || destinationNode === undefined) { const credentials = await WorkflowCredentials(workflowData.nodes); const additionalData = await WorkflowExecuteAdditionalData.getBase(credentials); const nodeTypes = NodeTypes(); diff --git a/packages/cli/src/TestWebhooks.ts b/packages/cli/src/TestWebhooks.ts index 61d7213305..45ae624e2b 100644 --- a/packages/cli/src/TestWebhooks.ts +++ b/packages/cli/src/TestWebhooks.ts @@ -129,6 +129,10 @@ export class TestWebhooks { return false; } + if (workflow.id === undefined) { + throw new Error('Webhooks can only be added for saved workflows as an id is needed!'); + } + // Remove test-webhooks automatically if they do not get called (after 120 seconds) const timeout = setTimeout(() => { this.cancelTestWebhook(workflowData.id.toString()); diff --git a/packages/workflow/src/NodeHelpers.ts b/packages/workflow/src/NodeHelpers.ts index 76d8f8b55f..08e9660b99 100644 --- a/packages/workflow/src/NodeHelpers.ts +++ b/packages/workflow/src/NodeHelpers.ts @@ -728,12 +728,6 @@ export function getNodeWebhooks(workflow: Workflow, node: INode, additionalData: return []; } - if (workflow.id === undefined) { - // Workflow has no id which means it is not saved and so webhooks - // will not be enabled - return []; - } - const nodeType = workflow.nodeTypes.getByName(node.type) as INodeType; if (nodeType.description.webhooks === undefined) { @@ -741,12 +735,14 @@ export function getNodeWebhooks(workflow: Workflow, node: INode, additionalData: return []; } + const workflowId = workflow.id || '__UNSAVED__'; + const returnData: IWebhookData[] = []; for (const webhookDescription of nodeType.description.webhooks) { let nodeWebhookPath = workflow.getSimpleParameterValue(node, webhookDescription['path'], 'GET'); if (nodeWebhookPath === undefined) { // TODO: Use a proper logger - console.error(`No webhook path could be found for node "${node.name}" in workflow "${workflow.id}".`); + console.error(`No webhook path could be found for node "${node.name}" in workflow "${workflowId}".`); continue; } @@ -756,13 +752,13 @@ export function getNodeWebhooks(workflow: Workflow, node: INode, additionalData: nodeWebhookPath = nodeWebhookPath.slice(1); } - const path = getNodeWebhookPath(workflow.id, node, nodeWebhookPath); + const path = getNodeWebhookPath(workflowId, node, nodeWebhookPath); const httpMethod = workflow.getSimpleParameterValue(node, webhookDescription['httpMethod'], 'GET'); if (httpMethod === undefined) { // TODO: Use a proper logger - console.error(`The webhook "${path}" for node "${node.name}" in workflow "${workflow.id}" could not be added because the httpMethod is not defined.`); + console.error(`The webhook "${path}" for node "${node.name}" in workflow "${workflowId}" could not be added because the httpMethod is not defined.`); continue; } @@ -771,7 +767,7 @@ export function getNodeWebhooks(workflow: Workflow, node: INode, additionalData: node: node.name, path, webhookDescription, - workflowId: workflow.id, + workflowId, workflowExecuteAdditionalData: additionalData, }); } From 9dbec2277a4570b92d940f9fc2eb4c12814c3082 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 4 May 2020 09:01:37 +0200 Subject: [PATCH 19/29] :bookmark: Release n8n-workflow@0.30.0 --- packages/workflow/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/workflow/package.json b/packages/workflow/package.json index 7e90bfb26f..72af56092d 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -1,6 +1,6 @@ { "name": "n8n-workflow", - "version": "0.29.0", + "version": "0.30.0", "description": "Workflow base code of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From 2d429a789dd55cabc90cd62c95f9c27150999a6a Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 4 May 2020 09:03:11 +0200 Subject: [PATCH 20/29] :arrow_up: Set n8n-workflow@0.30.0 on n8n-core --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 8ee7217eb4..9813720a0d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -44,7 +44,7 @@ "crypto-js": "3.1.9-1", "lodash.get": "^4.4.2", "mmmagic": "^0.5.2", - "n8n-workflow": "~0.29.0", + "n8n-workflow": "~0.30.0", "p-cancelable": "^2.0.0", "request": "^2.88.2", "request-promise-native": "^1.0.7" From d282d3390359955c999e74e13e707d49e5f59cd8 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 4 May 2020 09:03:52 +0200 Subject: [PATCH 21/29] :bookmark: Release n8n-core@0.33.0 --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 9813720a0d..b58064344b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "n8n-core", - "version": "0.32.0", + "version": "0.33.0", "description": "Core functionality of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From f6e821d12281954db3d0c7393f1c972cacf07a05 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 4 May 2020 09:05:14 +0200 Subject: [PATCH 22/29] :arrow_up: Set n8n-core@0.33.0 and n8n-workflow@0.30.0 on n8n-nodes-base --- packages/nodes-base/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index e8e7078787..e59fd8354f 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -276,7 +276,7 @@ "@types/xml2js": "^0.4.3", "gulp": "^4.0.0", "jest": "^24.9.0", - "n8n-workflow": "~0.29.0", + "n8n-workflow": "~0.30.0", "ts-jest": "^24.0.2", "tslint": "^5.17.0", "typescript": "~3.7.4" @@ -300,7 +300,7 @@ "moment-timezone": "^0.5.28", "mongodb": "^3.5.5", "mysql2": "^2.0.1", - "n8n-core": "~0.32.0", + "n8n-core": "~0.33.0", "nodemailer": "^5.1.1", "pdf-parse": "^1.1.1", "pg-promise": "^9.0.3", From 516a4e7b54ea7a7ae6948e609db8aa078bb03f38 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 4 May 2020 09:05:54 +0200 Subject: [PATCH 23/29] :bookmark: Release n8n-nodes-base@0.61.0 --- packages/nodes-base/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index e59fd8354f..2bb3628c13 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -1,6 +1,6 @@ { "name": "n8n-nodes-base", - "version": "0.60.0", + "version": "0.61.0", "description": "Base nodes of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From 49e4bd971e6e83e7db6ebb45a985914b7f4f23fe Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 4 May 2020 09:07:03 +0200 Subject: [PATCH 24/29] :arrow_up: Set n8n-workflow@0.30.0 on n8n-editor-ui --- packages/editor-ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index f99eecd94d..0ec977d7b6 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -64,7 +64,7 @@ "lodash.debounce": "^4.0.8", "lodash.get": "^4.4.2", "lodash.set": "^4.3.2", - "n8n-workflow": "~0.29.0", + "n8n-workflow": "~0.30.0", "node-sass": "^4.12.0", "prismjs": "^1.17.1", "quill": "^2.0.0-dev.3", From 55c6fca4e30d071cd7630d86536ad740581282eb Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 4 May 2020 09:07:34 +0200 Subject: [PATCH 25/29] :bookmark: Release n8n-editor-ui@0.44.0 --- packages/editor-ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 0ec977d7b6..f25c05530f 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -1,6 +1,6 @@ { "name": "n8n-editor-ui", - "version": "0.43.0", + "version": "0.44.0", "description": "Workflow Editor UI for n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From 3d491adbaf1b57ceb8143be3dc7edf5384f7a69a Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 4 May 2020 09:09:07 +0200 Subject: [PATCH 26/29] :arrow_up: Set n8n-core@0.33.0, n8n-editor-ui@0.44.0, n8n-nodes-base@0.61.0 and n8n-workflow@0.30.0 on n8n --- packages/cli/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 913ea23410..da2376e1bb 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -95,10 +95,10 @@ "lodash.get": "^4.4.2", "mongodb": "^3.5.5", "mysql2": "^2.0.1", - "n8n-core": "~0.32.0", - "n8n-editor-ui": "~0.43.0", - "n8n-nodes-base": "~0.60.0", - "n8n-workflow": "~0.29.0", + "n8n-core": "~0.33.0", + "n8n-editor-ui": "~0.44.0", + "n8n-nodes-base": "~0.61.0", + "n8n-workflow": "~0.30.0", "open": "^7.0.0", "pg": "^7.11.0", "request-promise-native": "^1.0.7", From 1a63af460a4980c3011411ff9a17aa347288a2df Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 4 May 2020 09:09:50 +0200 Subject: [PATCH 27/29] :bookmark: Release n8n@0.66.0 --- packages/cli/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index da2376e1bb..bb18b70179 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "0.65.0", + "version": "0.66.0", "description": "n8n Workflow Automation Tool", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From 98ac9d93bce9ef041bea501b88f38ee3c107452c Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 4 May 2020 19:35:02 +0200 Subject: [PATCH 28/29] :zap: Fix loading of projects for hosted Jira --- packages/nodes-base/nodes/Jira/IssueDescription.ts | 3 +++ packages/nodes-base/nodes/Jira/JiraSoftwareCloud.node.ts | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/nodes-base/nodes/Jira/IssueDescription.ts b/packages/nodes-base/nodes/Jira/IssueDescription.ts index 5f3c18c9d6..cd788c886e 100644 --- a/packages/nodes-base/nodes/Jira/IssueDescription.ts +++ b/packages/nodes-base/nodes/Jira/IssueDescription.ts @@ -81,6 +81,9 @@ export const issueFields = [ }, typeOptions: { loadOptionsMethod: 'getProjects', + loadOptionsDependsOn: [ + 'jiraVersion', + ], }, description: 'Project', }, diff --git a/packages/nodes-base/nodes/Jira/JiraSoftwareCloud.node.ts b/packages/nodes-base/nodes/Jira/JiraSoftwareCloud.node.ts index 7335d24b6b..17839e3368 100644 --- a/packages/nodes-base/nodes/Jira/JiraSoftwareCloud.node.ts +++ b/packages/nodes-base/nodes/Jira/JiraSoftwareCloud.node.ts @@ -111,9 +111,10 @@ export class JiraSoftwareCloud implements INodeType { // select them easily async getProjects(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - const jiraCloudCredentials = this.getCredentials('jiraSoftwareCloudApi'); + const jiraVersion = this.getCurrentNodeParameter('jiraVersion') as string; + let endpoint = '/project/search'; - if (jiraCloudCredentials === undefined) { + if (jiraVersion === 'server') { endpoint = '/project'; } let projects = await jiraSoftwareCloudApiRequest.call(this, endpoint, 'GET'); From 4269e8b91468af80d4e468efb60f9a32b5346c7a Mon Sep 17 00:00:00 2001 From: Boris Berenberg Date: Mon, 4 May 2020 16:06:13 -0400 Subject: [PATCH 29/29] minor grammar fixes --- packages/editor-ui/src/components/ExecutionsList.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/editor-ui/src/components/ExecutionsList.vue b/packages/editor-ui/src/components/ExecutionsList.vue index 541e77d1b7..e8721bcf9b 100644 --- a/packages/editor-ui/src/components/ExecutionsList.vue +++ b/packages/editor-ui/src/components/ExecutionsList.vue @@ -504,11 +504,11 @@ export default mixins( } else if (entry.finished === true) { return 'The worklow execution was successful.'; } else if (entry.retryOf !== undefined) { - return `The workflow execution was a retry of "${entry.retryOf}" and did fail.
New retries have to be started from the original execution.`; + return `The workflow execution was a retry of "${entry.retryOf}" and failed.
New retries have to be started from the original execution.`; } else if (entry.retrySuccessId !== undefined) { - return `The workflow execution did fail but the retry "${entry.retrySuccessId}" was successful.`; + return `The workflow execution failed but the retry "${entry.retrySuccessId}" was successful.`; } else { - return 'The workflow execution did fail.'; + return 'The workflow execution failed.'; } }, async stopExecution (activeExecutionId: string) {