diff --git a/LICENSE.md b/LICENSE.md index 24a7d38fc..3dd835dee 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -19,7 +19,7 @@ Condition notice. Software: n8n -License: Apache 2.0 +License: Apache 2.0 with Commons Clause Licensor: n8n GmbH diff --git a/docker/compose/withMongo/.env b/docker/compose/withMongo/.env deleted file mode 100644 index a1141f23f..000000000 --- a/docker/compose/withMongo/.env +++ /dev/null @@ -1,9 +0,0 @@ -MONGO_INITDB_ROOT_USERNAME=changeUser -MONGO_INITDB_ROOT_PASSWORD=changePassword -MONGO_INITDB_DATABASE=n8n - -MONGO_NON_ROOT_USERNAME=changeUser -MONGO_NON_ROOT_PASSWORD=changePassword - -N8N_BASIC_AUTH_USER=changeUser -N8N_BASIC_AUTH_PASSWORD=changePassword diff --git a/docker/compose/withMongo/README.md b/docker/compose/withMongo/README.md deleted file mode 100644 index bfb4b0f64..000000000 --- a/docker/compose/withMongo/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# n8n with MongoDB - -Starts n8n with MongoDB as database. - - -## Start - -To start n8n with MongoDB simply start docker-compose by executing the following -command in the current folder. - - -**IMPORTANT:** But before you do that change the default users and passwords in the `.env` file! - -``` -docker-compose up -d -``` - -To stop it execute: - -``` -docker-compose stop -``` - -## Configuration - -The default name of the database, user and password for MongoDB can be changed in the `.env` file in the current directory. diff --git a/docker/compose/withMongo/docker-compose.yml b/docker/compose/withMongo/docker-compose.yml deleted file mode 100644 index 67cfcf001..000000000 --- a/docker/compose/withMongo/docker-compose.yml +++ /dev/null @@ -1,34 +0,0 @@ -version: '3.1' - -services: - - mongo: - image: mongo:4.0 - restart: always - environment: - - MONGO_INITDB_ROOT_USERNAME - - MONGO_INITDB_ROOT_PASSWORD - - MONGO_INITDB_DATABASE - - MONGO_NON_ROOT_USERNAME - - MONGO_NON_ROOT_PASSWORD - volumes: - - ./init-data.sh:/docker-entrypoint-initdb.d/init-data.sh - - n8n: - image: n8nio/n8n - restart: always - environment: - - DB_TYPE=mongodb - - DB_MONGODB_CONNECTION_URL=mongodb://${MONGO_NON_ROOT_USERNAME}:${MONGO_NON_ROOT_PASSWORD}@mongo:27017/${MONGO_INITDB_DATABASE} - - N8N_BASIC_AUTH_ACTIVE=true - - N8N_BASIC_AUTH_USER - - N8N_BASIC_AUTH_PASSWORD - ports: - - 5678:5678 - links: - - mongo - volumes: - - ~/.n8n:/root/.n8n - # Wait 5 seconds to start n8n to make sure that MongoDB is ready - # when n8n tries to connect to it - command: /bin/sh -c "sleep 5; n8n start" diff --git a/docker/compose/withMongo/init-data.sh b/docker/compose/withMongo/init-data.sh deleted file mode 100755 index bf5c10c84..000000000 --- a/docker/compose/withMongo/init-data.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -set -e; - -# Create a default non-root role -MONGO_NON_ROOT_ROLE="${MONGO_NON_ROOT_ROLE:-readWrite}" - -if [ -n "${MONGO_NON_ROOT_USERNAME:-}" ] && [ -n "${MONGO_NON_ROOT_PASSWORD:-}" ]; then - "${mongo[@]}" "$MONGO_INITDB_DATABASE" <<-EOJS - db.createUser({ - user: $(_js_escape "$MONGO_NON_ROOT_USERNAME"), - pwd: $(_js_escape "$MONGO_NON_ROOT_PASSWORD"), - roles: [ { role: $(_js_escape "$MONGO_NON_ROOT_ROLE"), db: $(_js_escape "$MONGO_INITDB_DATABASE") } ] - }) - EOJS -else - echo "SETUP INFO: No Environment variables given!" -fi diff --git a/packages/cli/BREAKING-CHANGES.md b/packages/cli/BREAKING-CHANGES.md index 9269427ae..849e5f26b 100644 --- a/packages/cli/BREAKING-CHANGES.md +++ b/packages/cli/BREAKING-CHANGES.md @@ -2,6 +2,43 @@ This list shows all the versions which include breaking changes and how to upgrade. +## 0.87.0 + +### What changed? + +The link.fish node got removed because the service is shutting down. + +### When is action necessary? + +If you are are actively using the link.fish node. + +### How to upgrade: + +Unfortunately, that's not possible. We'd recommend you to look for an alternative service. + + +## 0.83.0 + +### What changed? + +In the Active Campaign Node, we have changed how the `getAll` operation works with various resources for the sake of consistency. To achieve this, a new parameter called 'Simple' has been added. + +### When is action necessary? + +When one of the following resources/operations is used: + +| Resource | Operation | +|--|--| +| Deal | Get All | +| Connector | Get All | +| E-commerce Order | Get All | +| E-commerce Customer | Get All | +| E-commerce Order Products | Get All | + +### How to upgrade: + +Open the affected resource/operation and set the parameter `Simple` to false. + ## 0.79.0 ### What changed? diff --git a/packages/cli/LICENSE.md b/packages/cli/LICENSE.md index 24a7d38fc..3dd835dee 100644 --- a/packages/cli/LICENSE.md +++ b/packages/cli/LICENSE.md @@ -19,7 +19,7 @@ Condition notice. Software: n8n -License: Apache 2.0 +License: Apache 2.0 with Commons Clause Licensor: n8n GmbH diff --git a/packages/cli/bin/n8n b/packages/cli/bin/n8n index 7f02bd1a7..583dfa5bf 100755 --- a/packages/cli/bin/n8n +++ b/packages/cli/bin/n8n @@ -10,7 +10,7 @@ process.env.NODE_CONFIG_DIR = process.env.NODE_CONFIG_DIR || path.join(__dirname var versionFlags = [ // tslint:disable-line:no-var-keyword '-v', '-V', - '--version' + '--version', ]; if (versionFlags.includes(process.argv.slice(-1)[0])) { console.log(require('../package').version); @@ -22,23 +22,10 @@ if (process.argv.length === 2) { process.argv.push('start'); } -var command = process.argv[2]; // tslint:disable-line:no-var-keyword +var nodeVersion = process.versions.node.split('.'); -// Check if the command the user did enter is supported else stop -var supportedCommands = [ // tslint:disable-line:no-var-keyword - 'execute', - 'help', - 'start', -]; - -if (!supportedCommands.includes(command)) { - console.log('\nThe command "' + command + '" is not known!\n'); - process.argv.pop(); - process.argv.push('--help'); -} - -if (parseInt(process.versions.node.split('.')[0], 10) < 10) { - console.log('\nThe Node.js version is too old to run n8n. Please use version 10 or later!\n'); +if (parseInt(nodeVersion[0], 10) < 12 || parseInt(nodeVersion[0], 10) === 12 && parseInt(nodeVersion[1], 10) < 9) { + console.log(`\nYour Node.js version (${process.versions.node}) is too old to run n8n.\nPlease update to version 12.9 or later!\n`); process.exit(0); } diff --git a/packages/cli/commands/config/workflow/deactivate.ts b/packages/cli/commands/config/workflow/deactivate.ts new file mode 100644 index 000000000..8c76dcf79 --- /dev/null +++ b/packages/cli/commands/config/workflow/deactivate.ts @@ -0,0 +1,70 @@ +import { + Command, flags, +} from '@oclif/command'; + +import { + IDataObject +} from 'n8n-workflow'; + +import { + Db, + GenericHelpers, +} from "../../../src"; + + +export class DeactivateCommand extends Command { + static description = '\nDeactivates workflows'; + + static examples = [ + `$ n8n config:workflow:deactivate --all`, + `$ n8n config:workflow:deactivate --id=5`, + ]; + + static flags = { + help: flags.help({ char: 'h' }), + all: flags.boolean({ + description: 'Deactivates all workflows', + }), + id: flags.string({ + description: 'Deactivats the workflow with the given ID', + }), + }; + + async run() { + const { flags } = this.parse(DeactivateCommand); + + if (!flags.all && !flags.id) { + GenericHelpers.logOutput(`Either option "--all" or "--id" have to be set!`); + return; + } + + if (flags.all && flags.id) { + GenericHelpers.logOutput(`Either "--all" or "--id" can be set never both!`); + return; + } + + try { + await Db.init(); + + const findQuery: IDataObject = {}; + if (flags.id) { + console.log(`Deactivating workflow with ID: ${flags.id}`); + findQuery.id = flags.id; + } else { + console.log('Deactivating all workflows'); + findQuery.active = true; + } + + await Db.collections.Workflow!.update(findQuery, { active: false }); + console.log('Done'); + } catch (e) { + console.error('\nGOT ERROR'); + console.log('===================================='); + console.error(e.message); + console.error(e.stack); + this.exit(1); + } + + this.exit(); + } +} diff --git a/packages/cli/commands/execute.ts b/packages/cli/commands/execute.ts index 3eb5956e9..b3450e9ef 100644 --- a/packages/cli/commands/execute.ts +++ b/packages/cli/commands/execute.ts @@ -10,6 +10,7 @@ import { import { ActiveExecutions, CredentialsOverwrites, + CredentialTypes, Db, ExternalHooks, GenericHelpers, @@ -116,6 +117,8 @@ export class Execute extends Command { // Add the found types to an instance other parts of the application can use const nodeTypes = NodeTypes(); await nodeTypes.init(loadNodesAndCredentials.nodeTypes); + const credentialTypes = CredentialTypes(); + await credentialTypes.init(loadNodesAndCredentials.credentialTypes); if (!WorkflowHelpers.isWorkflowIdValid(workflowId)) { workflowId = undefined; diff --git a/packages/cli/commands/start.ts b/packages/cli/commands/start.ts index ef9170a92..8e14b98e2 100644 --- a/packages/cli/commands/start.ts +++ b/packages/cli/commands/start.ts @@ -8,12 +8,14 @@ const open = require('open'); import * as config from '../config'; import { + ActiveExecutions, ActiveWorkflowRunner, - CredentialTypes, CredentialsOverwrites, + CredentialTypes, Db, ExternalHooks, GenericHelpers, + IExecutionsCurrentSummary, LoadNodesAndCredentials, NodeTypes, Server, @@ -68,23 +70,46 @@ export class Start extends Command { static async stopProcess() { console.log(`\nStopping n8n...`); - setTimeout(() => { - // In case that something goes wrong with shutdown we - // kill after max. 30 seconds no matter what - process.exit(processExistCode); - }, 30000); + try { + const externalHooks = ExternalHooks(); + await externalHooks.run('n8n.stop', []); - const removePromises = []; - if (activeWorkflowRunner !== undefined) { - removePromises.push(activeWorkflowRunner.removeAll()); + setTimeout(() => { + // In case that something goes wrong with shutdown we + // kill after max. 30 seconds no matter what + process.exit(processExistCode); + }, 30000); + + const removePromises = []; + if (activeWorkflowRunner !== undefined) { + removePromises.push(activeWorkflowRunner.removeAll()); + } + + // Remove all test webhooks + const testWebhooks = TestWebhooks.getInstance(); + removePromises.push(testWebhooks.removeAll()); + + await Promise.all(removePromises); + + // Wait for active workflow executions to finish + const activeExecutionsInstance = ActiveExecutions.getInstance(); + let executingWorkflows = activeExecutionsInstance.getActiveExecutions(); + + let count = 0; + while (executingWorkflows.length !== 0) { + if (count++ % 4 === 0) { + console.log(`Waiting for ${executingWorkflows.length} active executions to finish...`); + } + await new Promise((resolve) => { + setTimeout(resolve, 500); + }); + executingWorkflows = activeExecutionsInstance.getActiveExecutions(); + } + + } catch (error) { + console.error('There was an error shutting down n8n.', error); } - // Remove all test webhooks - const testWebhooks = TestWebhooks.getInstance(); - removePromises.push(testWebhooks.removeAll()); - - await Promise.all(removePromises); - process.exit(processExistCode); } diff --git a/packages/cli/config/index.ts b/packages/cli/config/index.ts index 32dec2438..e654b50d0 100644 --- a/packages/cli/config/index.ts +++ b/packages/cli/config/index.ts @@ -10,58 +10,58 @@ const config = convict({ doc: 'Type of database to use', format: ['sqlite', 'mariadb', 'mongodb', 'mysqldb', 'postgresdb'], default: 'sqlite', - env: 'DB_TYPE' + env: 'DB_TYPE', }, mongodb: { connectionUrl: { doc: 'MongoDB Connection URL', format: '*', default: 'mongodb://user:password@localhost:27017/database', - env: 'DB_MONGODB_CONNECTION_URL' - } + env: 'DB_MONGODB_CONNECTION_URL', + }, }, tablePrefix: { doc: 'Prefix for table names', format: '*', default: '', - env: 'DB_TABLE_PREFIX' + env: 'DB_TABLE_PREFIX', }, postgresdb: { database: { doc: 'PostgresDB Database', format: String, default: 'n8n', - env: 'DB_POSTGRESDB_DATABASE' + env: 'DB_POSTGRESDB_DATABASE', }, host: { doc: 'PostgresDB Host', format: String, default: 'localhost', - env: 'DB_POSTGRESDB_HOST' + env: 'DB_POSTGRESDB_HOST', }, password: { doc: 'PostgresDB Password', format: String, default: '', - env: 'DB_POSTGRESDB_PASSWORD' + env: 'DB_POSTGRESDB_PASSWORD', }, port: { doc: 'PostgresDB Port', format: Number, default: 5432, - env: 'DB_POSTGRESDB_PORT' + env: 'DB_POSTGRESDB_PORT', }, user: { doc: 'PostgresDB User', format: String, default: 'root', - env: 'DB_POSTGRESDB_USER' + env: 'DB_POSTGRESDB_USER', }, schema: { doc: 'PostgresDB Schema', format: String, default: 'public', - env: 'DB_POSTGRESDB_SCHEMA' + env: 'DB_POSTGRESDB_SCHEMA', }, ssl: { @@ -89,7 +89,7 @@ const config = convict({ default: true, env: 'DB_POSTGRESDB_SSL_REJECT_UNAUTHORIZED', }, - } + }, }, mysqldb: { @@ -97,31 +97,31 @@ const config = convict({ doc: 'MySQL Database', format: String, default: 'n8n', - env: 'DB_MYSQLDB_DATABASE' + env: 'DB_MYSQLDB_DATABASE', }, host: { doc: 'MySQL Host', format: String, default: 'localhost', - env: 'DB_MYSQLDB_HOST' + env: 'DB_MYSQLDB_HOST', }, password: { doc: 'MySQL Password', format: String, default: '', - env: 'DB_MYSQLDB_PASSWORD' + env: 'DB_MYSQLDB_PASSWORD', }, port: { doc: 'MySQL Port', format: Number, default: 3306, - env: 'DB_MYSQLDB_PORT' + env: 'DB_MYSQLDB_PORT', }, user: { doc: 'MySQL User', format: String, default: 'root', - env: 'DB_MYSQLDB_USER' + env: 'DB_MYSQLDB_USER', }, }, }, @@ -136,7 +136,7 @@ const config = convict({ doc: 'Overwrites for credentials', format: '*', default: '{}', - env: 'CREDENTIALS_OVERWRITE_DATA' + env: 'CREDENTIALS_OVERWRITE_DATA', }, endpoint: { doc: 'Fetch credentials from API', @@ -156,7 +156,7 @@ const config = convict({ doc: 'In what process workflows should be executed', format: ['main', 'own'], default: 'own', - env: 'EXECUTIONS_PROCESS' + env: 'EXECUTIONS_PROCESS', }, // A Workflow times out and gets canceled after this time (seconds). @@ -174,13 +174,13 @@ const config = convict({ doc: 'Max run time (seconds) before stopping the workflow execution', format: Number, default: -1, - env: 'EXECUTIONS_TIMEOUT' + env: 'EXECUTIONS_TIMEOUT', }, maxTimeout: { doc: 'Max execution time (seconds) that can be set for a workflow individually', format: Number, default: 3600, - env: 'EXECUTIONS_TIMEOUT_MAX' + env: 'EXECUTIONS_TIMEOUT_MAX', }, // If a workflow executes all the data gets saved by default. This @@ -193,13 +193,13 @@ const config = convict({ doc: 'What workflow execution data to save on error', format: ['all', 'none'], default: 'all', - env: 'EXECUTIONS_DATA_SAVE_ON_ERROR' + env: 'EXECUTIONS_DATA_SAVE_ON_ERROR', }, saveDataOnSuccess: { doc: 'What workflow execution data to save on success', format: ['all', 'none'], default: 'all', - env: 'EXECUTIONS_DATA_SAVE_ON_SUCCESS' + env: 'EXECUTIONS_DATA_SAVE_ON_SUCCESS', }, // If the executions of workflows which got started via the editor @@ -211,7 +211,7 @@ const config = convict({ doc: 'Save data of executions when started manually via editor', format: 'Boolean', default: false, - env: 'EXECUTIONS_DATA_SAVE_MANUAL_EXECUTIONS' + env: 'EXECUTIONS_DATA_SAVE_MANUAL_EXECUTIONS', }, // To not exceed the database's capacity and keep its size moderate @@ -223,19 +223,19 @@ const config = convict({ doc: 'Delete data of past executions on a rolling basis', format: 'Boolean', default: false, - env: 'EXECUTIONS_DATA_PRUNE' + env: 'EXECUTIONS_DATA_PRUNE', }, pruneDataMaxAge: { doc: 'How old (hours) the execution data has to be to get deleted', format: Number, default: 336, - env: 'EXECUTIONS_DATA_MAX_AGE' + env: 'EXECUTIONS_DATA_MAX_AGE', }, pruneDataTimeout: { doc: 'Timeout (seconds) after execution data has been pruned', format: Number, default: 3600, - env: 'EXECUTIONS_DATA_PRUNE_TIMEOUT' + env: 'EXECUTIONS_DATA_PRUNE_TIMEOUT', }, }, @@ -248,7 +248,7 @@ const config = convict({ doc: 'The timezone to use', format: '*', default: 'America/New_York', - env: 'GENERIC_TIMEZONE' + env: 'GENERIC_TIMEZONE', }, }, @@ -258,66 +258,78 @@ const config = convict({ default: '/', arg: 'path', env: 'N8N_PATH', - doc: 'Path n8n is deployed to' + doc: 'Path n8n is deployed to', }, host: { format: String, default: 'localhost', arg: 'host', env: 'N8N_HOST', - doc: 'Host name n8n can be reached' + doc: 'Host name n8n can be reached', }, port: { format: Number, default: 5678, arg: 'port', env: 'N8N_PORT', - doc: 'HTTP port n8n can be reached' + doc: 'HTTP port n8n can be reached', }, listen_address: { format: String, default: '0.0.0.0', env: 'N8N_LISTEN_ADDRESS', - doc: 'IP address n8n should listen on' + doc: 'IP address n8n should listen on', }, protocol: { format: ['http', 'https'], default: 'http', env: 'N8N_PROTOCOL', - doc: 'HTTP Protocol via which n8n can be reached' + doc: 'HTTP Protocol via which n8n can be reached', }, ssl_key: { format: String, default: '', env: 'N8N_SSL_KEY', - doc: 'SSL Key for HTTPS Protocol' + doc: 'SSL Key for HTTPS Protocol', }, ssl_cert: { format: String, default: '', env: 'N8N_SSL_CERT', - doc: 'SSL Cert for HTTPS Protocol' + doc: 'SSL Cert for HTTPS Protocol', }, security: { + excludeEndpoints: { + doc: 'Additional endpoints to exclude auth checks. Multiple endpoints can be separated by colon (":")', + format: String, + default: '', + env: 'N8N_AUTH_EXCLUDE_ENDPOINTS', + }, basicAuth: { active: { format: 'Boolean', default: false, env: 'N8N_BASIC_AUTH_ACTIVE', - doc: 'If basic auth should be activated for editor and REST-API' + doc: 'If basic auth should be activated for editor and REST-API', }, user: { format: String, default: '', env: 'N8N_BASIC_AUTH_USER', - doc: 'The name of the basic auth user' + doc: 'The name of the basic auth user', }, password: { format: String, default: '', env: 'N8N_BASIC_AUTH_PASSWORD', - doc: 'The password of the basic auth user' + doc: 'The password of the basic auth user', + }, + hash: { + format: 'Boolean', + default: false, + env: 'N8N_BASIC_AUTH_HASH', + doc: 'If password for basic auth is hashed', }, }, jwtAuth: { @@ -325,49 +337,49 @@ const config = convict({ format: 'Boolean', default: false, env: 'N8N_JWT_AUTH_ACTIVE', - doc: 'If JWT auth should be activated for editor and REST-API' + doc: 'If JWT auth should be activated for editor and REST-API', }, jwtHeader: { format: String, default: '', env: 'N8N_JWT_AUTH_HEADER', - doc: 'The request header containing a signed JWT' + doc: 'The request header containing a signed JWT', }, jwtHeaderValuePrefix: { format: String, default: '', env: 'N8N_JWT_AUTH_HEADER_VALUE_PREFIX', - doc: 'The request header value prefix to strip (optional)' + doc: 'The request header value prefix to strip (optional)', }, jwksUri: { format: String, default: '', env: 'N8N_JWKS_URI', - doc: 'The URI to fetch JWK Set for JWT authentication' + doc: 'The URI to fetch JWK Set for JWT authentication', }, jwtIssuer: { format: String, default: '', env: 'N8N_JWT_ISSUER', - doc: 'JWT issuer to expect (optional)' + doc: 'JWT issuer to expect (optional)', }, jwtNamespace: { format: String, default: '', env: 'N8N_JWT_NAMESPACE', - doc: 'JWT namespace to expect (optional)' + doc: 'JWT namespace to expect (optional)', }, jwtAllowedTenantKey: { format: String, default: '', env: 'N8N_JWT_ALLOWED_TENANT_KEY', - doc: 'JWT tenant key name to inspect within JWT namespace (optional)' + doc: 'JWT tenant key name to inspect within JWT namespace (optional)', }, jwtAllowedTenant: { format: String, default: '', env: 'N8N_JWT_ALLOWED_TENANT', - doc: 'JWT tenant to allow (optional)' + doc: 'JWT tenant to allow (optional)', }, }, }, @@ -377,19 +389,19 @@ const config = convict({ format: String, default: 'rest', env: 'N8N_ENDPOINT_REST', - doc: 'Path for rest endpoint' + doc: 'Path for rest endpoint', }, webhook: { format: String, default: 'webhook', env: 'N8N_ENDPOINT_WEBHOOK', - doc: 'Path for webhook endpoint' + doc: 'Path for webhook endpoint', }, webhookTest: { format: String, default: 'webhook-test', env: 'N8N_ENDPOINT_WEBHOOK_TEST', - doc: 'Path for test-webhook endpoint' + doc: 'Path for test-webhook endpoint', }, }, @@ -397,7 +409,7 @@ const config = convict({ doc: 'Files containing external hooks. Multiple files can be separated by colon (":")', format: String, default: '', - env: 'EXTERNAL_HOOK_FILES' + env: 'EXTERNAL_HOOK_FILES', }, nodes: { @@ -421,13 +433,13 @@ const config = convict({ } }, default: '[]', - env: 'NODES_EXCLUDE' + env: 'NODES_EXCLUDE', }, errorTriggerType: { doc: 'Node Type to use as Error Trigger', format: String, default: 'n8n-nodes-base.errorTrigger', - env: 'NODES_ERROR_TRIGGER_TYPE' + env: 'NODES_ERROR_TRIGGER_TYPE', }, }, diff --git a/packages/cli/package.json b/packages/cli/package.json index 049af1d77..580994fa9 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "0.80.0", + "version": "0.89.2", "description": "n8n Workflow Automation Tool", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", @@ -28,6 +28,7 @@ "start:windows": "cd bin && n8n", "test": "jest", "tslint": "tslint -p tsconfig.json -c tslint.json", + "tslintfix": "tslint --fix -p tsconfig.json -c tslint.json", "watch": "tsc --watch", "typeorm": "ts-node ./node_modules/typeorm/cli.js" }, @@ -54,33 +55,35 @@ "devDependencies": { "@oclif/dev-cli": "^1.22.2", "@types/basic-auth": "^1.1.2", + "@types/bcryptjs": "^2.4.1", "@types/compression": "1.0.1", "@types/connect-history-api-fallback": "^1.3.1", "@types/convict": "^4.2.1", "@types/dotenv": "^8.2.0", "@types/express": "^4.17.6", - "@types/jest": "^25.2.1", + "@types/jest": "^26.0.13", "@types/localtunnel": "^1.9.0", "@types/lodash.get": "^4.4.6", - "@types/node": "^14.0.27", + "@types/node": "14.0.27", "@types/open": "^6.1.0", "@types/parseurl": "^1.3.1", "@types/request-promise-native": "~1.0.15", "concurrently": "^5.1.0", - "jest": "^24.9.0", + "jest": "^26.4.2", "nodemon": "^2.0.2", "p-cancelable": "^2.0.0", "run-script-os": "^1.0.7", - "ts-jest": "^25.4.0", + "ts-jest": "^26.3.0", + "ts-node": "^8.9.1", "tslint": "^6.1.2", - "typescript": "~3.7.4", - "ts-node": "^8.9.1" + "typescript": "~3.9.7" }, "dependencies": { "@oclif/command": "^1.5.18", "@oclif/errors": "^1.2.2", "@types/jsonwebtoken": "^8.3.4", "basic-auth": "^2.0.1", + "bcryptjs": "^2.4.3", "body-parser": "^1.18.3", "body-parser-xml": "^1.1.0", "client-oauth2": "^4.2.5", @@ -95,15 +98,15 @@ "google-timezones-json": "^1.0.2", "inquirer": "^7.0.1", "jsonwebtoken": "^8.5.1", - "jwks-rsa": "^1.6.0", + "jwks-rsa": "~1.9.0", "localtunnel": "^2.0.0", "lodash.get": "^4.4.2", "mongodb": "^3.5.5", - "mysql2": "^2.0.1", - "n8n-core": "~0.44.0", - "n8n-editor-ui": "~0.55.0", - "n8n-nodes-base": "~0.75.0", - "n8n-workflow": "~0.39.0", + "mysql2": "~2.1.0", + "n8n-core": "~0.48.0", + "n8n-editor-ui": "~0.60.0", + "n8n-nodes-base": "~0.85.0", + "n8n-workflow": "~0.42.0", "oauth-1.0a": "^2.2.6", "open": "^7.0.0", "pg": "^8.3.0", diff --git a/packages/cli/src/ActiveExecutions.ts b/packages/cli/src/ActiveExecutions.ts index 8d7c73dd0..e6ed0fa58 100644 --- a/packages/cli/src/ActiveExecutions.ts +++ b/packages/cli/src/ActiveExecutions.ts @@ -7,8 +7,8 @@ import { } from 'n8n-core'; import { - IExecutionsCurrentSummary, IExecutingWorkflowData, + IExecutionsCurrentSummary, IWorkflowExecutionDataProcess, } from '.'; diff --git a/packages/cli/src/ActiveWorkflowRunner.ts b/packages/cli/src/ActiveWorkflowRunner.ts index 0d2e9bad3..6a955969a 100644 --- a/packages/cli/src/ActiveWorkflowRunner.ts +++ b/packages/cli/src/ActiveWorkflowRunner.ts @@ -1,17 +1,17 @@ import { - IActivationError, Db, - NodeTypes, + IActivationError, IResponseCallbackData, + IWebhookDb, IWorkflowDb, IWorkflowExecutionDataProcess, + NodeTypes, ResponseHelper, WebhookHelpers, WorkflowCredentials, + WorkflowExecuteAdditionalData, WorkflowHelpers, WorkflowRunner, - WorkflowExecuteAdditionalData, - IWebhookDb, } from './'; import { @@ -26,8 +26,8 @@ import { INode, INodeExecutionData, IRunExecutionData, - NodeHelpers, IWorkflowExecuteAdditionalData as IWorkflowExecuteAdditionalDataWorkflow, + NodeHelpers, WebhookHttpMethod, Workflow, WorkflowExecuteMode, @@ -181,8 +181,9 @@ export class ActiveWorkflowRunner { * @returns {string[]} * @memberof ActiveWorkflowRunner */ - getActiveWorkflows(): Promise { - return Db.collections.Workflow?.find({ where: { active: true }, select: ['id'] }) as Promise; + async getActiveWorkflows(): Promise { + const activeWorkflows = await Db.collections.Workflow?.find({ where: { active: true }, select: ['id'] }) as IWorkflowDb[]; + return activeWorkflows.filter(workflow => this.activationErrors[workflow.id.toString()] === undefined); } @@ -234,7 +235,7 @@ export class ActiveWorkflowRunner { path = node.parameters.path as string; if (node.parameters.path === undefined) { - path = workflow.getSimpleParameterValue(node, webhookData.webhookDescription['path']) as string | undefined; + path = workflow.expression.getSimpleParameterValue(node, webhookData.webhookDescription['path']) as string | undefined; if (path === undefined) { // TODO: Use a proper logger @@ -243,7 +244,7 @@ export class ActiveWorkflowRunner { } } - const isFullPath: boolean = workflow.getSimpleParameterValue(node, webhookData.webhookDescription['isFullPath'], false) as boolean; + const isFullPath: boolean = workflow.expression.getSimpleParameterValue(node, webhookData.webhookDescription['isFullPath'], false) as boolean; const webhook = { workflowId: webhookData.workflowId, @@ -257,17 +258,20 @@ export class ActiveWorkflowRunner { await Db.collections.Webhook?.insert(webhook); const webhookExists = await workflow.runWebhookMethod('checkExists', webhookData, NodeExecuteFunctions, mode, false); - if (webhookExists === false) { + if (webhookExists !== true) { // If webhook does not exist yet create it await workflow.runWebhookMethod('create', webhookData, NodeExecuteFunctions, mode, false); } } catch (error) { + try { + await this.removeWorkflowWebhooks(workflow.id as string); + } catch (error) { + console.error(`Could not remove webhooks of workflow "${workflow.id}" because of error: "${error.message}"`); + } let errorMessage = ''; - await Db.collections.Webhook?.delete({ workflowId: workflow.id }); - // if it's a workflow from the the insert // TODO check if there is standard error code for deplicate key violation that works // with all databases @@ -317,6 +321,8 @@ export class ActiveWorkflowRunner { await workflow.runWebhookMethod('delete', webhookData, NodeExecuteFunctions, mode, false); } + await WorkflowHelpers.saveStaticData(workflow); + // if it's a mongo objectId convert it to string if (typeof workflowData.id === 'object') { workflowData.id = workflowData.id.toString(); @@ -346,8 +352,8 @@ export class ActiveWorkflowRunner { node, data: { main: data, - } - } + }, + }, ]; const executionData: IRunExecutionData = { @@ -411,7 +417,7 @@ export class ActiveWorkflowRunner { const returnFunctions = NodeExecuteFunctions.getExecuteTriggerFunctions(workflow, node, additionalData, mode); returnFunctions.emit = (data: INodeExecutionData[][]): void => { WorkflowHelpers.saveStaticData(workflow); - this.runWorkflow(workflowData, node, data, additionalData, mode); + this.runWorkflow(workflowData, node, data, additionalData, mode).catch((err) => console.error(err)); }; return returnFunctions; }); @@ -495,7 +501,11 @@ export class ActiveWorkflowRunner { if (this.activeWorkflows !== null) { // Remove all the webhooks of the workflow - await this.removeWorkflowWebhooks(workflowId); + try { + await this.removeWorkflowWebhooks(workflowId); + } catch (error) { + console.error(`Could not remove webhooks of workflow "${workflowId}" because of error: "${error.message}"`); + } if (this.activationErrors[workflowId] !== undefined) { // If there were any activation errors delete them diff --git a/packages/cli/src/CredentialsHelper.ts b/packages/cli/src/CredentialsHelper.ts index 84e368adf..4eae7932c 100644 --- a/packages/cli/src/CredentialsHelper.ts +++ b/packages/cli/src/CredentialsHelper.ts @@ -5,9 +5,14 @@ import { import { ICredentialDataDecryptedObject, ICredentialsHelper, + INode, INodeParameters, INodeProperties, + INodeType, + INodeTypeData, + INodeTypes, NodeHelpers, + Workflow, } from 'n8n-workflow'; import { @@ -18,6 +23,19 @@ import { } from './'; +const mockNodeTypes: INodeTypes = { + nodeTypes: {}, + init: async (nodeTypes?: INodeTypeData): Promise => { }, + getAll: (): INodeType[] => { + // Does not get used in Workflow so no need to return it + return []; + }, + getByName: (nodeType: string): INodeType | undefined => { + return undefined; + }, +}; + + export class CredentialsHelper extends ICredentialsHelper { /** @@ -107,7 +125,7 @@ export class CredentialsHelper extends ICredentialsHelper { const credentialsProperties = this.getCredentialsProperties(type); // Add the default credential values - const decryptedData = NodeHelpers.getNodeParameters(credentialsProperties, decryptedDataOriginal as INodeParameters, true, false) as ICredentialDataDecryptedObject; + let decryptedData = NodeHelpers.getNodeParameters(credentialsProperties, decryptedDataOriginal as INodeParameters, true, false) as ICredentialDataDecryptedObject; if (decryptedDataOriginal.oauthTokenData !== undefined) { // The OAuth data gets removed as it is not defined specifically as a parameter @@ -115,6 +133,18 @@ export class CredentialsHelper extends ICredentialsHelper { decryptedData.oauthTokenData = decryptedDataOriginal.oauthTokenData; } + const mockNode: INode = { + name: '', + typeVersion: 1, + type: 'mock', + position: [0, 0], + parameters: decryptedData as INodeParameters, + }; + + const workflow = new Workflow({ nodes: [mockNode], connections: {}, active: false, nodeTypes: mockNodeTypes}); + // Resolve expressions if any are set + decryptedData = workflow.expression.getComplexParameterValue(mockNode, decryptedData as INodeParameters, undefined) as ICredentialDataDecryptedObject; + // Load and apply the credentials overwrites if any exist const credentialsOverwrites = CredentialsOverwrites(); return credentialsOverwrites.applyOverwrite(type, decryptedData); diff --git a/packages/cli/src/CredentialsOverwrites.ts b/packages/cli/src/CredentialsOverwrites.ts index 40b6419da..5d202ce6b 100644 --- a/packages/cli/src/CredentialsOverwrites.ts +++ b/packages/cli/src/CredentialsOverwrites.ts @@ -3,32 +3,53 @@ import { } from 'n8n-workflow'; import { - ICredentialsOverwrite, + CredentialTypes, GenericHelpers, + ICredentialsOverwrite, } from './'; class CredentialsOverwritesClass { + private credentialTypes = CredentialTypes(); private overwriteData: ICredentialsOverwrite = {}; + private resolvedTypes: string[] = []; + async init(overwriteData?: ICredentialsOverwrite) { if (overwriteData !== undefined) { // If data is already given it can directly be set instead of // loaded from environment - this.overwriteData = overwriteData; + this.__setData(JSON.parse(JSON.stringify(overwriteData))); return; } const data = await GenericHelpers.getConfigValue('credentials.overwrite.data') as string; try { - this.overwriteData = JSON.parse(data); + const overwriteData = JSON.parse(data); + this.__setData(overwriteData); } catch (error) { throw new Error(`The credentials-overwrite is not valid JSON.`); } } + + __setData(overwriteData: ICredentialsOverwrite) { + this.overwriteData = overwriteData; + + for (const credentialTypeData of this.credentialTypes.getAll()) { + const type = credentialTypeData.name; + + const overwrites = this.__getExtended(type); + + if (overwrites && Object.keys(overwrites).length) { + this.overwriteData[type] = overwrites; + } + } + } + + applyOverwrite(type: string, data: ICredentialDataDecryptedObject) { const overwrites = this.get(type); @@ -48,10 +69,45 @@ class CredentialsOverwritesClass { return returnData; } + + __getExtended(type: string): ICredentialDataDecryptedObject | undefined { + + if (this.resolvedTypes.includes(type)) { + // Type got already resolved and can so returned directly + return this.overwriteData[type]; + } + + const credentialTypeData = this.credentialTypes.getByName(type); + + if (credentialTypeData === undefined) { + throw new Error(`The credentials of type "${type}" are not known.`); + } + + if (credentialTypeData.extends === undefined) { + this.resolvedTypes.push(type); + return this.overwriteData[type]; + } + + const overwrites: ICredentialDataDecryptedObject = {}; + for (const credentialsTypeName of credentialTypeData.extends) { + Object.assign(overwrites, this.__getExtended(credentialsTypeName)); + } + + if (this.overwriteData[type] !== undefined) { + Object.assign(overwrites, this.overwriteData[type]); + } + + this.resolvedTypes.push(type); + + return overwrites; + } + + get(type: string): ICredentialDataDecryptedObject | undefined { return this.overwriteData[type]; } + getAll(): ICredentialsOverwrite { return this.overwriteData; } diff --git a/packages/cli/src/Db.ts b/packages/cli/src/Db.ts index 06eeca797..f4522e52e 100644 --- a/packages/cli/src/Db.ts +++ b/packages/cli/src/Db.ts @@ -33,27 +33,27 @@ export let collections: IDatabaseCollections = { }; import { + CreateIndexStoppedAt1594828256133, InitialMigration1587669153312, WebhookModel1589476000887, - CreateIndexStoppedAt1594828256133, } from './databases/postgresdb/migrations'; import { + CreateIndexStoppedAt1594910478695, InitialMigration1587563438936, WebhookModel1592679094242, - CreateIndexStoppedAt1594910478695, } from './databases/mongodb/migrations'; import { + CreateIndexStoppedAt1594902918301, InitialMigration1588157391238, WebhookModel1592447867632, - CreateIndexStoppedAt1594902918301, } from './databases/mysqldb/migrations'; import { + CreateIndexStoppedAt1594825041918, InitialMigration1588102412422, WebhookModel1592445003908, - CreateIndexStoppedAt1594825041918, } from './databases/sqlite/migrations'; import * as path from 'path'; @@ -154,7 +154,7 @@ export async function init(): Promise { migrations: [ InitialMigration1588102412422, WebhookModel1592445003908, - CreateIndexStoppedAt1594825041918 + CreateIndexStoppedAt1594825041918, ], migrationsRun: true, migrationsTableName: `${entityPrefix}migrations`, diff --git a/packages/cli/src/ExternalHooks.ts b/packages/cli/src/ExternalHooks.ts index 355415158..acdae264a 100644 --- a/packages/cli/src/ExternalHooks.ts +++ b/packages/cli/src/ExternalHooks.ts @@ -1,7 +1,7 @@ import { Db, - IExternalHooksFunctions, IExternalHooksClass, + IExternalHooksFunctions, } from './'; import * as config from '../config'; @@ -64,6 +64,10 @@ class ExternalHooksClass implements IExternalHooksClass { } } + exists(hookName: string): boolean { + return !!this.externalHooks[hookName]; + } + } diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index bef98af6f..b808a7356 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -288,6 +288,10 @@ export interface IN8nUISettings { saveManualExecutions: boolean; executionTimeout: number; maxExecutionTimeout: number; + oauthCallbackUrls: { + oauth1: string; + oauth2: string; + }; timezone: string; urlBaseWebhook: string; versionCli: string; diff --git a/packages/cli/src/NodeTypes.ts b/packages/cli/src/NodeTypes.ts index 5f37a3376..0901ab7ed 100644 --- a/packages/cli/src/NodeTypes.ts +++ b/packages/cli/src/NodeTypes.ts @@ -1,7 +1,7 @@ import { INodeType, - INodeTypes, INodeTypeData, + INodeTypes, NodeHelpers, } from 'n8n-workflow'; diff --git a/packages/cli/src/ResponseHelper.ts b/packages/cli/src/ResponseHelper.ts index c71647036..d09011eac 100644 --- a/packages/cli/src/ResponseHelper.ts +++ b/packages/cli/src/ResponseHelper.ts @@ -67,7 +67,7 @@ export function sendSuccessResponse(res: Response, data: any, raw?: boolean, res res.json(data); } else { res.json({ - data + data, }); } } @@ -183,7 +183,7 @@ export function unflattenExecutionData(fullExecutionData: IExecutionFlattedDb): mode: fullExecutionData.mode, startedAt: fullExecutionData.startedAt, stoppedAt: fullExecutionData.stoppedAt, - finished: fullExecutionData.finished ? fullExecutionData.finished : false + finished: fullExecutionData.finished ? fullExecutionData.finished : false, }); return returnData; diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 43886323a..df7dbdaf8 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -20,20 +20,24 @@ import { RequestOptions } from 'oauth-1.0a'; import * as csrf from 'csrf'; import * as requestPromise from 'request-promise-native'; import { createHmac } from 'crypto'; +import { compare } from 'bcryptjs'; import { ActiveExecutions, ActiveWorkflowRunner, CredentialsHelper, + CredentialsOverwrites, CredentialTypes, Db, ExternalHooks, + GenericHelpers, IActivationError, - ICustomRequest, ICredentialsDb, ICredentialsDecryptedDb, ICredentialsDecryptedResponse, + ICredentialsOverwrite, ICredentialsResponse, + ICustomRequest, IExecutionDeleteFilter, IExecutionFlatted, IExecutionFlattedDb, @@ -46,21 +50,18 @@ import { IN8nUISettings, IPackageVersions, IWorkflowBase, - IWorkflowShortResponse, - IWorkflowResponse, IWorkflowExecutionDataProcess, + IWorkflowResponse, + IWorkflowShortResponse, + LoadNodesAndCredentials, NodeTypes, Push, ResponseHelper, TestWebhooks, - WorkflowCredentials, WebhookHelpers, + WorkflowCredentials, WorkflowExecuteAdditionalData, WorkflowRunner, - GenericHelpers, - CredentialsOverwrites, - ICredentialsOverwrite, - LoadNodesAndCredentials, } from './'; import { @@ -74,9 +75,9 @@ import { ICredentialType, IDataObject, INodeCredentials, - INodeTypeDescription, INodeParameters, INodePropertyOptions, + INodeTypeDescription, IRunData, IWorkflowCredentials, Workflow, @@ -120,7 +121,7 @@ class App { push: Push.Push; versions: IPackageVersions | undefined; restEndpoint: string; - + frontendSettings: IN8nUISettings; protocol: string; sslKey: string; sslCert: string; @@ -154,6 +155,25 @@ class App { this.presetCredentialsLoaded = false; this.endpointPresetCredentials = config.get('credentials.overwrite.endpoint') as string; + + const urlBaseWebhook = WebhookHelpers.getWebhookBaseUrl(); + + this.frontendSettings = { + endpointWebhook: this.endpointWebhook, + endpointWebhookTest: this.endpointWebhookTest, + saveDataErrorExecution: this.saveDataErrorExecution, + saveDataSuccessExecution: this.saveDataSuccessExecution, + saveManualExecutions: this.saveManualExecutions, + executionTimeout: this.executionTimeout, + maxExecutionTimeout: this.maxExecutionTimeout, + timezone: this.timezone, + urlBaseWebhook, + versionCli: '', + oauthCallbackUrls: { + 'oauth1': urlBaseWebhook + `${this.restEndpoint}/oauth1-credential/callback`, + 'oauth2': urlBaseWebhook + `${this.restEndpoint}/oauth2-credential/callback`, + }, + }; } @@ -171,7 +191,16 @@ class App { async config(): Promise { this.versions = await GenericHelpers.getVersions(); - const authIgnoreRegex = new RegExp(`^\/(healthz|${this.endpointWebhook}|${this.endpointWebhookTest})\/?.*$`); + this.frontendSettings.versionCli = this.versions.cli; + + await this.externalHooks.run('frontend.settings', [this.frontendSettings]); + + const excludeEndpoints = config.get('security.excludeEndpoints') as string; + + const ignoredEndpoints = ['healthz', this.endpointWebhook, this.endpointWebhookTest, this.endpointPresetCredentials]; + ignoredEndpoints.push.apply(ignoredEndpoints, excludeEndpoints.split(':')); + + const authIgnoreRegex = new RegExp(`^\/(${_(ignoredEndpoints).compact().join('|')})\/?.*$`); // Check for basic auth credentials if activated const basicAuthActive = config.get('security.basicAuth.active') as boolean; @@ -186,7 +215,11 @@ class App { throw new Error('Basic auth is activated but no password got defined. Please set one!'); } - this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { + const basicAuthHashEnabled = await GenericHelpers.getConfigValue('security.basicAuth.hash') as boolean; + + let validPassword: null | string = null; + + this.app.use(async (req: express.Request, res: express.Response, next: express.NextFunction) => { if (req.url.match(authIgnoreRegex)) { return next(); } @@ -198,12 +231,27 @@ class App { return ResponseHelper.basicAuthAuthorizationError(res, realm, 'Authorization is required!'); } - if (basicAuthData.name !== basicAuthUser || basicAuthData.pass !== basicAuthPassword) { - // Provided authentication data is wrong - return ResponseHelper.basicAuthAuthorizationError(res, realm, 'Authorization data is wrong!'); + if (basicAuthData.name === basicAuthUser) { + if (basicAuthHashEnabled === true) { + if (validPassword === null && await compare(basicAuthData.pass, basicAuthPassword)) { + // Password is valid so save for future requests + validPassword = basicAuthData.pass; + } + + if (validPassword === basicAuthData.pass && validPassword !== null) { + // Provided hash is correct + return next(); + } + } else { + if (basicAuthData.pass === basicAuthPassword) { + // Provided password is correct + return next(); + } + } } - next(); + // Provided authentication data is wrong + return ResponseHelper.basicAuthAuthorizationError(res, realm, 'Authorization data is wrong!'); }); } @@ -265,7 +313,7 @@ class App { const jwtVerifyOptions: jwt.VerifyOptions = { issuer: jwtIssuer !== '' ? jwtIssuer : undefined, - ignoreExpiration: false + ignoreExpiration: false, }; jwt.verify(token, getKey, jwtVerifyOptions, (err: jwt.VerifyErrors, decoded: object) => { @@ -307,7 +355,7 @@ class App { limit: '16mb', verify: (req, res, buf) => { // @ts-ignore req.rawBody = buf; - } + }, })); // Support application/xml type post data @@ -317,14 +365,14 @@ class App { normalize: true, // Trim whitespace inside text nodes normalizeTags: true, // Transform tags to lowercase explicitArray: false, // Only put properties in array if length > 1 - } + }, })); this.app.use(bodyParser.text({ limit: '16mb', verify: (req, res, buf) => { // @ts-ignore req.rawBody = buf; - } + }, })); // Make sure that Vue history mode works properly @@ -334,9 +382,9 @@ class App { from: new RegExp(`^\/(${this.restEndpoint}|healthz|css|js|${this.endpointWebhook}|${this.endpointWebhookTest})\/?.*$`), to: (context) => { return context.parsedUrl!.pathname!.toString(); - } - } - ] + }, + }, + ], })); //support application/x-www-form-urlencoded post data @@ -344,7 +392,7 @@ class App { verify: (req, res, buf) => { // @ts-ignore req.rawBody = buf; - } + }, })); if (process.env['NODE_ENV'] !== 'production') { @@ -532,6 +580,7 @@ class App { newWorkflowData.updatedAt = this.getCurrentDate(); await Db.collections.Workflow!.update(id, newWorkflowData); + await this.externalHooks.run('workflow.afterUpdate', [newWorkflowData]); // We sadly get nothing back from "update". Neither if it updated a record // nor the new value. So query now the hopefully updated entry. @@ -580,6 +629,7 @@ class App { } await Db.collections.Workflow!.delete(id); + await this.externalHooks.run('workflow.afterDelete', [id]); return true; })); @@ -665,13 +715,36 @@ class App { const allNodes = nodeTypes.getAll(); allNodes.forEach((nodeData) => { - returnData.push(nodeData.description); + // Make a copy of the object. If we don't do this, then when + // The method below is called the properties are removed for good + // This happens because nodes are returned as reference. + const nodeInfo: INodeTypeDescription = {...nodeData.description}; + if (req.query.includeProperties !== 'true') { + // @ts-ignore + delete nodeInfo.properties; + } + returnData.push(nodeInfo); }); return returnData; })); + // Returns node information baesd on namese + this.app.post(`/${this.restEndpoint}/node-types`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + const nodeNames = _.get(req, 'body.nodeNames', []) as string[]; + const nodeTypes = NodeTypes(); + + return nodeNames.map(name => { + try { + return nodeTypes.getByName(name); + } catch (e) { + return undefined; + } + }).filter(nodeData => !!nodeData).map(nodeData => nodeData!.description); + })); + + // ---------------------------------------- // Node-Types @@ -1009,7 +1082,7 @@ class App { const signatureMethod = _.get(oauthCredentials, 'signatureMethod') as string; - const oauth = new clientOAuth1({ + const oAuthOptions: clientOAuth1.Options = { consumer: { key: _.get(oauthCredentials, 'consumerKey') as string, secret: _.get(oauthCredentials, 'consumerSecret') as string, @@ -1021,16 +1094,20 @@ class App { .update(base) .digest('base64'); }, - }); + }; - const callback = `${WebhookHelpers.getWebhookBaseUrl()}${this.restEndpoint}/oauth1-credential/callback?cid=${req.query.id}`; + const oauthRequestData = { + oauth_callback: `${WebhookHelpers.getWebhookBaseUrl()}${this.restEndpoint}/oauth1-credential/callback?cid=${req.query.id}`, + }; + + await this.externalHooks.run('oauth1.authenticate', [oAuthOptions, oauthRequestData]); + + const oauth = new clientOAuth1(oAuthOptions); const options: RequestOptions = { method: 'POST', url: (_.get(oauthCredentials, 'requestTokenUrl') as string), - data: { - oauth_callback: callback, - }, + data: oauthRequestData, }; const data = oauth.toHeader(oauth.authorize(options as RequestOptions)); @@ -1099,7 +1176,7 @@ class App { qs: { oauth_token, oauth_verifier, - } + }, }; let oauthToken; @@ -1169,11 +1246,11 @@ class App { const csrfSecret = token.secretSync(); const state = { token: token.create(csrfSecret), - cid: req.query.id + cid: req.query.id, }; const stateEncodedStr = Buffer.from(JSON.stringify(state)).toString('base64') as string; - const oAuthObj = new clientOAuth2({ + const oAuthOptions: clientOAuth2.Options = { clientId: _.get(oauthCredentials, 'clientId') as string, clientSecret: _.get(oauthCredentials, 'clientSecret', '') as string, accessTokenUri: _.get(oauthCredentials, 'accessTokenUrl', '') as string, @@ -1181,7 +1258,11 @@ class App { redirectUri: `${WebhookHelpers.getWebhookBaseUrl()}${this.restEndpoint}/oauth2-credential/callback`, scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ','), state: stateEncodedStr, - }); + }; + + await this.externalHooks.run('oauth2.authenticate', [oAuthOptions]); + + const oAuthObj = new clientOAuth2(oAuthOptions); // Encrypt the data const credentials = new Credentials(result.name, result.type, result.nodesAccess); @@ -1267,11 +1348,11 @@ class App { const oAuth2Parameters = { clientId: _.get(oauthCredentials, 'clientId') as string, - clientSecret: _.get(oauthCredentials, 'clientSecret', '') as string, + clientSecret: _.get(oauthCredentials, 'clientSecret', '') as string | undefined, accessTokenUri: _.get(oauthCredentials, 'accessTokenUrl', '') as string, authorizationUri: _.get(oauthCredentials, 'authUrl', '') as string, redirectUri: `${WebhookHelpers.getWebhookBaseUrl()}${this.restEndpoint}/oauth2-credential/callback`, - scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ',') + scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ','), }; if (_.get(oauthCredentials, 'authentication', 'header') as string === 'body') { @@ -1283,13 +1364,14 @@ class App { }; delete oAuth2Parameters.clientSecret; } - const redirectUri = `${WebhookHelpers.getWebhookBaseUrl()}${this.restEndpoint}/oauth2-credential/callback`; + + await this.externalHooks.run('oauth2.callback', [oAuth2Parameters]); const oAuthObj = new clientOAuth2(oAuth2Parameters); const queryParameters = req.originalUrl.split('?').splice(1, 1).join(''); - const oauthToken = await oAuthObj.code.getToken(`${redirectUri}?${queryParameters}`, options); + const oauthToken = await oAuthObj.code.getToken(`${oAuth2Parameters.redirectUri}?${queryParameters}`, options); if (oauthToken === undefined) { const errorResponse = new ResponseHelper.ResponseError('Unable to get access tokens!', undefined, 404); @@ -1582,18 +1664,7 @@ class App { // Returns the settings which are needed in the UI this.app.get(`/${this.restEndpoint}/settings`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - return { - endpointWebhook: this.endpointWebhook, - endpointWebhookTest: this.endpointWebhookTest, - saveDataErrorExecution: this.saveDataErrorExecution, - saveDataSuccessExecution: this.saveDataSuccessExecution, - saveManualExecutions: this.saveManualExecutions, - executionTimeout: this.executionTimeout, - maxExecutionTimeout: this.maxExecutionTimeout, - timezone: this.timezone, - urlBaseWebhook: WebhookHelpers.getWebhookBaseUrl(), - versionCli: this.versions!.cli, - }; + return this.frontendSettings; })); @@ -1829,7 +1900,7 @@ class App { // got used res.setHeader('Last-Modified', startTime); } - } + }, })); } @@ -1860,5 +1931,7 @@ export async function start(): Promise { const versions = await GenericHelpers.getVersions(); console.log(`n8n ready on ${ADDRESS}, port ${PORT}`); console.log(`Version: ${versions.cli}`); + + await app.externalHooks.run('n8n.ready', [app]); }); } diff --git a/packages/cli/src/TestWebhooks.ts b/packages/cli/src/TestWebhooks.ts index 129425854..5d81b8dd1 100644 --- a/packages/cli/src/TestWebhooks.ts +++ b/packages/cli/src/TestWebhooks.ts @@ -3,11 +3,9 @@ import * as express from 'express'; import { IResponseCallbackData, IWorkflowDb, - NodeTypes, Push, ResponseHelper, WebhookHelpers, - WorkflowHelpers, } from './'; import { @@ -31,6 +29,7 @@ export class TestWebhooks { sessionId?: string; timeout: NodeJS.Timeout, workflowData: IWorkflowDb; + workflow: Workflow; }; } = {}; private activeWebhooks: ActiveWebhooks | null = null; @@ -64,10 +63,13 @@ export class TestWebhooks { const webhookKey = this.activeWebhooks!.getWebhookKey(webhookData.httpMethod, webhookData.path); - const workflowData = this.testWebhookData[webhookKey].workflowData; + // TODO: Clean that duplication up one day and improve code generally + if (this.testWebhookData[webhookKey] === undefined) { + // The requested webhook is not registered + throw new ResponseHelper.ResponseError(`The requested webhook "${httpMethod} ${path}" is not registered.`, 404, 404); + } - const nodeTypes = NodeTypes(); - const workflow = new Workflow({ id: webhookData.workflowId, name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, nodeTypes, staticData: workflowData.staticData, settings: workflowData.settings}); + const workflow = this.testWebhookData[webhookKey].workflow; // Get the node which has the webhook defined to know where to start from and to // get additional data @@ -154,19 +156,26 @@ export class TestWebhooks { }, 120000); let key: string; + const activatedKey: string[] = []; for (const webhookData of webhooks) { key = this.activeWebhooks!.getWebhookKey(webhookData.httpMethod, webhookData.path); - await this.activeWebhooks!.add(workflow, webhookData, mode); + activatedKey.push(key); this.testWebhookData[key] = { sessionId, timeout, + workflow, workflowData, }; - // Save static data! - this.testWebhookData[key].workflowData.staticData = workflow.staticData; + try { + await this.activeWebhooks!.add(workflow, webhookData, mode); + } catch (error) { + activatedKey.forEach(deleteKey => delete this.testWebhookData[deleteKey] ); + await this.activeWebhooks!.removeWorkflow(workflow); + throw error; + } } return true; @@ -181,8 +190,6 @@ export class TestWebhooks { * @memberof TestWebhooks */ cancelTestWebhook(workflowId: string): boolean { - const nodeTypes = NodeTypes(); - let foundWebhook = false; for (const webhookKey of Object.keys(this.testWebhookData)) { const webhookData = this.testWebhookData[webhookKey]; @@ -191,8 +198,6 @@ export class TestWebhooks { continue; } - foundWebhook = true; - clearTimeout(this.testWebhookData[webhookKey].timeout); // Inform editor-ui that webhook got received @@ -205,12 +210,17 @@ export class TestWebhooks { } } - const workflowData = webhookData.workflowData; - const workflow = new Workflow({ id: workflowData.id.toString(), name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, nodeTypes, staticData: workflowData.staticData, settings: workflowData.settings }); + const workflow = this.testWebhookData[webhookKey].workflow; // Remove the webhook delete this.testWebhookData[webhookKey]; - this.activeWebhooks!.removeWorkflow(workflow); + + if (foundWebhook === false) { + // As it removes all webhooks of the workflow execute only once + this.activeWebhooks!.removeWorkflow(workflow); + } + + foundWebhook = true; } return foundWebhook; @@ -225,14 +235,10 @@ export class TestWebhooks { return; } - const nodeTypes = NodeTypes(); - - let workflowData: IWorkflowDb; let workflow: Workflow; const workflows: Workflow[] = []; for (const webhookKey of Object.keys(this.testWebhookData)) { - workflowData = this.testWebhookData[webhookKey].workflowData; - workflow = new Workflow({ id: workflowData.id.toString(), name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, nodeTypes, staticData: workflowData.staticData, settings: workflowData.settings }); + workflow = this.testWebhookData[webhookKey].workflow; workflows.push(workflow); } diff --git a/packages/cli/src/WebhookHelpers.ts b/packages/cli/src/WebhookHelpers.ts index ffb544989..114111a82 100644 --- a/packages/cli/src/WebhookHelpers.ts +++ b/packages/cli/src/WebhookHelpers.ts @@ -3,16 +3,17 @@ import { get } from 'lodash'; import { ActiveExecutions, + ExternalHooks, GenericHelpers, IExecutionDb, IResponseCallbackData, IWorkflowDb, IWorkflowExecutionDataProcess, ResponseHelper, - WorkflowHelpers, - WorkflowRunner, WorkflowCredentials, WorkflowExecuteAdditionalData, + WorkflowHelpers, + WorkflowRunner, } from './'; import { @@ -114,8 +115,8 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] { } // Get the responseMode - const responseMode = workflow.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseMode'], 'onReceived'); - const responseCode = workflow.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseCode'], 200) as number; + const responseMode = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseMode'], 'onReceived'); + const responseCode = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseCode'], 200) as number; if (!['onReceived', 'lastNode'].includes(responseMode as string)) { // If the mode is not known we error. Is probably best like that instead of using @@ -173,7 +174,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] { await WorkflowHelpers.saveStaticData(workflow); if (webhookData.webhookDescription['responseHeaders'] !== undefined) { - const responseHeaders = workflow.getComplexParameterValue(workflowStartNode, webhookData.webhookDescription['responseHeaders'], undefined) as { + const responseHeaders = workflow.expression.getComplexParameterValue(workflowStartNode, webhookData.webhookDescription['responseHeaders'], undefined) as { entries?: Array<{ name: string; value: string; @@ -251,7 +252,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] { data: { main: webhookResultData.workflowData, }, - }, + } ); const runExecutionData: IRunExecutionData = { @@ -325,7 +326,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] { return data; } - const responseData = workflow.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseData'], 'firstEntryJson'); + const responseData = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseData'], 'firstEntryJson'); if (didSendResponse === false) { let data: IDataObject | IDataObject[]; @@ -340,13 +341,13 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] { data = returnData.data!.main[0]![0].json; - const responsePropertyName = workflow.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responsePropertyName'], undefined); + const responsePropertyName = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responsePropertyName'], undefined); if (responsePropertyName !== undefined) { data = get(data, responsePropertyName as string) as IDataObject; } - const responseContentType = workflow.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseContentType'], undefined); + const responseContentType = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseContentType'], undefined); if (responseContentType !== undefined) { // Send the webhook response manually to be able to set the content-type @@ -379,7 +380,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] { didSendResponse = true; } - const responseBinaryPropertyName = workflow.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseBinaryPropertyName'], 'data'); + const responseBinaryPropertyName = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseBinaryPropertyName'], 'data'); if (responseBinaryPropertyName === undefined && didSendResponse === false) { responseCallback(new Error('No "responseBinaryPropertyName" is set.'), {}); diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index 4230e79f5..11c15aee8 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -25,8 +25,8 @@ import { IExecuteData, IExecuteWorkflowInfo, INode, - INodeParameters, INodeExecutionData, + INodeParameters, IRun, IRunExecutionData, ITaskData, @@ -74,7 +74,7 @@ function executeErrorWorkflow(workflowData: IWorkflowBase, fullRunData: IRun, mo workflow: { id: workflowData.id !== undefined ? workflowData.id.toString() as string : undefined, name: workflowData.name, - } + }, }; // Run the error workflow WorkflowHelpers.executeErrorWorkflow(workflowData.settings.errorWorkflow as string, workflowErrorData); @@ -191,13 +191,13 @@ function hookFunctionsPush(): IWorkflowExecuteHooks { workflowId: this.workflowData.id as string, workflowName: this.workflowData.name, }); - } + }, ], workflowExecuteAfter: [ async function (this: WorkflowHooks, fullRunData: IRun, newStaticData: IDataObject): Promise { pushExecutionFinished(this.mode, fullRunData, this.executionId, undefined, this.retryOf); }, - ] + ], }; } @@ -298,7 +298,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks { } } }, - ] + ], }; } @@ -374,8 +374,8 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi // Always start with empty data if no inputData got supplied inputData = inputData || [ { - json: {} - } + json: {}, + }, ]; // Initialize the incoming data @@ -386,7 +386,7 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi data: { main: [inputData], }, - }, + } ); const runExecutionData: IRunExecutionData = { @@ -406,6 +406,8 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi const workflowExecute = new WorkflowExecute(additionalDataIntegrated, mode, runExecutionData); const data = await workflowExecute.processRunExecutionData(workflow); + await externalHooks.run('workflow.postExecute', [data, workflowData]); + if (data.finished === true) { // Workflow did finish successfully const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data); diff --git a/packages/cli/src/WorkflowHelpers.ts b/packages/cli/src/WorkflowHelpers.ts index 0f824398f..7071c1b71 100644 --- a/packages/cli/src/WorkflowHelpers.ts +++ b/packages/cli/src/WorkflowHelpers.ts @@ -3,8 +3,8 @@ import { Db, ICredentialsTypeData, ITransferNodeTypes, - IWorkflowExecutionDataProcess, IWorkflowErrorData, + IWorkflowExecutionDataProcess, NodeTypes, WorkflowCredentials, WorkflowRunner, @@ -120,12 +120,12 @@ export async function executeErrorWorkflow(workflowId: string, workflowErrorData main: [ [ { - json: workflowErrorData - } - ] + json: workflowErrorData, + }, + ], ], }, - }, + } ); const runExecutionData: IRunExecutionData = { diff --git a/packages/cli/src/WorkflowRunner.ts b/packages/cli/src/WorkflowRunner.ts index 0eb4f7a01..3a5e197f1 100644 --- a/packages/cli/src/WorkflowRunner.ts +++ b/packages/cli/src/WorkflowRunner.ts @@ -24,8 +24,8 @@ import { IExecutionError, IRun, Workflow, - WorkflowHooks, WorkflowExecuteMode, + WorkflowHooks, } from 'n8n-workflow'; import * as config from '../config'; @@ -104,11 +104,25 @@ export class WorkflowRunner { await externalHooks.run('workflow.execute', [data.workflowData, data.executionMode]); const executionsProcess = config.get('executions.process') as string; + + let executionId: string; if (executionsProcess === 'main') { - return this.runMainProcess(data, loadStaticData); + executionId = await this.runMainProcess(data, loadStaticData); + } else { + executionId = await this.runSubprocess(data, loadStaticData); } - return this.runSubprocess(data, loadStaticData); + if (externalHooks.exists('workflow.postExecute')) { + this.activeExecutions.getPostExecutePromise(executionId) + .then(async (executionData) => { + await externalHooks.run('workflow.postExecute', [executionData, data.workflowData]); + }) + .catch(error => { + console.error('There was a problem running hook "workflow.postExecute"', error); + }); + } + + return executionId; } @@ -212,6 +226,7 @@ export class WorkflowRunner { let nodeTypeData: ITransferNodeTypes; let credentialTypeData: ICredentialsTypeData; + let credentialsOverwrites = this.credentialsOverwrites; if (loadAllNodeTypes === true) { // Supply all nodeTypes and credentialTypes @@ -219,15 +234,22 @@ export class WorkflowRunner { const credentialTypes = CredentialTypes(); credentialTypeData = credentialTypes.credentialTypes; } else { - // Supply only nodeTypes and credentialTypes which the workflow needs + // Supply only nodeTypes, credentialTypes and overwrites that the workflow needs nodeTypeData = WorkflowHelpers.getNodeTypeData(data.workflowData.nodes); credentialTypeData = WorkflowHelpers.getCredentialsData(data.credentials); + + credentialsOverwrites = {}; + for (const credentialName of Object.keys(credentialTypeData)) { + if (this.credentialsOverwrites[credentialName] !== undefined) { + credentialsOverwrites[credentialName] = this.credentialsOverwrites[credentialName]; + } + } } (data as unknown as IWorkflowExecutionDataProcessWithExecution).executionId = executionId; (data as unknown as IWorkflowExecutionDataProcessWithExecution).nodeTypeData = nodeTypeData; - (data as unknown as IWorkflowExecutionDataProcessWithExecution).credentialsOverwrite = this.credentialsOverwrites; + (data as unknown as IWorkflowExecutionDataProcessWithExecution).credentialsOverwrite = credentialsOverwrites; (data as unknown as IWorkflowExecutionDataProcessWithExecution).credentialsTypeData = credentialTypeData; // TODO: Still needs correct value const workflowHooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId); diff --git a/packages/cli/src/WorkflowRunnerProcess.ts b/packages/cli/src/WorkflowRunnerProcess.ts index 41f467a8f..894e11029 100644 --- a/packages/cli/src/WorkflowRunnerProcess.ts +++ b/packages/cli/src/WorkflowRunnerProcess.ts @@ -66,7 +66,7 @@ export class WorkflowRunnerProcess { // Load the credentials overwrites if any exist const credentialsOverwrites = CredentialsOverwrites(); - await credentialsOverwrites.init(); + await credentialsOverwrites.init(inputData.credentialsOverwrite); this.workflow = new Workflow({ id: this.data.workflowData.id as string | undefined, name: this.data.workflowData.name, nodes: this.data.workflowData!.nodes, connections: this.data.workflowData!.connections, active: this.data.workflowData!.active, nodeTypes, staticData: this.data.workflowData!.staticData, settings: this.data.workflowData!.settings}); const additionalData = await WorkflowExecuteAdditionalData.getBase(this.data.credentials); @@ -135,13 +135,13 @@ export class WorkflowRunnerProcess { workflowExecuteBefore: [ async (): Promise => { this.sendHookToParentProcess('workflowExecuteBefore', []); - } + }, ], workflowExecuteAfter: [ async (fullRunData: IRun, newStaticData?: IDataObject): Promise => { this.sendHookToParentProcess('workflowExecuteAfter', [fullRunData, newStaticData]); }, - ] + ], }; return new WorkflowHooks(hookFunctions, this.data!.executionMode, this.data!.executionId, this.data!.workflowData, { sessionId: this.data!.sessionId, retryOf: this.data!.retryOf as string }); diff --git a/packages/cli/src/databases/mysqldb/CredentialsEntity.ts b/packages/cli/src/databases/mysqldb/CredentialsEntity.ts index 5654581ff..037835fcf 100644 --- a/packages/cli/src/databases/mysqldb/CredentialsEntity.ts +++ b/packages/cli/src/databases/mysqldb/CredentialsEntity.ts @@ -20,7 +20,7 @@ export class CredentialsEntity implements ICredentialsDb { id: number; @Column({ - length: 128 + length: 128, }) name: string; @@ -29,7 +29,7 @@ export class CredentialsEntity implements ICredentialsDb { @Index() @Column({ - length: 32 + length: 32, }) type: string; diff --git a/packages/cli/src/databases/mysqldb/WorkflowEntity.ts b/packages/cli/src/databases/mysqldb/WorkflowEntity.ts index 4cca4e62a..ea96195ca 100644 --- a/packages/cli/src/databases/mysqldb/WorkflowEntity.ts +++ b/packages/cli/src/databases/mysqldb/WorkflowEntity.ts @@ -22,7 +22,7 @@ export class WorkflowEntity implements IWorkflowDb { id: number; @Column({ - length: 128 + length: 128, }) name: string; diff --git a/packages/cli/src/databases/postgresdb/CredentialsEntity.ts b/packages/cli/src/databases/postgresdb/CredentialsEntity.ts index cddaf7559..d2a3f7871 100644 --- a/packages/cli/src/databases/postgresdb/CredentialsEntity.ts +++ b/packages/cli/src/databases/postgresdb/CredentialsEntity.ts @@ -20,7 +20,7 @@ export class CredentialsEntity implements ICredentialsDb { id: number; @Column({ - length: 128 + length: 128, }) name: string; @@ -29,7 +29,7 @@ export class CredentialsEntity implements ICredentialsDb { @Index() @Column({ - length: 32 + length: 32, }) type: string; diff --git a/packages/cli/src/databases/postgresdb/WorkflowEntity.ts b/packages/cli/src/databases/postgresdb/WorkflowEntity.ts index 3f870b929..d6d097ef8 100644 --- a/packages/cli/src/databases/postgresdb/WorkflowEntity.ts +++ b/packages/cli/src/databases/postgresdb/WorkflowEntity.ts @@ -22,7 +22,7 @@ export class WorkflowEntity implements IWorkflowDb { id: number; @Column({ - length: 128 + length: 128, }) name: string; diff --git a/packages/cli/src/databases/sqlite/CredentialsEntity.ts b/packages/cli/src/databases/sqlite/CredentialsEntity.ts index 6974b6b05..8b49d779d 100644 --- a/packages/cli/src/databases/sqlite/CredentialsEntity.ts +++ b/packages/cli/src/databases/sqlite/CredentialsEntity.ts @@ -20,7 +20,7 @@ export class CredentialsEntity implements ICredentialsDb { id: number; @Column({ - length: 128 + length: 128, }) name: string; @@ -29,7 +29,7 @@ export class CredentialsEntity implements ICredentialsDb { @Index() @Column({ - length: 32 + length: 32, }) type: string; diff --git a/packages/cli/src/databases/sqlite/WorkflowEntity.ts b/packages/cli/src/databases/sqlite/WorkflowEntity.ts index 486dcbbb5..933a146ca 100644 --- a/packages/cli/src/databases/sqlite/WorkflowEntity.ts +++ b/packages/cli/src/databases/sqlite/WorkflowEntity.ts @@ -22,7 +22,7 @@ export class WorkflowEntity implements IWorkflowDb { id: number; @Column({ - length: 128 + length: 128, }) name: string; diff --git a/packages/cli/templates/oauth-callback.html b/packages/cli/templates/oauth-callback.html index e479c5ea9..5f7b736d9 100644 --- a/packages/cli/templates/oauth-callback.html +++ b/packages/cli/templates/oauth-callback.html @@ -1,9 +1,9 @@ - + -Got connected. The window can be closed now. + Got connected. The window can be closed now. diff --git a/packages/cli/tslint.json b/packages/cli/tslint.json index 7eb9d0110..9b5a55973 100644 --- a/packages/cli/tslint.json +++ b/packages/cli/tslint.json @@ -46,6 +46,11 @@ "forin": true, "jsdoc-format": true, "label-position": true, + "indent": [ + true, + "tabs", + 2 + ], "member-access": [ true, "no-public" @@ -60,6 +65,13 @@ "no-default-export": true, "no-duplicate-variable": true, "no-inferrable-types": true, + "ordered-imports": [ + true, + { + "import-sources-order": "any", + "named-imports-order": "case-insensitive" + } + ], "no-namespace": [ true, "allow-declarations" @@ -82,6 +94,18 @@ "ignore-bound-class-methods" ], "switch-default": true, + "trailing-comma": [ + true, + { + "multiline": { + "objects": "always", + "arrays": "always", + "functions": "never", + "typeLiterals": "ignore" + }, + "esSpecCompliant": true + } + ], "triple-equals": [ true, "allow-null-check" diff --git a/packages/core/LICENSE.md b/packages/core/LICENSE.md index 24a7d38fc..3dd835dee 100644 --- a/packages/core/LICENSE.md +++ b/packages/core/LICENSE.md @@ -19,7 +19,7 @@ Condition notice. Software: n8n -License: Apache 2.0 +License: Apache 2.0 with Commons Clause Licensor: n8n GmbH diff --git a/packages/core/package.json b/packages/core/package.json index 1c8a6b67d..4138b503a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "n8n-core", - "version": "0.44.0", + "version": "0.48.1", "description": "Core functionality of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", @@ -18,6 +18,7 @@ "build": "tsc", "dev": "npm run watch", "tslint": "tslint -p tsconfig.json -c tslint.json", + "tslintfix": "tslint --fix -p tsconfig.json -c tslint.json", "watch": "tsc --watch", "test": "jest" }, @@ -26,27 +27,28 @@ ], "devDependencies": { "@types/cron": "^1.7.1", - "@types/crypto-js": "^3.1.43", + "@types/crypto-js": "^4.0.1", "@types/express": "^4.17.6", - "@types/jest": "^25.2.1", + "@types/jest": "^26.0.13", "@types/lodash.get": "^4.4.6", "@types/mime-types": "^2.1.0", - "@types/node": "^14.0.27", + "@types/node": "14.0.27", "@types/request-promise-native": "~1.0.15", - "jest": "^24.9.0", + "jest": "^26.4.2", "source-map-support": "^0.5.9", - "ts-jest": "^25.4.0", + "ts-jest": "^26.3.0", "tslint": "^6.1.2", - "typescript": "~3.7.4" + "typescript": "~3.9.7" }, "dependencies": { "client-oauth2": "^4.2.5", "cron": "^1.7.2", - "crypto-js": "3.1.9-1", + "crypto-js": "4.0.0", "file-type": "^14.6.2", "lodash.get": "^4.4.2", "mime-types": "^2.1.27", - "n8n-workflow": "~0.39.0", + "n8n-workflow": "~0.42.0", + "oauth-1.0a": "^2.2.6", "p-cancelable": "^2.0.0", "request": "^2.88.2", "request-promise-native": "^1.0.7" diff --git a/packages/core/src/ActiveWebhooks.ts b/packages/core/src/ActiveWebhooks.ts index 5b69fab24..f4b1f1f86 100644 --- a/packages/core/src/ActiveWebhooks.ts +++ b/packages/core/src/ActiveWebhooks.ts @@ -52,7 +52,7 @@ export class ActiveWebhooks { try { const webhookExists = await workflow.runWebhookMethod('checkExists', webhookData, NodeExecuteFunctions, mode, this.testWebhooks); - if (webhookExists === false) { + if (webhookExists !== true) { // If webhook does not exist yet create it await workflow.runWebhookMethod('create', webhookData, NodeExecuteFunctions, mode, this.testWebhooks); @@ -60,7 +60,6 @@ export class ActiveWebhooks { } catch (error) { // If there was a problem unregister the webhook again delete this.webhookUrls[webhookKey]; - delete this.workflowWebhooks[webhookData.workflowId]; throw error; } @@ -159,7 +158,7 @@ export class ActiveWebhooks { /** - * Removes all the webhooks of the given workflow + * Removes all the webhooks of the given workflows */ async removeAll(workflows: Workflow[]): Promise { const removePromises = []; diff --git a/packages/core/src/ActiveWorkflows.ts b/packages/core/src/ActiveWorkflows.ts index 5576aeed3..2e3b6d7c1 100644 --- a/packages/core/src/ActiveWorkflows.ts +++ b/packages/core/src/ActiveWorkflows.ts @@ -67,8 +67,6 @@ export class ActiveWorkflows { * @memberof ActiveWorkflows */ async add(id: string, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, getTriggerFunctions: IGetExecuteTriggerFunctions, getPollFunctions: IGetExecutePollFunctions): Promise { - console.log('ADD ID (active): ' + id); - this.workflowData[id] = {}; const triggerNodes = workflow.getTriggerNodes(); @@ -204,8 +202,6 @@ export class ActiveWorkflows { * @memberof ActiveWorkflows */ async remove(id: string): Promise { - console.log('REMOVE ID (active): ' + id); - if (!this.isActive(id)) { // Workflow is currently not registered throw new Error(`The workflow with the id "${id}" is currently not active and can so not be removed`); diff --git a/packages/core/src/Credentials.ts b/packages/core/src/Credentials.ts index e692f597c..b8c4d337f 100644 --- a/packages/core/src/Credentials.ts +++ b/packages/core/src/Credentials.ts @@ -5,7 +5,7 @@ import { ICredentialsEncrypted, } from 'n8n-workflow'; -import { enc, AES } from 'crypto-js'; +import { AES, enc } from 'crypto-js'; export class Credentials extends ICredentials { diff --git a/packages/core/src/Interfaces.ts b/packages/core/src/Interfaces.ts index 483cc1c0c..b67d1ccfc 100644 --- a/packages/core/src/Interfaces.ts +++ b/packages/core/src/Interfaces.ts @@ -9,13 +9,13 @@ import { ILoadOptionsFunctions as ILoadOptionsFunctionsBase, INodeExecutionData, INodeType, + IOAuth2Options, IPollFunctions as IPollFunctionsBase, IPollResponse, ITriggerFunctions as ITriggerFunctionsBase, ITriggerResponse, IWebhookFunctions as IWebhookFunctionsBase, IWorkflowSettings as IWorkflowSettingsWorkflow, - IOAuth2Options, } from 'n8n-workflow'; diff --git a/packages/core/src/LoadNodeParameterOptions.ts b/packages/core/src/LoadNodeParameterOptions.ts index e56121606..6a9b45750 100644 --- a/packages/core/src/LoadNodeParameterOptions.ts +++ b/packages/core/src/LoadNodeParameterOptions.ts @@ -36,7 +36,7 @@ export class LoadNodeParameterOptions { position: [ 0, 0, - ] + ], }; if (credentials) { diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index be0cf6497..bf7a6cd7c 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -1,9 +1,9 @@ import { + BINARY_ENCODING, IHookFunctions, ILoadOptionsFunctions, IResponseError, IWorkflowSettings, - BINARY_ENCODING, } from './'; import { @@ -19,6 +19,7 @@ import { INodeExecutionData, INodeParameters, INodeType, + IOAuth2Options, IPollFunctions, IRunExecutionData, ITaskDataConnections, @@ -34,7 +35,6 @@ import { Workflow, WorkflowDataProxy, WorkflowExecuteMode, - IOAuth2Options, } from 'n8n-workflow'; import * as clientOAuth1 from 'oauth-1.0a'; @@ -43,7 +43,7 @@ import * as clientOAuth2 from 'client-oauth2'; import { get } from 'lodash'; import * as express from 'express'; import * as path from 'path'; -import { OptionsWithUrl, OptionsWithUri } from 'request'; +import { OptionsWithUri, OptionsWithUrl } from 'request'; import * as requestPromise from 'request-promise-native'; import { createHmac } from 'crypto'; import { fromBuffer } from 'file-type'; @@ -91,7 +91,7 @@ export async function prepareBinaryData(binaryData: Buffer, filePath?: string, m // TODO: Should program it in a way that it does not have to converted to base64 // It should only convert to and from base64 when saved in database because // of for example an error or when there is a wait node. - data: binaryData.toString(BINARY_ENCODING) + data: binaryData.toString(BINARY_ENCODING), }; if (filePath) { @@ -152,11 +152,16 @@ export function requestOAuth2(this: IAllExecuteFunctions, credentialsType: strin // on the token-type used. const newRequestOptions = token.sign(requestOptions as clientOAuth2.RequestObject); + // If keep bearer is false remove the it from the authorization header + if (oAuth2Options?.keepBearer === false) { + //@ts-ignore + newRequestOptions?.headers?.Authorization = newRequestOptions?.headers?.Authorization.split(' ')[1]; + } + return this.helpers.request!(newRequestOptions) .catch(async (error: IResponseError) => { // TODO: Check if also other codes are possible if (error.statusCode === 401) { - // TODO: Whole refresh process is not tested yet // Token is probably not valid anymore. So try refresh it. const tokenRefreshOptions: IDataObject = {}; @@ -388,7 +393,7 @@ export function getNodeParameter(workflow: Workflow, runExecutionData: IRunExecu let returnData; try { - returnData = workflow.getParameterValue(value, runExecutionData, runIndex, itemIndex, node.name, connectionInputData); + returnData = workflow.expression.getParameterValue(value, runExecutionData, runIndex, itemIndex, node.name, connectionInputData); } catch (e) { e.message += ` [Error in parameter: "${parameterName}"]`; throw e; @@ -434,12 +439,12 @@ export function getNodeWebhookUrl(name: string, workflow: Workflow, node: INode, return undefined; } - const path = workflow.getSimpleParameterValue(node, webhookDescription['path']); + const path = workflow.expression.getSimpleParameterValue(node, webhookDescription['path']); if (path === undefined) { return undefined; } - const isFullPath: boolean = workflow.getSimpleParameterValue(node, webhookDescription['isFullPath'], false) as boolean; + const isFullPath: boolean = workflow.expression.getSimpleParameterValue(node, webhookDescription['isFullPath'], false) as boolean; return NodeHelpers.getNodeWebhookUrl(baseUrl, workflow.id!, node, path.toString(), isFullPath); } @@ -654,7 +659,7 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx return continueOnFail(node); }, evaluateExpression: (expression: string, itemIndex: number) => { - return workflow.resolveSimpleParameterValue('=' + expression, runExecutionData, runIndex, itemIndex, node.name, connectionInputData); + return workflow.expression.resolveSimpleParameterValue('=' + expression, runExecutionData, runIndex, itemIndex, node.name, connectionInputData); }, async executeWorkflow(workflowInfo: IExecuteWorkflowInfo, inputData?: INodeExecutionData[]): Promise { // tslint:disable-line:no-any return additionalData.executeWorkflow(workflowInfo, additionalData, inputData); @@ -752,7 +757,7 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData: }, evaluateExpression: (expression: string, evaluateItemIndex: number | undefined) => { evaluateItemIndex = evaluateItemIndex === undefined ? itemIndex : evaluateItemIndex; - return workflow.resolveSimpleParameterValue('=' + expression, runExecutionData, runIndex, evaluateItemIndex, node.name, connectionInputData); + return workflow.expression.resolveSimpleParameterValue('=' + expression, runExecutionData, runIndex, evaluateItemIndex, node.name, connectionInputData); }, getContext(type: string): IContextObject { return NodeHelpers.getContext(runExecutionData, type, node); diff --git a/packages/core/src/UserSettings.ts b/packages/core/src/UserSettings.ts index fb38fc779..83fba72d6 100644 --- a/packages/core/src/UserSettings.ts +++ b/packages/core/src/UserSettings.ts @@ -1,10 +1,10 @@ import { ENCRYPTION_KEY_ENV_OVERWRITE, EXTENSIONS_SUBDIRECTORY, + IUserSettings, USER_FOLDER_ENV_OVERWRITE, USER_SETTINGS_FILE_NAME, USER_SETTINGS_SUBFOLDER, - IUserSettings, } from '.'; diff --git a/packages/core/src/WorkflowExecute.ts b/packages/core/src/WorkflowExecute.ts index 072689acf..4133db0d9 100644 --- a/packages/core/src/WorkflowExecute.ts +++ b/packages/core/src/WorkflowExecute.ts @@ -15,8 +15,8 @@ import { ITaskDataConnections, IWaitingForExecution, IWorkflowExecuteAdditionalData, - WorkflowExecuteMode, Workflow, + WorkflowExecuteMode, } from 'n8n-workflow'; import { NodeExecuteFunctions, @@ -84,7 +84,7 @@ export class WorkflowExecute { ], ], }, - } + }, ]; this.runExecutionData = { @@ -137,8 +137,8 @@ export class WorkflowExecute { // If it has no incoming data add the default empty data incomingData.push([ { - json: {} - } + json: {}, + }, ]); } else { // Get the data of the incoming connections @@ -156,7 +156,7 @@ export class WorkflowExecute { node: workflow.getNode(startNode) as INode, data: { main: incomingData, - } + }, }; nodeExecutionStack.push(executeData); @@ -252,7 +252,7 @@ export class WorkflowExecute { if (this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] === undefined) { // Node does not have data for runIndex yet so create also empty one and init it this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] = { - main: [] + main: [], }; for (let i = 0; i < workflow.connectionsByDestinationNode[connectionData.node]['main'].length; i++) { this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main.push(null); @@ -282,7 +282,7 @@ export class WorkflowExecute { // So add it to the execution stack this.runExecutionData.executionData!.nodeExecutionStack.push({ node: workflow.nodes[connectionData.node], - data: this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] + data: this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex], }); // Remove the data from waiting @@ -426,15 +426,15 @@ export class WorkflowExecute { this.runExecutionData.executionData!.waitingExecution[connectionData.node] = {}; } this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] = { - main: connectionDataArray + main: connectionDataArray, }; } else { // All data is there so add it directly to stack this.runExecutionData.executionData!.nodeExecutionStack.push({ node: workflow.nodes[connectionData.node], data: { - main: connectionDataArray - } + main: connectionDataArray, + }, }); } } @@ -608,7 +608,7 @@ export class WorkflowExecute { nodeSuccessData[0] = [ { json: {}, - } + }, ]; } } @@ -622,6 +622,8 @@ export class WorkflowExecute { break; } catch (error) { + this.runExecutionData.resultData.lastNodeExecuted = executionData.node.name; + executionError = { message: error.message, stack: error.stack, @@ -637,7 +639,7 @@ export class WorkflowExecute { } taskData = { startTime, - executionTime: (new Date().getTime()) - startTime + executionTime: (new Date().getTime()) - startTime, }; if (executionError !== undefined) { @@ -667,7 +669,7 @@ export class WorkflowExecute { // Node executed successfully. So add data and go on. taskData.data = ({ - 'main': nodeSuccessData + 'main': nodeSuccessData, } as ITaskDataConnections); this.executeHook('nodeExecuteAfter', [executionNode.name, taskData]); @@ -698,7 +700,10 @@ export class WorkflowExecute { return Promise.reject(new Error(`The node "${executionNode.name}" connects to not found node "${connectionData.node}"`)); } - this.addNodeToBeExecuted(workflow, connectionData, parseInt(outputIndex, 10), executionNode.name, nodeSuccessData!, runIndex); + if (nodeSuccessData![outputIndex] && nodeSuccessData![outputIndex].length !== 0) { + // Add the node only if there is data for it to process + this.addNodeToBeExecuted(workflow, connectionData, parseInt(outputIndex, 10), executionNode.name, nodeSuccessData!, runIndex); + } } } } diff --git a/packages/core/test/Credentials.test.ts b/packages/core/test/Credentials.test.ts index dc8e151b8..5fbcfc7be 100644 --- a/packages/core/test/Credentials.test.ts +++ b/packages/core/test/Credentials.test.ts @@ -3,86 +3,86 @@ import { Credentials } from '../src'; describe('Credentials', () => { - describe('without nodeType set', () => { + describe('without nodeType set', () => { - test('should be able to set and read key data without initial data set', () => { + test('should be able to set and read key data without initial data set', () => { - const credentials = new Credentials('testName', 'testType', []); + const credentials = new Credentials('testName', 'testType', []); - const key = 'key1'; - const password = 'password'; - // const nodeType = 'base.noOp'; - const newData = 1234; + const key = 'key1'; + const password = 'password'; + // const nodeType = 'base.noOp'; + const newData = 1234; - credentials.setDataKey(key, newData, password); + credentials.setDataKey(key, newData, password); - expect(credentials.getDataKey(key, password)).toEqual(newData); - }); + expect(credentials.getDataKey(key, password)).toEqual(newData); + }); - test('should be able to set and read key data with initial data set', () => { + test('should be able to set and read key data with initial data set', () => { - const key = 'key2'; - const password = 'password'; + const key = 'key2'; + const password = 'password'; - // Saved under "key1" - const initialData = 4321; - const initialDataEncoded = 'U2FsdGVkX1+0baznXt+Ag/ub8A2kHLyoLxn/rR9h4XQ='; + // Saved under "key1" + const initialData = 4321; + const initialDataEncoded = 'U2FsdGVkX1+0baznXt+Ag/ub8A2kHLyoLxn/rR9h4XQ='; - const credentials = new Credentials('testName', 'testType', [], initialDataEncoded); + const credentials = new Credentials('testName', 'testType', [], initialDataEncoded); - const newData = 1234; + const newData = 1234; - // Set and read new data - credentials.setDataKey(key, newData, password); - expect(credentials.getDataKey(key, password)).toEqual(newData); + // Set and read new data + credentials.setDataKey(key, newData, password); + expect(credentials.getDataKey(key, password)).toEqual(newData); - // Read the data which got provided encrypted on init - expect(credentials.getDataKey('key1', password)).toEqual(initialData); - }); + // Read the data which got provided encrypted on init + expect(credentials.getDataKey('key1', password)).toEqual(initialData); + }); - }); + }); - describe('with nodeType set', () => { + describe('with nodeType set', () => { - test('should be able to set and read key data without initial data set', () => { + test('should be able to set and read key data without initial data set', () => { - const nodeAccess = [ - { - nodeType: 'base.noOp', - user: 'userName', - date: new Date(), - } - ]; + const nodeAccess = [ + { + nodeType: 'base.noOp', + user: 'userName', + date: new Date(), + }, + ]; - const credentials = new Credentials('testName', 'testType', nodeAccess); + const credentials = new Credentials('testName', 'testType', nodeAccess); - const key = 'key1'; - const password = 'password'; - const nodeType = 'base.noOp'; - const newData = 1234; + const key = 'key1'; + const password = 'password'; + const nodeType = 'base.noOp'; + const newData = 1234; - credentials.setDataKey(key, newData, password); + credentials.setDataKey(key, newData, password); - // Should be able to read with nodeType which has access - expect(credentials.getDataKey(key, password, nodeType)).toEqual(newData); + // Should be able to read with nodeType which has access + expect(credentials.getDataKey(key, password, nodeType)).toEqual(newData); - // Should not be able to read with nodeType which does NOT have access - // expect(credentials.getDataKey(key, password, 'base.otherNode')).toThrowError(Error); - try { - credentials.getDataKey(key, password, 'base.otherNode'); - expect(true).toBe(false); - } catch (e) { - expect(e.message).toBe('The node of type "base.otherNode" does not have access to credentials "testName" of type "testType".'); - } + // Should not be able to read with nodeType which does NOT have access + // expect(credentials.getDataKey(key, password, 'base.otherNode')).toThrowError(Error); + try { + credentials.getDataKey(key, password, 'base.otherNode'); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe('The node of type "base.otherNode" does not have access to credentials "testName" of type "testType".'); + } - // Get the data which will be saved in database - const dbData = credentials.getDataToSave(); - expect(dbData.name).toEqual('testName'); - expect(dbData.type).toEqual('testType'); - expect(dbData.nodesAccess).toEqual(nodeAccess); - // Compare only the first 6 characters as the rest seems to change with each execution - expect(dbData.data!.slice(0, 6)).toEqual('U2FsdGVkX1+wpQWkj+YTzaPSNTFATjnlmFKIsUTZdhk='.slice(0, 6)); - }); - }); + // Get the data which will be saved in database + const dbData = credentials.getDataToSave(); + expect(dbData.name).toEqual('testName'); + expect(dbData.type).toEqual('testType'); + expect(dbData.nodesAccess).toEqual(nodeAccess); + // Compare only the first 6 characters as the rest seems to change with each execution + expect(dbData.data!.slice(0, 6)).toEqual('U2FsdGVkX1+wpQWkj+YTzaPSNTFATjnlmFKIsUTZdhk='.slice(0, 6)); + }); + }); }); diff --git a/packages/core/test/Helpers.ts b/packages/core/test/Helpers.ts index 790025dcd..b79270833 100644 --- a/packages/core/test/Helpers.ts +++ b/packages/core/test/Helpers.ts @@ -7,8 +7,8 @@ import { INodeExecutionData, INodeParameters, INodeType, - INodeTypes, INodeTypeData, + INodeTypes, IRun, ITaskData, IWorkflowBase, @@ -87,7 +87,7 @@ class NodeTypesClass implements INodeTypes { displayOptions: { show: { mode: [ - 'passThrough' + 'passThrough', ], }, }, @@ -104,7 +104,7 @@ class NodeTypesClass implements INodeTypes { default: 'input1', description: 'Defines of which input the data should be used as output of node.', }, - ] + ], }, async execute(this: IExecuteFunctions): Promise { // const itemsInput2 = this.getInputData(1); @@ -131,7 +131,7 @@ class NodeTypesClass implements INodeTypes { } return [returnData]; - } + }, }, }, 'n8n-nodes-base.set': { @@ -186,11 +186,11 @@ class NodeTypesClass implements INodeTypes { default: 0, description: 'The number value to write in the property.', }, - ] + ], }, ], }, - ] + ], }, execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); @@ -213,7 +213,7 @@ class NodeTypesClass implements INodeTypes { } return this.prepareOutputData(returnData); - } + }, }, }, 'n8n-nodes-base.start': { @@ -231,7 +231,7 @@ class NodeTypesClass implements INodeTypes { }, inputs: [], outputs: ['main'], - properties: [] + properties: [], }, execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); diff --git a/packages/core/test/WorkflowExecute.test.ts b/packages/core/test/WorkflowExecute.test.ts index 811b90cef..4213cba39 100644 --- a/packages/core/test/WorkflowExecute.test.ts +++ b/packages/core/test/WorkflowExecute.test.ts @@ -47,8 +47,8 @@ describe('WorkflowExecute', () => { "typeVersion": 1, "position": [ 100, - 300 - ] + 300, + ], }, { "parameters": { @@ -56,19 +56,19 @@ describe('WorkflowExecute', () => { "number": [ { "name": "value1", - "value": 1 - } - ] - } + "value": 1, + }, + ], + }, }, "name": "Set", "type": "n8n-nodes-base.set", "typeVersion": 1, "position": [ 280, - 300 - ] - } + 300, + ], + }, ], "connections": { "Start": { @@ -77,12 +77,12 @@ describe('WorkflowExecute', () => { { "node": "Set", "type": "main", - "index": 0 - } - ] - ] - } - } + "index": 0, + }, + ], + ], + }, + }, }, }, output: { @@ -115,8 +115,8 @@ describe('WorkflowExecute', () => { "typeVersion": 1, "position": [ 100, - 300 - ] + 300, + ], }, { "parameters": { @@ -124,18 +124,18 @@ describe('WorkflowExecute', () => { "number": [ { "name": "value1", - "value": 1 - } - ] - } + "value": 1, + }, + ], + }, }, "name": "Set1", "type": "n8n-nodes-base.set", "typeVersion": 1, "position": [ 300, - 250 - ] + 250, + ], }, { "parameters": { @@ -143,19 +143,19 @@ describe('WorkflowExecute', () => { "number": [ { "name": "value2", - "value": 2 - } - ] - } + "value": 2, + }, + ], + }, }, "name": "Set2", "type": "n8n-nodes-base.set", "typeVersion": 1, "position": [ 500, - 400 - ] - } + 400, + ], + }, ], "connections": { "Start": { @@ -164,15 +164,15 @@ describe('WorkflowExecute', () => { { "node": "Set1", "type": "main", - "index": 0 + "index": 0, }, { "node": "Set2", "type": "main", - "index": 0 - } - ] - ] + "index": 0, + }, + ], + ], }, "Set1": { "main": [ @@ -180,12 +180,12 @@ describe('WorkflowExecute', () => { { "node": "Set2", "type": "main", - "index": 0 - } - ] - ] - } - } + "index": 0, + }, + ], + ], + }, + }, }, }, output: { @@ -201,7 +201,7 @@ describe('WorkflowExecute', () => { { value1: 1, }, - ] + ], ], Set2: [ [ @@ -228,15 +228,15 @@ describe('WorkflowExecute', () => { "nodes": [ { "parameters": { - "mode": "passThrough" + "mode": "passThrough", }, "name": "Merge4", "type": "n8n-nodes-base.merge", "typeVersion": 1, "position": [ 1150, - 500 - ] + 500, + ], }, { "parameters": { @@ -244,18 +244,18 @@ describe('WorkflowExecute', () => { "number": [ { "name": "value2", - "value": 2 - } - ] - } + "value": 2, + }, + ], + }, }, "name": "Set2", "type": "n8n-nodes-base.set", "typeVersion": 1, "position": [ 290, - 400 - ] + 400, + ], }, { "parameters": { @@ -263,18 +263,18 @@ describe('WorkflowExecute', () => { "number": [ { "name": "value4", - "value": 4 - } - ] - } + "value": 4, + }, + ], + }, }, "name": "Set4", "type": "n8n-nodes-base.set", "typeVersion": 1, "position": [ 850, - 200 - ] + 200, + ], }, { "parameters": { @@ -282,30 +282,30 @@ describe('WorkflowExecute', () => { "number": [ { "name": "value3", - "value": 3 - } - ] - } + "value": 3, + }, + ], + }, }, "name": "Set3", "type": "n8n-nodes-base.set", "typeVersion": 1, "position": [ 650, - 200 - ] + 200, + ], }, { "parameters": { - "mode": "passThrough" + "mode": "passThrough", }, "name": "Merge4", "type": "n8n-nodes-base.merge", "typeVersion": 1, "position": [ 1150, - 500 - ] + 500, + ], }, { "parameters": {}, @@ -314,21 +314,21 @@ describe('WorkflowExecute', () => { "typeVersion": 1, "position": [ 1000, - 400 - ] + 400, + ], }, { "parameters": { "mode": "passThrough", - "output": "input2" + "output": "input2", }, "name": "Merge2", "type": "n8n-nodes-base.merge", "typeVersion": 1, "position": [ 700, - 400 - ] + 400, + ], }, { "parameters": {}, @@ -337,8 +337,8 @@ describe('WorkflowExecute', () => { "typeVersion": 1, "position": [ 500, - 300 - ] + 300, + ], }, { "parameters": { @@ -346,18 +346,18 @@ describe('WorkflowExecute', () => { "number": [ { "name": "value1", - "value": 1 - } - ] - } + "value": 1, + }, + ], + }, }, "name": "Set1", "type": "n8n-nodes-base.set", "typeVersion": 1, "position": [ 300, - 200 - ] + 200, + ], }, { "parameters": {}, @@ -366,9 +366,9 @@ describe('WorkflowExecute', () => { "typeVersion": 1, "position": [ 100, - 300 - ] - } + 300, + ], + }, ], "connections": { "Set2": { @@ -377,15 +377,15 @@ describe('WorkflowExecute', () => { { "node": "Merge1", "type": "main", - "index": 1 + "index": 1, }, { "node": "Merge2", "type": "main", - "index": 1 - } - ] - ] + "index": 1, + }, + ], + ], }, "Set4": { "main": [ @@ -393,10 +393,10 @@ describe('WorkflowExecute', () => { { "node": "Merge3", "type": "main", - "index": 0 - } - ] - ] + "index": 0, + }, + ], + ], }, "Set3": { "main": [ @@ -404,10 +404,10 @@ describe('WorkflowExecute', () => { { "node": "Set4", "type": "main", - "index": 0 - } - ] - ] + "index": 0, + }, + ], + ], }, "Merge3": { "main": [ @@ -415,10 +415,10 @@ describe('WorkflowExecute', () => { { "node": "Merge4", "type": "main", - "index": 0 - } - ] - ] + "index": 0, + }, + ], + ], }, "Merge2": { "main": [ @@ -426,10 +426,10 @@ describe('WorkflowExecute', () => { { "node": "Merge3", "type": "main", - "index": 1 - } - ] - ] + "index": 1, + }, + ], + ], }, "Merge1": { "main": [ @@ -437,10 +437,10 @@ describe('WorkflowExecute', () => { { "node": "Merge2", "type": "main", - "index": 0 - } - ] - ] + "index": 0, + }, + ], + ], }, "Set1": { "main": [ @@ -448,15 +448,15 @@ describe('WorkflowExecute', () => { { "node": "Merge1", "type": "main", - "index": 0 + "index": 0, }, { "node": "Set3", "type": "main", - "index": 0 - } - ] - ] + "index": 0, + }, + ], + ], }, "Start": { "main": [ @@ -464,22 +464,22 @@ describe('WorkflowExecute', () => { { "node": "Set1", "type": "main", - "index": 0 + "index": 0, }, { "node": "Set2", "type": "main", - "index": 0 + "index": 0, }, { "node": "Merge4", "type": "main", - "index": 1 - } - ] - ] - } - } + "index": 1, + }, + ], + ], + }, + }, }, }, output: { @@ -534,14 +534,14 @@ describe('WorkflowExecute', () => { { value2: 2, }, - ] + ], ], Merge2: [ [ { value2: 2, }, - ] + ], ], Merge3: [ [ @@ -553,7 +553,7 @@ describe('WorkflowExecute', () => { { value2: 2, }, - ] + ], ], Merge4: [ [ @@ -565,7 +565,7 @@ describe('WorkflowExecute', () => { { value2: 2, }, - ] + ], ], }, }, diff --git a/packages/core/tslint.json b/packages/core/tslint.json index 7eb9d0110..f03bbfee3 100644 --- a/packages/core/tslint.json +++ b/packages/core/tslint.json @@ -46,6 +46,11 @@ "forin": true, "jsdoc-format": true, "label-position": true, + "indent": [ + true, + "tabs", + 2 + ], "member-access": [ true, "no-public" @@ -60,6 +65,13 @@ "no-default-export": true, "no-duplicate-variable": true, "no-inferrable-types": true, + "ordered-imports": [ + true, + { + "import-sources-order": "any", + "named-imports-order": "case-insensitive" + } + ], "no-namespace": [ true, "allow-declarations" @@ -82,6 +94,18 @@ "ignore-bound-class-methods" ], "switch-default": true, + "trailing-comma": [ + true, + { + "multiline": { + "objects": "always", + "arrays": "always", + "functions": "always", + "typeLiterals": "ignore" + }, + "esSpecCompliant": true + } + ], "triple-equals": [ true, "allow-null-check" diff --git a/packages/editor-ui/LICENSE.md b/packages/editor-ui/LICENSE.md index 24a7d38fc..3dd835dee 100644 --- a/packages/editor-ui/LICENSE.md +++ b/packages/editor-ui/LICENSE.md @@ -19,7 +19,7 @@ Condition notice. Software: n8n -License: Apache 2.0 +License: Apache 2.0 with Commons Clause Licensor: n8n GmbH diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index a325c78ab..ae33f0e2e 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -1,6 +1,6 @@ { "name": "n8n-editor-ui", - "version": "0.55.0", + "version": "0.60.0", "description": "Workflow Editor UI for n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", @@ -20,12 +20,11 @@ "serve": "cross-env VUE_APP_URL_BASE_API=http://localhost:5678/ vue-cli-service serve", "test": "npm run test:unit", "tslint": "tslint -p tsconfig.json -c tslint.json", + "tslintfix": "tslint --fix -p tsconfig.json -c tslint.json", "test:e2e": "vue-cli-service test:e2e", "test:unit": "vue-cli-service test:unit" }, - "dependencies": { - "uuid": "^8.1.0" - }, + "dependencies": {}, "devDependencies": { "@beyonk/google-fonts-webpack-plugin": "^1.2.3", "@fortawesome/fontawesome-svg-core": "^1.2.19", @@ -34,16 +33,16 @@ "@types/dateformat": "^3.0.0", "@types/express": "^4.17.6", "@types/file-saver": "^2.0.1", - "@types/jest": "^25.2.1", + "@types/jest": "^26.0.13", "@types/lodash.get": "^4.4.6", "@types/lodash.set": "^4.3.6", - "@types/node": "^14.0.27", + "@types/node": "14.0.27", "@types/quill": "^2.0.1", "@typescript-eslint/eslint-plugin": "^2.13.0", "@typescript-eslint/parser": "^2.13.0", "@vue/cli-plugin-babel": "^4.1.2", "@vue/cli-plugin-eslint": "^4.1.2", - "@vue/cli-plugin-typescript": "~4.1.2", + "@vue/cli-plugin-typescript": "~4.5.6", "@vue/cli-plugin-unit-jest": "^4.1.2", "@vue/cli-service": "^3.11.0", "@vue/eslint-config-standard": "^5.0.1", @@ -66,16 +65,18 @@ "lodash.debounce": "^4.0.8", "lodash.get": "^4.4.2", "lodash.set": "^4.3.2", - "n8n-workflow": "~0.39.0", + "n8n-workflow": "~0.42.0", "node-sass": "^4.12.0", + "normalize-wheel": "^1.0.1", "prismjs": "^1.17.1", "quill": "^2.0.0-dev.3", "quill-autoformat": "^0.1.1", "sass-loader": "^8.0.0", "string-template-parser": "^1.2.6", - "ts-jest": "^25.4.0", + "ts-jest": "^26.3.0", "tslint": "^6.1.2", - "typescript": "~3.7.4", + "typescript": "~3.9.7", + "uuid": "^8.1.0", "vue": "^2.6.9", "vue-cli-plugin-webpack-bundle-analyzer": "^2.0.0", "vue-json-tree": "^0.4.1", @@ -83,6 +84,7 @@ "vue-router": "^3.0.6", "vue-template-compiler": "^2.5.17", "vue-typed-mixins": "^0.2.0", + "vue2-touch-events": "^2.3.2", "vuex": "^3.1.1" } } diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 2f3973b84..5e2874577 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -126,6 +126,7 @@ export interface IRestApi { makeRestApiRequest(method: string, endpoint: string, data?: any): Promise; // tslint:disable-line:no-any getSettings(): Promise; getNodeTypes(): Promise; + getNodesInformation(nodeList: string[]): Promise; getNodeParameterOptions(nodeType: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise; removeTestWebhook(workflowId: string): Promise; runWorkflow(runData: IStartRunData): Promise; @@ -399,6 +400,10 @@ export interface IN8nUISettings { timezone: string; executionTimeout: number; maxExecutionTimeout: number; + oauthCallbackUrls: { + oauth1: string; + oauth2: string; + }; urlBaseWebhook: string; versionCli: string; } diff --git a/packages/editor-ui/src/components/CredentialsEdit.vue b/packages/editor-ui/src/components/CredentialsEdit.vue index 9ea199ef3..49e3a12ac 100644 --- a/packages/editor-ui/src/components/CredentialsEdit.vue +++ b/packages/editor-ui/src/components/CredentialsEdit.vue @@ -4,7 +4,7 @@
{{title}}
-
+
- Need help? Open credential docs + Need help? Open credential docs
@@ -109,27 +109,19 @@ export default mixins( } } }, - documentationUrl (): string { + documentationUrl (): string | undefined { + let credentialTypeName = ''; if (this.editCredentials) { - const credentialType = this.$store.getters.credentialType(this.editCredentials.type); - if (credentialType.documentationUrl === undefined) { - return credentialType.name; - } else { - return `${credentialType.documentationUrl}`; - } + credentialTypeName = this.editCredentials.type as string; } else { - if (this.credentialType) { - const credentialType = this.$store.getters.credentialType(this.credentialType); - - if (credentialType.documentationUrl === undefined) { - return credentialType.name; - } else { - return `${credentialType.documentationUrl}`; - } - } else { - return ''; - } + credentialTypeName = this.credentialType as string; } + + const credentialType = this.$store.getters.credentialType(credentialTypeName); + if (credentialType.documentationUrl !== undefined) { + return `${credentialType.documentationUrl}`; + } + return undefined; }, node (): INodeUi { return this.$store.getters.activeNode; diff --git a/packages/editor-ui/src/components/CredentialsInput.vue b/packages/editor-ui/src/components/CredentialsInput.vue index 27a459733..ae678cb25 100644 --- a/packages/editor-ui/src/components/CredentialsInput.vue +++ b/packages/editor-ui/src/components/CredentialsInput.vue @@ -235,7 +235,7 @@ export default mixins( oAuthCallbackUrl (): string { const types = this.parentTypes(this.credentialTypeData.name); const oauthType = (this.credentialTypeData.name === 'oAuth2Api' || types.includes('oAuth2Api')) ? 'oauth2' : 'oauth1'; - return this.$store.getters.getWebhookBaseUrl + `rest/${oauthType}-credential/callback`; + return this.$store.getters.oauthCallbackUrls[oauthType]; }, requiredPropertiesFilled (): boolean { for (const property of this.credentialProperties) { @@ -404,10 +404,11 @@ export default mixins( message: 'Connected successfully!', type: 'success', }); + + // Make sure that the event gets removed again + window.removeEventListener('message', receiveMessage, false); } - // Make sure that the event gets removed again - window.removeEventListener('message', receiveMessage, false); }; window.addEventListener('message', receiveMessage, false); diff --git a/packages/editor-ui/src/components/Node.vue b/packages/editor-ui/src/components/Node.vue index 13c2fee62..d8af701f4 100644 --- a/packages/editor-ui/src/components/Node.vue +++ b/packages/editor-ui/src/components/Node.vue @@ -1,6 +1,6 @@