From 56c4c6991fb21ba4b7bdcd22c929f63cc1d1defe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Sun, 29 Aug 2021 20:58:11 +0200 Subject: [PATCH] :art: Set up linting and formatting (#2120) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :arrow_up: Upgrade TS to 4.3.5 * :shirt: Add ESLint configs * :art: Add Prettier config * :package: Add deps and commands * :zap: Adjust global .editorconfig to new ruleset * :fire: Remove unneeded local .editorconfig * :package: Update deps in editor-ui * :hammer: Limit Prettier to only TS files * :zap: Add recommended VSCode extensions * :shirt: Fix build * :fire: Remove Vue setting from global config * :zap: Disable prefer-default-export per feedback * :pencil2: Add forgotten divider * :shirt: Disable no-plusplus * :shirt: Disable class-methods-use-this * :pencil2: Alphabetize overrides * :shirt: Add one-var consecutive override * :rewind: Revert one-var consecutive override This reverts commit b9252cf935659ba6d76727ad484a1d3c00008fcc. * 🎨 👕 Lint and format workflow package (#2121) * :art: Format /workflow package * :shirt: Lint /workflow package * :art: Re-format /workflow package * :shirt: Re-lint /workflow package * :pencil2: Fix typo * :zap: Consolidate if-checks * :fire: Remove prefer-default-export exceptions * :fire: Remove no-plusplus exceptions * :fire: Remove class-methods-use-this exceptions * 🎨 👕 Lint and format node-dev package (#2122) * :art: Format /node-dev package * :zap: Exclude templates from ESLint config This keeps the templates consistent with the codebase while preventing lint exceptions from being made part of the templates. * :shirt: Lint /node-dev package * :fire: Remove prefer-default-export exceptions * :fire: Remove no-plusplus exceptions * 🎨 👕 Lint and format core package (#2123) * :art: Format /core package * :shirt: Lint /core package * :art: Re-format /core package * :shirt: Re-lint /core package * :fire: Remove prefer-default-export exceptions * :fire: Remove no-plusplus exceptions * :fire: Remove class-methods-use-this exceptions * 🎨 👕 Lint and format cli package (#2124) * :art: Format /cli package * :shirt: Exclude migrations from linting * :shirt: Lint /cli package * :art: Re-format /cli package * :shirt: Re-lint /cli package * :shirt: Fix build * :fire: Remove prefer-default-export exceptions * :zap: Update exceptions in ActiveExecutions * :fire: Remove no-plusplus exceptions * :fire: Remove class-methods-use-this exceptions * 👕 fix lint issues * :wrench: use package specific linter, remove tslint command * :hammer: resolve build issue, sync dependencies * :wrench: change lint command Co-authored-by: Ben Hesseldieck --- .editorconfig | 3 - .eslintrc.js | 354 ++ .github/workflows/tests.yml | 2 +- .gitignore | 4 +- .prettierrc.js | 51 + .vscode/extensions.json | 8 + package.json | 4 +- packages/cli/commands/Interfaces.d.ts | 14 +- packages/cli/commands/execute.ts | 47 +- packages/cli/commands/executeBatch.ts | 284 +- packages/cli/commands/export/credentials.ts | 61 +- packages/cli/commands/export/workflow.ts | 55 +- packages/cli/commands/import/credentials.ts | 44 +- packages/cli/commands/import/workflow.ts | 34 +- packages/cli/commands/list/workflow.ts | 25 +- packages/cli/commands/start.ts | 106 +- packages/cli/commands/update/workflow.ts | 44 +- packages/cli/commands/webhook.ts | 52 +- packages/cli/commands/worker.ts | 121 +- packages/cli/config/index.ts | 8 +- packages/cli/migrations/ormconfig.ts | 154 +- packages/cli/package.json | 7 +- packages/cli/src/ActiveExecutions.ts | 84 +- packages/cli/src/ActiveWorkflowRunner.ts | 388 +- packages/cli/src/CredentialTypes.ts | 23 +- packages/cli/src/CredentialsHelper.ts | 117 +- packages/cli/src/CredentialsOverwrites.ts | 38 +- packages/cli/src/Db.ts | 85 +- packages/cli/src/ExternalHooks.ts | 46 +- packages/cli/src/GenericHelpers.ts | 40 +- packages/cli/src/Interfaces.ts | 54 +- packages/cli/src/LoadNodesAndCredentials.ts | 88 +- packages/cli/src/Logger.ts | 37 +- packages/cli/src/NodeTypes.ts | 20 +- packages/cli/src/Push.ts | 27 +- packages/cli/src/Queue.ts | 39 +- packages/cli/src/ResponseHelper.ts | 49 +- packages/cli/src/Server.ts | 3569 ++++++----- packages/cli/src/TagHelpers.ts | 64 +- packages/cli/src/TestWebhooks.ts | 144 +- packages/cli/src/WaitTracker.ts | 67 +- packages/cli/src/WaitingWebhooks.ts | 126 +- packages/cli/src/WebhookHelpers.ts | 523 +- packages/cli/src/WebhookServer.ts | 283 +- packages/cli/src/WorkflowCredentials.ts | 24 +- .../cli/src/WorkflowExecuteAdditionalData.ts | 552 +- packages/cli/src/WorkflowHelpers.ts | 170 +- packages/cli/src/WorkflowRunner.ts | 545 +- packages/cli/src/WorkflowRunnerProcess.ts | 243 +- .../databases/entities/CredentialsEntity.ts | 25 +- .../src/databases/entities/ExecutionEntity.ts | 24 +- .../cli/src/databases/entities/TagEntity.ts | 22 +- .../src/databases/entities/WebhookEntity.ts | 13 +- .../src/databases/entities/WorkflowEntity.ts | 45 +- packages/cli/src/databases/entities/index.ts | 2 + packages/cli/src/databases/utils.ts | 16 +- packages/cli/src/index.ts | 3 + packages/cli/test/placeholder.test.ts | 2 - packages/core/package.json | 7 +- packages/core/src/ActiveWebhooks.ts | 91 +- packages/core/src/ActiveWorkflows.ts | 80 +- packages/core/src/Credentials.ts | 20 +- packages/core/src/DeferredPromise.ts | 6 +- packages/core/src/Interfaces.ts | 152 +- packages/core/src/LoadNodeParameterOptions.ts | 60 +- packages/core/src/NodeExecuteFunctions.ts | 905 ++- packages/core/src/UserSettings.ts | 49 +- packages/core/src/WorkflowExecute.ts | 491 +- packages/core/src/index.ts | 20 +- packages/core/test/Credentials.test.ts | 139 +- packages/core/test/Helpers.ts | 174 +- packages/core/test/WorkflowExecute.test.ts | 1197 ++-- packages/design-system/package.json | 23 +- packages/editor-ui/.editorconfig | 15 - packages/editor-ui/babel.config.js | 3 + packages/editor-ui/package.json | 20 +- .../editor-ui/src/components/VersionCard.vue | 1 + packages/node-dev/commands/build.ts | 19 +- packages/node-dev/commands/new.ts | 48 +- packages/node-dev/package.json | 9 +- packages/node-dev/src/Build.ts | 71 +- packages/node-dev/src/Create.ts | 19 +- packages/node-dev/src/index.ts | 1 + .../node-dev/templates/credentials/simple.ts | 8 +- packages/node-dev/templates/execute/simple.ts | 16 +- packages/node-dev/templates/trigger/simple.ts | 14 +- packages/node-dev/templates/webhook/simple.ts | 33 +- .../nodes-base/nodes/Discord/Discord.node.ts | 1 + packages/nodes-base/nodes/Slack/Slack.node.ts | 8 +- .../nodes/Snowflake/GenericFunctions.ts | 2 + .../nodes/Twitter/GenericFunctions.ts | 1 + packages/nodes-base/package.json | 7 +- packages/workflow/package.json | 15 +- packages/workflow/src/Expression.ts | 212 +- packages/workflow/src/Interfaces.ts | 325 +- packages/workflow/src/LoggerProxy.ts | 8 +- packages/workflow/src/NodeErrors.ts | 64 +- packages/workflow/src/NodeHelpers.ts | 492 +- packages/workflow/src/ObservableObject.ts | 41 +- packages/workflow/src/Workflow.ts | 360 +- packages/workflow/src/WorkflowDataProxy.ts | 332 +- packages/workflow/src/WorkflowErrors.ts | 4 +- packages/workflow/src/WorkflowHooks.ts | 23 +- packages/workflow/src/index.ts | 15 +- packages/workflow/test/Helpers.ts | 9 +- packages/workflow/test/NodeHelpers.test.ts | 5265 ++++++++--------- .../workflow/test/ObservableObject.test.ts | 29 +- packages/workflow/test/Workflow.test.ts | 265 +- 108 files changed, 11832 insertions(+), 8416 deletions(-) create mode 100644 .eslintrc.js create mode 100644 .prettierrc.js create mode 100644 .vscode/extensions.json delete mode 100644 packages/editor-ui/.editorconfig diff --git a/.editorconfig b/.editorconfig index b6b59f3ccf..fdda2197f7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,9 +12,6 @@ trim_trailing_whitespace = true indent_style = space indent_size = 2 -[*.ts] -quote_type = single - [*.yml] indent_style = space indent_size = 2 diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000000..17ee2a4cb0 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,354 @@ +module.exports = { + env: { + browser: true, + es6: true, + node: true, + }, + + parser: '@typescript-eslint/parser', + parserOptions: { + project: ['./packages/*/tsconfig.json'], + sourceType: 'module', + }, + ignorePatterns: [ + '.eslintrc.js', + '**/*.js', + '**/node_modules/**', + '**/dist/**', + '**/test/**', + '**/templates/**', + '**/ormconfig.ts', + '**/migrations/**', + ], + + extends: [ + /** + * Config for typescript-eslint recommended ruleset (without type checking) + * + * https://github.com/typescript-eslint/typescript-eslint/blob/1c1b572c3000d72cfe665b7afbada0ec415e7855/packages/eslint-plugin/src/configs/recommended.ts + */ + 'plugin:@typescript-eslint/recommended', + + /** + * Config for typescript-eslint recommended ruleset (with type checking) + * + * https://github.com/typescript-eslint/typescript-eslint/blob/1c1b572c3000d72cfe665b7afbada0ec415e7855/packages/eslint-plugin/src/configs/recommended-requiring-type-checking.ts + */ + 'plugin:@typescript-eslint/recommended-requiring-type-checking', + + /** + * Config for Airbnb style guide for TS, /base to remove React rules + * + * https://github.com/iamturns/eslint-config-airbnb-typescript + * https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb-base/rules + */ + 'eslint-config-airbnb-typescript/base', + + /** + * Config to disable ESLint rules covered by Prettier + * + * https://github.com/prettier/eslint-config-prettier + */ + 'eslint-config-prettier', + ], + + plugins: [ + /** + * Plugin with lint rules for import/export syntax + * https://github.com/import-js/eslint-plugin-import + */ + 'eslint-plugin-import', + + /** + * @typescript-eslint/eslint-plugin is required by eslint-config-airbnb-typescript + * See step 2: https://github.com/iamturns/eslint-config-airbnb-typescript#2-install-eslint-plugins + */ + '@typescript-eslint', + + /** + * Plugin to report formatting violations as lint violations + * https://github.com/prettier/eslint-plugin-prettier + */ + 'eslint-plugin-prettier', + ], + + rules: { + // ****************************************************************** + // required by prettier plugin + // ****************************************************************** + + // The following rule enables eslint-plugin-prettier + // See: https://github.com/prettier/eslint-plugin-prettier#recommended-configuration + + 'prettier/prettier': 'error', + + // The following two rules must be disabled when using eslint-plugin-prettier: + // See: https://github.com/prettier/eslint-plugin-prettier#arrow-body-style-and-prefer-arrow-callback-issue + + /** + * https://eslint.org/docs/rules/arrow-body-style + */ + 'arrow-body-style': 'off', + + /** + * https://eslint.org/docs/rules/prefer-arrow-callback + */ + 'prefer-arrow-callback': 'off', + + // ****************************************************************** + // additions to base ruleset + // ****************************************************************** + + // ---------------------------------- + // ESLint + // ---------------------------------- + + /** + * https://eslint.org/docs/rules/id-denylist + */ + 'id-denylist': [ + 'error', + 'err', + 'cb', + 'callback', + 'any', + 'Number', + 'number', + 'String', + 'string', + 'Boolean', + 'boolean', + 'Undefined', + 'undefined', + ], + + // ---------------------------------- + // @typescript-eslint + // ---------------------------------- + + /** + * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/array-type.md + */ + '@typescript-eslint/array-type': ['error', { default: 'array-simple' }], + + /** + * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/ban-ts-comment.md + */ + '@typescript-eslint/ban-ts-comment': 'off', + + /** + * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/ban-types.md + */ + '@typescript-eslint/ban-types': [ + 'error', + { + types: { + Object: { + message: 'Use object instead', + fixWith: 'object', + }, + String: { + message: 'Use string instead', + fixWith: 'string', + }, + Boolean: { + message: 'Use boolean instead', + fixWith: 'boolean', + }, + Number: { + message: 'Use number instead', + fixWith: 'number', + }, + Symbol: { + message: 'Use symbol instead', + fixWith: 'symbol', + }, + Function: { + message: [ + 'The `Function` type accepts any function-like value.', + 'It provides no type safety when calling the function, which can be a common source of bugs.', + 'It also accepts things like class declarations, which will throw at runtime as they will not be called with `new`.', + 'If you are expecting the function to accept certain arguments, you should explicitly define the function shape.', + ].join('\n'), + }, + }, + extendDefaults: false, + }, + ], + + /** + * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/consistent-type-assertions.md + */ + '@typescript-eslint/consistent-type-assertions': 'error', + + /** + * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/explicit-member-accessibility.md + */ + '@typescript-eslint/explicit-member-accessibility': ['error', { accessibility: 'no-public' }], + + /** + * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/member-delimiter-style.md + */ + '@typescript-eslint/member-delimiter-style': [ + 'error', + { + multiline: { + delimiter: 'semi', + requireLast: true, + }, + singleline: { + delimiter: 'semi', + requireLast: false, + }, + }, + ], + + /** + * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/naming-convention.md + */ + '@typescript-eslint/naming-convention': [ + 'error', + { + selector: 'default', + format: ['camelCase'], + }, + { + selector: 'variable', + format: ['camelCase', 'snake_case', 'UPPER_CASE'], + leadingUnderscore: 'allowSingleOrDouble', + trailingUnderscore: 'allowSingleOrDouble', + }, + { + selector: 'property', + format: ['camelCase', 'snake_case'], + leadingUnderscore: 'allowSingleOrDouble', + trailingUnderscore: 'allowSingleOrDouble', + }, + { + selector: 'typeLike', + format: ['PascalCase'], + }, + { + selector: ['method', 'function'], + format: ['camelCase'], + leadingUnderscore: 'allowSingleOrDouble', + }, + ], + + /** + * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-duplicate-imports.md + */ + '@typescript-eslint/no-duplicate-imports': 'error', + + /** + * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-invalid-void-type.md + */ + '@typescript-eslint/no-invalid-void-type': 'error', + + /** + * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-misused-promises.md + */ + '@typescript-eslint/no-misused-promises': ['error', { checksVoidReturn: false }], + + /** + * https://eslint.org/docs/1.0.0/rules/no-throw-literal + */ + '@typescript-eslint/no-throw-literal': 'error', + + /** + * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unnecessary-boolean-literal-compare.md + */ + '@typescript-eslint/no-unnecessary-boolean-literal-compare': 'error', + + /** + * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unnecessary-qualifier.md + */ + '@typescript-eslint/no-unnecessary-qualifier': 'error', + + /** + * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unused-expressions.md + */ + '@typescript-eslint/no-unused-expressions': 'error', + + /** + * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unused-vars.md + */ + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '_' }], + + /** + * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md + */ + '@typescript-eslint/prefer-nullish-coalescing': 'error', + + /** + * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-optional-chain.md + */ + '@typescript-eslint/prefer-optional-chain': 'error', + + /** + * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/promise-function-async.md + */ + '@typescript-eslint/promise-function-async': 'error', + + // ---------------------------------- + // eslint-plugin-import + // ---------------------------------- + + /** + * https://github.com/import-js/eslint-plugin-import/blob/master/docs/rules/no-default-export.md + */ + 'import/no-default-export': 'error', + + /** + * https://github.com/import-js/eslint-plugin-import/blob/master/docs/rules/order.md + */ + 'import/order': 'error', + + // ****************************************************************** + // overrides to base ruleset + // ****************************************************************** + + // ---------------------------------- + // ESLint + // ---------------------------------- + + /** + * https://eslint.org/docs/rules/class-methods-use-this + */ + 'class-methods-use-this': 'off', + + /** + * https://eslint.org/docs/rules/eqeqeq + */ + eqeqeq: 'error', + + /** + * https://eslint.org/docs/rules/no-plusplus + */ + 'no-plusplus': 'off', + + /** + * https://eslint.org/docs/rules/object-shorthand + */ + 'object-shorthand': 'error', + + /** + * https://eslint.org/docs/rules/prefer-const + */ + 'prefer-const': 'error', + + /** + * https://eslint.org/docs/rules/prefer-spread + */ + 'prefer-spread': 'error', + + // ---------------------------------- + // import + // ---------------------------------- + + /** + * https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/prefer-default-export.md + */ + 'import/prefer-default-export': 'off', + }, +}; diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9b95d10d44..79c2ba2e32 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,6 +23,6 @@ jobs: npm run bootstrap npm run build --if-present npm test - npm run tslint + npm run lint env: CI: true diff --git a/.gitignore b/.gitignore index f805185c75..359d346dea 100644 --- a/.gitignore +++ b/.gitignore @@ -10,8 +10,8 @@ yarn.lock google-generated-credentials.json _START_PACKAGE .env -.vscode +.vscode/* +!.vscode/extensions.json .idea -.prettierrc.js vetur.config.js nodelinter.config.json diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000000..ebf28d8091 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,51 @@ +module.exports = { + /** + * https://prettier.io/docs/en/options.html#semicolons + */ + semi: true, + + /** + * https://prettier.io/docs/en/options.html#trailing-commas + */ + trailingComma: 'all', + + /** + * https://prettier.io/docs/en/options.html#bracket-spacing + */ + bracketSpacing: true, + + /** + * https://prettier.io/docs/en/options.html#tabs + */ + useTabs: true, + + /** + * https://prettier.io/docs/en/options.html#tab-width + */ + tabWidth: 2, + + /** + * https://prettier.io/docs/en/options.html#arrow-function-parentheses + */ + arrowParens: 'always', + + /** + * https://prettier.io/docs/en/options.html#quotes + */ + singleQuote: true, + + /** + * https://prettier.io/docs/en/options.html#quote-props + */ + quoteProps: 'as-needed', + + /** + * https://prettier.io/docs/en/options.html#end-of-line + */ + endOfLine: 'lf', + + /** + * https://prettier.io/docs/en/options.html#print-width + */ + printWidth: 100, +}; diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000..98ca38d356 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint", + "EditorConfig.EditorConfig", + "esbenp.prettier-vscode", + "octref.vetur" + ] +} diff --git a/package.json b/package.json index 8855d43d8f..802fa4f791 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,14 @@ "build": "lerna exec npm run build", "dev": "lerna exec npm run dev --parallel", "clean:dist": "lerna exec -- rimraf ./dist", + "format": "lerna exec npm run format", + "lint": "lerna exec npm run lint", + "lintfix": "lerna exec npm run lintfix", "optimize-svg": "find ./packages -name '*.svg' ! -name 'pipedrive.svg' -print0 | xargs -0 -P16 -L20 npx svgo", "start": "run-script-os", "start:default": "cd packages/cli/bin && ./n8n", "start:windows": "cd packages/cli/bin && n8n", "test": "lerna run test", - "tslint": "lerna exec npm run tslint", "watch": "lerna run --parallel watch", "webhook": "./packages/cli/bin/n8n webhook", "worker": "./packages/cli/bin/n8n worker" diff --git a/packages/cli/commands/Interfaces.d.ts b/packages/cli/commands/Interfaces.d.ts index adc41c6225..c47f1680e5 100644 --- a/packages/cli/commands/Interfaces.d.ts +++ b/packages/cli/commands/Interfaces.d.ts @@ -1,14 +1,14 @@ interface IResult { totalWorkflows: number; summary: { - failedExecutions: number, - successfulExecutions: number, - warningExecutions: number, - errors: IExecutionError[], - warnings: IExecutionError[], + failedExecutions: number; + successfulExecutions: number; + warningExecutions: number; + errors: IExecutionError[]; + warnings: IExecutionError[]; }; coveredNodes: { - [nodeType: string]: number + [nodeType: string]: number; }; executions: IExecutionResult[]; } @@ -21,7 +21,7 @@ interface IExecutionResult { error?: string; changes?: string; coveredNodes: { - [nodeType: string]: number + [nodeType: string]: number; }; } diff --git a/packages/cli/commands/execute.ts b/packages/cli/commands/execute.ts index 662d372023..374c753e99 100644 --- a/packages/cli/commands/execute.ts +++ b/packages/cli/commands/execute.ts @@ -1,11 +1,9 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable no-console */ import { promises as fs } from 'fs'; import { Command, flags } from '@oclif/command'; -import { - UserSettings, -} from 'n8n-core'; -import { - INode, -} from 'n8n-workflow'; +import { UserSettings } from 'n8n-core'; +import { INode, LoggerProxy } from 'n8n-workflow'; import { ActiveExecutions, @@ -17,26 +15,18 @@ import { IWorkflowExecutionDataProcess, LoadNodesAndCredentials, NodeTypes, + // eslint-disable-next-line @typescript-eslint/no-unused-vars WorkflowCredentials, WorkflowHelpers, WorkflowRunner, } from '../src'; -import { - getLogger, -} from '../src/Logger'; - -import { - LoggerProxy, -} from 'n8n-workflow'; +import { getLogger } from '../src/Logger'; export class Execute extends Command { static description = '\nExecutes a given workflow'; - static examples = [ - `$ n8n execute --id=5`, - `$ n8n execute --file=workflow.json`, - ]; + static examples = [`$ n8n execute --id=5`, `$ n8n execute --file=workflow.json`]; static flags = { help: flags.help({ char: 'h' }), @@ -51,11 +41,12 @@ export class Execute extends Command { }), }; - + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async run() { const logger = getLogger(); LoggerProxy.init(logger); + // eslint-disable-next-line @typescript-eslint/no-shadow const { flags } = this.parse(Execute); // Start directly with the init of the database to improve startup time @@ -76,12 +67,14 @@ export class Execute extends Command { } let workflowId: string | undefined; - let workflowData: IWorkflowBase | undefined = undefined; + let workflowData: IWorkflowBase | undefined; if (flags.file) { // Path to workflow is given try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment workflowData = JSON.parse(await fs.readFile(flags.file, 'utf8')); } catch (error) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (error.code === 'ENOENT') { console.info(`The file "${flags.file}" could not be found.`); return; @@ -92,10 +85,15 @@ export class Execute extends Command { // Do a basic check if the data in the file looks right // TODO: Later check with the help of TypeScript data if it is valid or not - if (workflowData === undefined || workflowData.nodes === undefined || workflowData.connections === undefined) { + if ( + workflowData === undefined || + workflowData.nodes === undefined || + workflowData.connections === undefined + ) { console.info(`The file "${flags.file}" does not contain valid workflow data.`); return; } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion workflowId = workflowData.id!.toString(); } @@ -105,7 +103,8 @@ export class Execute extends Command { if (flags.id) { // Id of workflow is given workflowId = flags.id; - workflowData = await Db.collections!.Workflow!.findOne(workflowId); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + workflowData = await Db.collections.Workflow!.findOne(workflowId); if (workflowData === undefined) { console.info(`The workflow with the id "${workflowId}" does not exist.`); process.exit(1); @@ -139,7 +138,8 @@ export class Execute extends Command { // Check if the workflow contains the required "Start" node // "requiredNodeTypes" are also defined in editor-ui/views/NodeView.vue const requiredNodeTypes = ['n8n-nodes-base.start']; - let startNode: INode | undefined = undefined; + let startNode: INode | undefined; + // eslint-disable-next-line no-restricted-syntax, @typescript-eslint/no-non-null-assertion for (const node of workflowData!.nodes) { if (requiredNodeTypes.includes(node.type)) { startNode = node; @@ -151,6 +151,7 @@ export class Execute extends Command { // If the workflow does not contain a start-node we can not know what // should be executed and with which data to start. console.info(`The workflow does not contain a "Start" node. So it can not be executed.`); + // eslint-disable-next-line consistent-return return Promise.resolve(); } @@ -158,6 +159,7 @@ export class Execute extends Command { const runData: IWorkflowExecutionDataProcess = { executionMode: 'cli', startNodes: [startNode.name], + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion workflowData: workflowData!, }; @@ -178,6 +180,7 @@ export class Execute extends Command { logger.info(JSON.stringify(data, null, 2)); const { error } = data.data.resultData; + // eslint-disable-next-line @typescript-eslint/no-throw-literal throw { ...error, stack: error.stack, diff --git a/packages/cli/commands/executeBatch.ts b/packages/cli/commands/executeBatch.ts index b1022c6561..1abcd916a5 100644 --- a/packages/cli/commands/executeBatch.ts +++ b/packages/cli/commands/executeBatch.ts @@ -1,18 +1,26 @@ +/* eslint-disable @typescript-eslint/prefer-optional-chain */ +/* eslint-disable array-callback-return */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable no-await-in-loop */ +/* eslint-disable no-async-promise-executor */ +/* eslint-disable no-param-reassign */ +/* eslint-disable @typescript-eslint/unbound-method */ +/* eslint-disable no-console */ import * as fs from 'fs'; -import { - Command, - flags, -} from '@oclif/command'; +import { Command, flags } from '@oclif/command'; -import { - UserSettings, -} from 'n8n-core'; +import { UserSettings } from 'n8n-core'; -import { - INode, - INodeExecutionData, - ITaskData, -} from 'n8n-workflow'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { INode, INodeExecutionData, ITaskData, LoggerProxy } from 'n8n-workflow'; + +import { sep } from 'path'; + +import { diff } from 'json-diff'; + +// eslint-disable-next-line import/no-extraneous-dependencies +import { pick } from 'lodash'; +import { getLogger } from '../src/Logger'; import { ActiveExecutions, @@ -20,35 +28,17 @@ import { CredentialTypes, Db, ExternalHooks, + // eslint-disable-next-line @typescript-eslint/no-unused-vars IExecutionsCurrentSummary, IWorkflowDb, IWorkflowExecutionDataProcess, LoadNodesAndCredentials, NodeTypes, + // eslint-disable-next-line @typescript-eslint/no-unused-vars WorkflowCredentials, WorkflowRunner, } from '../src'; -import { - sep, -} from 'path'; - -import { - diff, -} from 'json-diff'; - -import { - getLogger, -} from '../src/Logger'; - -import { - LoggerProxy, -} from 'n8n-workflow'; - -import { - pick, -} from 'lodash'; - export class ExecuteBatch extends Command { static description = '\nExecutes multiple workflows once'; @@ -87,19 +77,24 @@ export class ExecuteBatch extends Command { }), concurrency: flags.integer({ default: 1, - description: 'How many workflows can run in parallel. Defaults to 1 which means no concurrency.', + description: + 'How many workflows can run in parallel. Defaults to 1 which means no concurrency.', }), output: flags.string({ - description: 'Enable execution saving, You must inform an existing folder to save execution via this param', + description: + 'Enable execution saving, You must inform an existing folder to save execution via this param', }), snapshot: flags.string({ - description: 'Enables snapshot saving. You must inform an existing folder to save snapshots via this param.', + description: + 'Enables snapshot saving. You must inform an existing folder to save snapshots via this param.', }), compare: flags.string({ - description: 'Compares current execution with an existing snapshot. You must inform an existing folder where the snapshots are saved.', + description: + 'Compares current execution with an existing snapshot. You must inform an existing folder where the snapshots are saved.', }), shallow: flags.boolean({ - description: 'Compares only if attributes output from node are the same, with no regards to neste JSON objects.', + description: + 'Compares only if attributes output from node are the same, with no regards to neste JSON objects.', }), skipList: flags.string({ description: 'File containing a comma separated list of workflow IDs to skip.', @@ -117,15 +112,16 @@ export class ExecuteBatch extends Command { * Gracefully handles exit. * @param {boolean} skipExit Whether to skip exit or number according to received signal */ + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types static async stopProcess(skipExit: boolean | number = false) { - - if (ExecuteBatch.cancelled === true) { + if (ExecuteBatch.cancelled) { process.exit(0); } ExecuteBatch.cancelled = true; const activeExecutionsInstance = ActiveExecutions.getInstance(); - const stopPromises = activeExecutionsInstance.getActiveExecutions().map(async execution => { + const stopPromises = activeExecutionsInstance.getActiveExecutions().map(async (execution) => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises activeExecutionsInstance.stopExecution(execution.id); }); @@ -135,16 +131,17 @@ export class ExecuteBatch extends Command { process.exit(0); }, 30000); - let executingWorkflows = activeExecutionsInstance.getActiveExecutions() as IExecutionsCurrentSummary[]; + 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...`); - executingWorkflows.map(execution => { + executingWorkflows.map((execution) => { console.log(` - Execution ID ${execution.id}, workflow ID: ${execution.workflowId}`); }); } + // eslint-disable-next-line no-await-in-loop await new Promise((resolve) => { setTimeout(resolve, 500); }); @@ -157,12 +154,13 @@ export class ExecuteBatch extends Command { } } + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types formatJsonOutput(data: object) { return JSON.stringify(data, null, 2); } + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types shouldBeConsideredAsWarning(errorMessage: string) { - const warningStrings = [ 'refresh token is invalid', 'unable to connect to', @@ -174,6 +172,7 @@ export class ExecuteBatch extends Command { 'request timed out', ]; + // eslint-disable-next-line no-param-reassign errorMessage = errorMessage.toLowerCase(); for (let i = 0; i < warningStrings.length; i++) { @@ -185,18 +184,18 @@ export class ExecuteBatch extends Command { return false; } - + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async run() { - process.on('SIGTERM', ExecuteBatch.stopProcess); process.on('SIGINT', ExecuteBatch.stopProcess); const logger = getLogger(); LoggerProxy.init(logger); + // eslint-disable-next-line @typescript-eslint/no-shadow const { flags } = this.parse(ExecuteBatch); - ExecuteBatch.debug = flags.debug === true; + ExecuteBatch.debug = flags.debug; ExecuteBatch.concurrency = flags.concurrency || 1; const ids: number[] = []; @@ -241,7 +240,7 @@ export class ExecuteBatch extends Command { if (flags.ids !== undefined) { const paramIds = flags.ids.split(','); const re = /\d+/; - const matchedIds = paramIds.filter(id => id.match(re)).map(id => parseInt(id.trim(), 10)); + const matchedIds = paramIds.filter((id) => re.exec(id)).map((id) => parseInt(id.trim(), 10)); if (matchedIds.length === 0) { console.log(`The parameter --ids must be a list of numeric IDs separated by a comma.`); @@ -254,18 +253,17 @@ export class ExecuteBatch extends Command { if (flags.skipList !== undefined) { if (fs.existsSync(flags.skipList)) { const contents = fs.readFileSync(flags.skipList, { encoding: 'utf-8' }); - skipIds.push(...contents.split(',').map(id => parseInt(id.trim(), 10))); + skipIds.push(...contents.split(',').map((id) => parseInt(id.trim(), 10))); } else { console.log('Skip list file not found. Exiting.'); return; } } - if (flags.shallow === true) { + if (flags.shallow) { ExecuteBatch.shallow = true; } - // Start directly with the init of the database to improve startup time const startDbInitPromise = Db.init(); @@ -281,7 +279,7 @@ export class ExecuteBatch extends Command { let allWorkflows; - const query = Db.collections!.Workflow!.createQueryBuilder('workflows'); + const query = Db.collections.Workflow!.createQueryBuilder('workflows'); if (ids.length > 0) { query.andWhere(`workflows.id in (:...ids)`, { ids }); @@ -291,9 +289,10 @@ export class ExecuteBatch extends Command { query.andWhere(`workflows.id not in (:...skipIds)`, { skipIds }); } - allWorkflows = await query.getMany() as IWorkflowDb[]; + // eslint-disable-next-line prefer-const + allWorkflows = (await query.getMany()) as IWorkflowDb[]; - if (ExecuteBatch.debug === true) { + if (ExecuteBatch.debug) { process.stdout.write(`Found ${allWorkflows.length} workflows to execute.\n`); } @@ -318,12 +317,19 @@ export class ExecuteBatch extends Command { let { retries } = flags; - while (retries > 0 && (results.summary.warningExecutions + results.summary.failedExecutions > 0) && ExecuteBatch.cancelled === false) { - const failedWorkflowIds = results.summary.errors.map(execution => execution.workflowId); - failedWorkflowIds.push(...results.summary.warnings.map(execution => execution.workflowId)); + while ( + retries > 0 && + results.summary.warningExecutions + results.summary.failedExecutions > 0 && + !ExecuteBatch.cancelled + ) { + const failedWorkflowIds = results.summary.errors.map((execution) => execution.workflowId); + failedWorkflowIds.push(...results.summary.warnings.map((execution) => execution.workflowId)); - const newWorkflowList = allWorkflows.filter(workflow => failedWorkflowIds.includes(workflow.id)); + const newWorkflowList = allWorkflows.filter((workflow) => + failedWorkflowIds.includes(workflow.id), + ); + // eslint-disable-next-line no-await-in-loop const retryResults = await this.runTests(newWorkflowList); this.mergeResults(results, retryResults); @@ -343,12 +349,17 @@ export class ExecuteBatch extends Command { console.log(`\t${nodeName}: ${nodeCount}`); }); console.log('\nCheck the JSON file for more details.'); + } else if (flags.shortOutput) { + console.log( + this.formatJsonOutput({ + ...results, + executions: results.executions.filter( + (execution) => execution.executionStatus !== 'success', + ), + }), + ); } else { - if (flags.shortOutput === true) { - console.log(this.formatJsonOutput({ ...results, executions: results.executions.filter(execution => execution.executionStatus !== 'success') })); - } else { - console.log(this.formatJsonOutput(results)); - } + console.log(this.formatJsonOutput(results)); } await ExecuteBatch.stopProcess(true); @@ -357,23 +368,26 @@ export class ExecuteBatch extends Command { this.exit(1); } this.exit(0); - } + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types mergeResults(results: IResult, retryResults: IResult) { - if (retryResults.summary.successfulExecutions === 0) { // Nothing to replace. return; } // Find successful executions and replace them on previous result. - retryResults.executions.forEach(newExecution => { + retryResults.executions.forEach((newExecution) => { if (newExecution.executionStatus === 'success') { // Remove previous execution from list. - results.executions = results.executions.filter(previousExecutions => previousExecutions.workflowId !== newExecution.workflowId); + results.executions = results.executions.filter( + (previousExecutions) => previousExecutions.workflowId !== newExecution.workflowId, + ); - const errorIndex = results.summary.errors.findIndex(summaryInformation => summaryInformation.workflowId === newExecution.workflowId); + const errorIndex = results.summary.errors.findIndex( + (summaryInformation) => summaryInformation.workflowId === newExecution.workflowId, + ); if (errorIndex !== -1) { // This workflow errored previously. Decrement error count. results.summary.failedExecutions--; @@ -381,7 +395,9 @@ export class ExecuteBatch extends Command { results.summary.errors.splice(errorIndex, 1); } - const warningIndex = results.summary.warnings.findIndex(summaryInformation => summaryInformation.workflowId === newExecution.workflowId); + const warningIndex = results.summary.warnings.findIndex( + (summaryInformation) => summaryInformation.workflowId === newExecution.workflowId, + ); if (warningIndex !== -1) { // This workflow errored previously. Decrement error count. results.summary.warningExecutions--; @@ -420,7 +436,7 @@ export class ExecuteBatch extends Command { let workflow: IWorkflowDb | undefined; while (allWorkflows.length > 0) { workflow = allWorkflows.shift(); - if (ExecuteBatch.cancelled === true) { + if (ExecuteBatch.cancelled) { process.stdout.write(`Thread ${i + 1} resolving and quitting.`); resolve(true); break; @@ -440,6 +456,7 @@ export class ExecuteBatch extends Command { this.updateStatus(); } + // eslint-disable-next-line @typescript-eslint/no-loop-func await this.startThread(workflow).then((executionResult) => { if (ExecuteBatch.debug) { ExecuteBatch.workflowExecutionsProgress[i].pop(); @@ -456,7 +473,7 @@ export class ExecuteBatch extends Command { result.summary.successfulExecutions++; const nodeNames = Object.keys(executionResult.coveredNodes); - nodeNames.map(nodeName => { + nodeNames.map((nodeName) => { if (result.coveredNodes[nodeName] === undefined) { result.coveredNodes[nodeName] = 0; } @@ -506,19 +523,18 @@ export class ExecuteBatch extends Command { }); } + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types updateStatus() { - - if (ExecuteBatch.cancelled === true) { + if (ExecuteBatch.cancelled) { return; } - if (process.stdout.isTTY === true) { - process.stdout.moveCursor(0, - (ExecuteBatch.concurrency)); + if (process.stdout.isTTY) { + process.stdout.moveCursor(0, -ExecuteBatch.concurrency); process.stdout.cursorTo(0); process.stdout.clearLine(0); } - ExecuteBatch.workflowExecutionsProgress.map((concurrentThread, index) => { let message = `${index + 1}: `; concurrentThread.map((executionItem, workflowIndex) => { @@ -537,16 +553,19 @@ export class ExecuteBatch extends Command { default: break; } - message += (workflowIndex > 0 ? ', ' : '') + `${openColor}${executionItem.workflowId}${closeColor}`; + message += `${workflowIndex > 0 ? ', ' : ''}${openColor}${ + executionItem.workflowId + }${closeColor}`; }); - if (process.stdout.isTTY === true) { + if (process.stdout.isTTY) { process.stdout.cursorTo(0); process.stdout.clearLine(0); } - process.stdout.write(message + '\n'); + process.stdout.write(`${message}\n`); }); } + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types initializeLogs() { process.stdout.write('**********************************************\n'); process.stdout.write(' n8n test workflows\n'); @@ -560,7 +579,7 @@ export class ExecuteBatch extends Command { } } - startThread(workflowData: IWorkflowDb): Promise { + async startThread(workflowData: IWorkflowDb): Promise { // This will be the object returned by the promise. // It will be updated according to execution progress below. const executionResult: IExecutionResult = { @@ -572,10 +591,9 @@ export class ExecuteBatch extends Command { coveredNodes: {}, }; - - const requiredNodeTypes = ['n8n-nodes-base.start']; - let startNode: INode | undefined = undefined; + let startNode: INode | undefined; + // eslint-disable-next-line no-restricted-syntax for (const node of workflowData.nodes) { if (requiredNodeTypes.includes(node.type)) { startNode = node; @@ -593,10 +611,10 @@ export class ExecuteBatch extends Command { // properties from the JSON object (useful for optional properties that can // cause the comparison to detect changes when not true). const nodeEdgeCases = {} as INodeSpecialCases; - workflowData.nodes.forEach(node => { + workflowData.nodes.forEach((node) => { executionResult.coveredNodes[node.type] = (executionResult.coveredNodes[node.type] || 0) + 1; if (node.notes !== undefined && node.notes !== '') { - node.notes.split('\n').forEach(note => { + node.notes.split('\n').forEach((note) => { const parts = note.split('='); if (parts.length === 2) { if (nodeEdgeCases[node.name] === undefined) { @@ -605,9 +623,13 @@ export class ExecuteBatch extends Command { if (parts[0] === 'CAP_RESULTS_LENGTH') { nodeEdgeCases[node.name].capResults = parseInt(parts[1], 10); } else if (parts[0] === 'IGNORED_PROPERTIES') { - nodeEdgeCases[node.name].ignoredProperties = parts[1].split(',').map(property => property.trim()); + nodeEdgeCases[node.name].ignoredProperties = parts[1] + .split(',') + .map((property) => property.trim()); } else if (parts[0] === 'KEEP_ONLY_PROPERTIES') { - nodeEdgeCases[node.name].keepOnlyProperties = parts[1].split(',').map(property => property.trim()); + nodeEdgeCases[node.name].keepOnlyProperties = parts[1] + .split(',') + .map((property) => property.trim()); } } }); @@ -633,13 +655,11 @@ export class ExecuteBatch extends Command { resolve(executionResult); }, ExecuteBatch.executionTimeout); - try { - const runData: IWorkflowExecutionDataProcess = { executionMode: 'cli', startNodes: [startNode!.name], - workflowData: workflowData!, + workflowData, }; const workflowRunner = new WorkflowRunner(); @@ -647,7 +667,7 @@ export class ExecuteBatch extends Command { const activeExecutions = ActiveExecutions.getInstance(); const data = await activeExecutions.getPostExecutePromise(executionId); - if (gotCancel || ExecuteBatch.cancelled === true) { + if (gotCancel || ExecuteBatch.cancelled) { clearTimeout(timeoutTimer); // The promise was settled already so we simply ignore. return; @@ -657,14 +677,18 @@ export class ExecuteBatch extends Command { executionResult.error = 'Workflow did not return any data.'; executionResult.executionStatus = 'error'; } else { - executionResult.executionTime = (Date.parse(data.stoppedAt as unknown as string) - Date.parse(data.startedAt as unknown as string)) / 1000; - executionResult.finished = (data?.finished !== undefined) as boolean; + executionResult.executionTime = + (Date.parse(data.stoppedAt as unknown as string) - + Date.parse(data.startedAt as unknown as string)) / + 1000; + executionResult.finished = data?.finished !== undefined; if (data.data.resultData.error) { - executionResult.error = - data.data.resultData.error.hasOwnProperty('description') ? - // @ts-ignore - data.data.resultData.error.description : data.data.resultData.error.message; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, no-prototype-builtins + executionResult.error = data.data.resultData.error.hasOwnProperty('description') + ? // @ts-ignore + data.data.resultData.error.description + : data.data.resultData.error.message; if (data.data.resultData.lastNodeExecuted !== undefined) { executionResult.error += ` on node ${data.data.resultData.lastNodeExecuted}`; } @@ -674,7 +698,7 @@ export class ExecuteBatch extends Command { executionResult.executionStatus = 'warning'; } } else { - if (ExecuteBatch.shallow === true) { + if (ExecuteBatch.shallow) { // What this does is guarantee that top-level attributes // from the JSON are kept and the are the same type. @@ -688,34 +712,48 @@ export class ExecuteBatch extends Command { if (taskData.data === undefined) { return; } - Object.keys(taskData.data).map(connectionName => { - const connection = taskData.data![connectionName] as Array; - connection.map(executionDataArray => { + Object.keys(taskData.data).map((connectionName) => { + const connection = taskData.data![connectionName]; + connection.map((executionDataArray) => { if (executionDataArray === null) { return; } - if (nodeEdgeCases[nodeName] !== undefined && nodeEdgeCases[nodeName].capResults !== undefined) { + if ( + nodeEdgeCases[nodeName] !== undefined && + nodeEdgeCases[nodeName].capResults !== undefined + ) { executionDataArray.splice(nodeEdgeCases[nodeName].capResults!); } - executionDataArray.map(executionData => { + executionDataArray.map((executionData) => { if (executionData.json === undefined) { return; } - if (nodeEdgeCases[nodeName] !== undefined && nodeEdgeCases[nodeName].ignoredProperties !== undefined) { - nodeEdgeCases[nodeName].ignoredProperties!.forEach(ignoredProperty => delete executionData.json[ignoredProperty]); + if ( + nodeEdgeCases[nodeName] !== undefined && + nodeEdgeCases[nodeName].ignoredProperties !== undefined + ) { + nodeEdgeCases[nodeName].ignoredProperties!.forEach( + (ignoredProperty) => delete executionData.json[ignoredProperty], + ); } let keepOnlyFields = [] as string[]; - if (nodeEdgeCases[nodeName] !== undefined && nodeEdgeCases[nodeName].keepOnlyProperties !== undefined) { + if ( + nodeEdgeCases[nodeName] !== undefined && + nodeEdgeCases[nodeName].keepOnlyProperties !== undefined + ) { keepOnlyFields = nodeEdgeCases[nodeName].keepOnlyProperties!; } - executionData.json = keepOnlyFields.length > 0 ? pick(executionData.json, keepOnlyFields) : executionData.json; + executionData.json = + keepOnlyFields.length > 0 + ? pick(executionData.json, keepOnlyFields) + : executionData.json; const jsonProperties = executionData.json; const nodeOutputAttributes = Object.keys(jsonProperties); - nodeOutputAttributes.map(attributeName => { + nodeOutputAttributes.map((attributeName) => { if (Array.isArray(jsonProperties[attributeName])) { jsonProperties[attributeName] = ['json array']; } else if (typeof jsonProperties[attributeName] === 'object') { @@ -724,7 +762,6 @@ export class ExecuteBatch extends Command { }); }); }); - }); }); }); @@ -732,14 +769,14 @@ export class ExecuteBatch extends Command { // If not using shallow comparison then we only treat nodeEdgeCases. const specialCases = Object.keys(nodeEdgeCases); - specialCases.forEach(nodeName => { + specialCases.forEach((nodeName) => { data.data.resultData.runData[nodeName].map((taskData: ITaskData) => { if (taskData.data === undefined) { return; } - Object.keys(taskData.data).map(connectionName => { - const connection = taskData.data![connectionName] as Array; - connection.map(executionDataArray => { + Object.keys(taskData.data).map((connectionName) => { + const connection = taskData.data![connectionName]; + connection.map((executionDataArray) => { if (executionDataArray === null) { return; } @@ -749,15 +786,16 @@ export class ExecuteBatch extends Command { } if (nodeEdgeCases[nodeName].ignoredProperties !== undefined) { - executionDataArray.map(executionData => { + executionDataArray.map((executionData) => { if (executionData.json === undefined) { return; } - nodeEdgeCases[nodeName].ignoredProperties!.forEach(ignoredProperty => delete executionData.json[ignoredProperty]); + nodeEdgeCases[nodeName].ignoredProperties!.forEach( + (ignoredProperty) => delete executionData.json[ignoredProperty], + ); }); } }); - }); }); }); @@ -767,9 +805,12 @@ export class ExecuteBatch extends Command { if (ExecuteBatch.compare === undefined) { executionResult.executionStatus = 'success'; } else { - const fileName = (ExecuteBatch.compare.endsWith(sep) ? ExecuteBatch.compare : ExecuteBatch.compare + sep) + `${workflowData.id}-snapshot.json`; - if (fs.existsSync(fileName) === true) { - + const fileName = `${ + ExecuteBatch.compare.endsWith(sep) + ? ExecuteBatch.compare + : ExecuteBatch.compare + sep + }${workflowData.id}-snapshot.json`; + if (fs.existsSync(fileName)) { const contents = fs.readFileSync(fileName, { encoding: 'utf-8' }); const changes = diff(JSON.parse(contents), data, { keysOnly: true }); @@ -790,7 +831,11 @@ export class ExecuteBatch extends Command { // Save snapshots only after comparing - this is to make sure we're updating // After comparing to existing verion. if (ExecuteBatch.snapshot !== undefined) { - const fileName = (ExecuteBatch.snapshot.endsWith(sep) ? ExecuteBatch.snapshot : ExecuteBatch.snapshot + sep) + `${workflowData.id}-snapshot.json`; + const fileName = `${ + ExecuteBatch.snapshot.endsWith(sep) + ? ExecuteBatch.snapshot + : ExecuteBatch.snapshot + sep + }${workflowData.id}-snapshot.json`; fs.writeFileSync(fileName, serializedData); } } @@ -803,5 +848,4 @@ export class ExecuteBatch extends Command { resolve(executionResult); }); } - } diff --git a/packages/cli/commands/export/credentials.ts b/packages/cli/commands/export/credentials.ts index de1aafe6dd..9ee488fe0e 100644 --- a/packages/cli/commands/export/credentials.ts +++ b/packages/cli/commands/export/credentials.ts @@ -1,32 +1,16 @@ -import { - Command, - flags, -} from '@oclif/command'; +/* eslint-disable @typescript-eslint/restrict-plus-operands */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable no-console */ +import { Command, flags } from '@oclif/command'; -import { - Credentials, - UserSettings, -} from 'n8n-core'; +import { Credentials, UserSettings } from 'n8n-core'; -import { - IDataObject -} from 'n8n-workflow'; - -import { - Db, - ICredentialsDecryptedDb, -} from '../../src'; - -import { - getLogger, -} from '../../src/Logger'; - -import { - LoggerProxy, -} from 'n8n-workflow'; +import { IDataObject, LoggerProxy } from 'n8n-workflow'; import * as fs from 'fs'; import * as path from 'path'; +import { getLogger } from '../../src/Logger'; +import { Db, ICredentialsDecryptedDb } from '../../src'; export class ExportCredentialsCommand extends Command { static description = 'Export credentials'; @@ -45,7 +29,8 @@ export class ExportCredentialsCommand extends Command { description: 'Export all credentials', }), backup: flags.boolean({ - description: 'Sets --all --pretty --separate for simple backups. Only --output has to be set additionally.', + description: + 'Sets --all --pretty --separate for simple backups. Only --output has to be set additionally.', }), id: flags.string({ description: 'The ID of the credential to export', @@ -58,19 +43,23 @@ export class ExportCredentialsCommand extends Command { description: 'Format the output in an easier to read fashion', }), separate: flags.boolean({ - description: 'Exports one file per credential (useful for versioning). Must inform a directory via --output.', + description: + 'Exports one file per credential (useful for versioning). Must inform a directory via --output.', }), decrypted: flags.boolean({ - description: 'Exports data decrypted / in plain text. ALL SENSITIVE INFORMATION WILL BE VISIBLE IN THE FILES. Use to migrate from a installation to another that have a different secret key (in the config file).', + description: + 'Exports data decrypted / in plain text. ALL SENSITIVE INFORMATION WILL BE VISIBLE IN THE FILES. Use to migrate from a installation to another that have a different secret key (in the config file).', }), }; + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async run() { const logger = getLogger(); LoggerProxy.init(logger); + // eslint-disable-next-line @typescript-eslint/no-shadow const { flags } = this.parse(ExportCredentialsCommand); - + if (flags.backup) { flags.all = true; flags.pretty = true; @@ -103,7 +92,9 @@ export class ExportCredentialsCommand extends Command { fs.mkdirSync(flags.output, { recursive: true }); } } catch (e) { - console.error('Aborting execution as a filesystem error has been encountered while creating the output directory. See log messages for details.'); + console.error( + 'Aborting execution as a filesystem error has been encountered while creating the output directory. See log messages for details.', + ); logger.error('\nFILESYSTEM ERROR'); logger.info('===================================='); logger.error(e.message); @@ -127,6 +118,7 @@ export class ExportCredentialsCommand extends Command { findQuery.id = flags.id; } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const credentials = await Db.collections.Credentials!.find(findQuery); if (flags.decrypted) { @@ -148,17 +140,22 @@ export class ExportCredentialsCommand extends Command { } if (flags.separate) { - let fileContents: string, i: number; + let fileContents: string; + let i: number; for (i = 0; i < credentials.length; i++) { fileContents = JSON.stringify(credentials[i], null, flags.pretty ? 2 : undefined); - const filename = (flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) + credentials[i].id + '.json'; + const filename = `${ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) + + credentials[i].id + }.json`; fs.writeFileSync(filename, fileContents); } console.info(`Successfully exported ${i} credentials.`); } else { const fileContents = JSON.stringify(credentials, null, flags.pretty ? 2 : undefined); if (flags.output) { - fs.writeFileSync(flags.output!, fileContents); + fs.writeFileSync(flags.output, fileContents); console.info(`Successfully exported ${credentials.length} credentials.`); } else { console.info(fileContents); diff --git a/packages/cli/commands/export/workflow.ts b/packages/cli/commands/export/workflow.ts index 9d478dbaef..c9591e595b 100644 --- a/packages/cli/commands/export/workflow.ts +++ b/packages/cli/commands/export/workflow.ts @@ -1,26 +1,13 @@ -import { - Command, - flags, -} from '@oclif/command'; +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable no-console */ +import { Command, flags } from '@oclif/command'; -import { - IDataObject -} from 'n8n-workflow'; - -import { - Db, -} from '../../src'; - -import { - getLogger, -} from '../../src/Logger'; - -import { - LoggerProxy, -} from 'n8n-workflow'; +import { IDataObject, LoggerProxy } from 'n8n-workflow'; import * as fs from 'fs'; import * as path from 'path'; +import { getLogger } from '../../src/Logger'; +import { Db } from '../../src'; export class ExportWorkflowsCommand extends Command { static description = 'Export workflows'; @@ -38,7 +25,8 @@ export class ExportWorkflowsCommand extends Command { description: 'Export all workflows', }), backup: flags.boolean({ - description: 'Sets --all --pretty --separate for simple backups. Only --output has to be set additionally.', + description: + 'Sets --all --pretty --separate for simple backups. Only --output has to be set additionally.', }), id: flags.string({ description: 'The ID of the workflow to export', @@ -51,14 +39,17 @@ export class ExportWorkflowsCommand extends Command { description: 'Format the output in an easier to read fashion', }), separate: flags.boolean({ - description: 'Exports one file per workflow (useful for versioning). Must inform a directory via --output.', + description: + 'Exports one file per workflow (useful for versioning). Must inform a directory via --output.', }), }; + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async run() { const logger = getLogger(); LoggerProxy.init(logger); + // eslint-disable-next-line @typescript-eslint/no-shadow const { flags } = this.parse(ExportWorkflowsCommand); if (flags.backup) { @@ -93,7 +84,9 @@ export class ExportWorkflowsCommand extends Command { fs.mkdirSync(flags.output, { recursive: true }); } } catch (e) { - console.error('Aborting execution as a filesystem error has been encountered while creating the output directory. See log messages for details.'); + console.error( + 'Aborting execution as a filesystem error has been encountered while creating the output directory. See log messages for details.', + ); logger.error('\nFILESYSTEM ERROR'); logger.info('===================================='); logger.error(e.message); @@ -117,6 +110,7 @@ export class ExportWorkflowsCommand extends Command { findQuery.id = flags.id; } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const workflows = await Db.collections.Workflow!.find(findQuery); if (workflows.length === 0) { @@ -124,18 +118,27 @@ export class ExportWorkflowsCommand extends Command { } if (flags.separate) { - let fileContents: string, i: number; + let fileContents: string; + let i: number; for (i = 0; i < workflows.length; i++) { fileContents = JSON.stringify(workflows[i], null, flags.pretty ? 2 : undefined); - const filename = (flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) + workflows[i].id + '.json'; + const filename = `${ + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands, @typescript-eslint/no-non-null-assertion + (flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) + + workflows[i].id + }.json`; fs.writeFileSync(filename, fileContents); } console.info(`Successfully exported ${i} workflows.`); } else { const fileContents = JSON.stringify(workflows, null, flags.pretty ? 2 : undefined); if (flags.output) { - fs.writeFileSync(flags.output!, fileContents); - console.info(`Successfully exported ${workflows.length} ${workflows.length === 1 ? 'workflow.' : 'workflows.'}`); + fs.writeFileSync(flags.output, fileContents); + console.info( + `Successfully exported ${workflows.length} ${ + workflows.length === 1 ? 'workflow.' : 'workflows.' + }`, + ); } else { console.info(fileContents); } diff --git a/packages/cli/commands/import/credentials.ts b/packages/cli/commands/import/credentials.ts index 38835ed7fd..01ce0e5a45 100644 --- a/packages/cli/commands/import/credentials.ts +++ b/packages/cli/commands/import/credentials.ts @@ -1,28 +1,16 @@ -import { - Command, - flags, -} from '@oclif/command'; +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable no-console */ +import { Command, flags } from '@oclif/command'; -import { - Credentials, - UserSettings, -} from 'n8n-core'; +import { Credentials, UserSettings } from 'n8n-core'; -import { - Db, -} from '../../src'; - -import { - getLogger, -} from '../../src/Logger'; - -import { - LoggerProxy, -} from 'n8n-workflow'; +import { LoggerProxy } from 'n8n-workflow'; import * as fs from 'fs'; import * as glob from 'fast-glob'; import * as path from 'path'; +import { getLogger } from '../../src/Logger'; +import { Db } from '../../src'; export class ImportCredentialsCommand extends Command { static description = 'Import credentials'; @@ -43,10 +31,12 @@ export class ImportCredentialsCommand extends Command { }), }; + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async run() { const logger = getLogger(); LoggerProxy.init(logger); + // eslint-disable-next-line @typescript-eslint/no-shadow const { flags } = this.parse(ImportCredentialsCommand); if (!flags.input) { @@ -76,18 +66,25 @@ export class ImportCredentialsCommand extends Command { } if (flags.separate) { - const files = await glob((flags.input.endsWith(path.sep) ? flags.input : flags.input + path.sep) + '*.json'); + const files = await glob( + `${flags.input.endsWith(path.sep) ? flags.input : flags.input + path.sep}*.json`, + ); for (i = 0; i < files.length; i++) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const credential = JSON.parse(fs.readFileSync(files[i], { encoding: 'utf8' })); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (typeof credential.data === 'object') { // plain data / decrypted input. Should be encrypted first. + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access Credentials.prototype.setData.call(credential, credential.data, encryptionKey); } + // eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-non-null-assertion await Db.collections.Credentials!.save(credential); } } else { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const fileContents = JSON.parse(fs.readFileSync(flags.input, { encoding: 'utf8' })); if (!Array.isArray(fileContents)) { @@ -97,8 +94,13 @@ export class ImportCredentialsCommand extends Command { for (i = 0; i < fileContents.length; i++) { if (typeof fileContents[i].data === 'object') { // plain data / decrypted input. Should be encrypted first. - Credentials.prototype.setData.call(fileContents[i], fileContents[i].data, encryptionKey); + Credentials.prototype.setData.call( + fileContents[i], + fileContents[i].data, + encryptionKey, + ); } + // eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-non-null-assertion await Db.collections.Credentials!.save(fileContents[i]); } } diff --git a/packages/cli/commands/import/workflow.ts b/packages/cli/commands/import/workflow.ts index 2d9a3ac136..7fb94c1039 100644 --- a/packages/cli/commands/import/workflow.ts +++ b/packages/cli/commands/import/workflow.ts @@ -1,26 +1,15 @@ -import { - Command, - flags, -} from '@oclif/command'; +/* eslint-disable no-console */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { Command, flags } from '@oclif/command'; -import { - Db, -} from '../../src'; - -import { - getLogger, -} from '../../src/Logger'; - -import { - LoggerProxy, -} from 'n8n-workflow'; +import { LoggerProxy } from 'n8n-workflow'; import * as fs from 'fs'; import * as glob from 'fast-glob'; import * as path from 'path'; -import { - UserSettings, -} from 'n8n-core'; +import { UserSettings } from 'n8n-core'; +import { getLogger } from '../../src/Logger'; +import { Db } from '../../src'; export class ImportWorkflowsCommand extends Command { static description = 'Import workflows'; @@ -41,10 +30,12 @@ export class ImportWorkflowsCommand extends Command { }), }; + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async run() { const logger = getLogger(); LoggerProxy.init(logger); + // eslint-disable-next-line @typescript-eslint/no-shadow const { flags } = this.parse(ImportWorkflowsCommand); if (!flags.input) { @@ -68,9 +59,12 @@ export class ImportWorkflowsCommand extends Command { await UserSettings.prepareUserSettings(); let i; if (flags.separate) { - const files = await glob((flags.input.endsWith(path.sep) ? flags.input : flags.input + path.sep) + '*.json'); + const files = await glob( + `${flags.input.endsWith(path.sep) ? flags.input : flags.input + path.sep}*.json`, + ); for (i = 0; i < files.length; i++) { const workflow = JSON.parse(fs.readFileSync(files[i], { encoding: 'utf8' })); + // eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-non-null-assertion await Db.collections.Workflow!.save(workflow); } } else { @@ -81,6 +75,7 @@ export class ImportWorkflowsCommand extends Command { } for (i = 0; i < fileContents.length; i++) { + // eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-non-null-assertion await Db.collections.Workflow!.save(fileContents[i]); } } @@ -89,6 +84,7 @@ export class ImportWorkflowsCommand extends Command { process.exit(0); } catch (error) { console.error('An error occurred while exporting workflows. See log messages for details.'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access logger.error(error.message); this.exit(1); } diff --git a/packages/cli/commands/list/workflow.ts b/packages/cli/commands/list/workflow.ts index 6fdca2e253..19075bef0f 100644 --- a/packages/cli/commands/list/workflow.ts +++ b/packages/cli/commands/list/workflow.ts @@ -1,16 +1,10 @@ -import { - Command, - flags, -} from '@oclif/command'; +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable no-console */ +import { Command, flags } from '@oclif/command'; -import { - IDataObject -} from 'n8n-workflow'; - -import { - Db, -} from "../../src"; +import { IDataObject } from 'n8n-workflow'; +import { Db } from '../../src'; export class ListWorkflowCommand extends Command { static description = '\nList workflows'; @@ -31,7 +25,9 @@ export class ListWorkflowCommand extends Command { }), }; + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async run() { + // eslint-disable-next-line @typescript-eslint/no-shadow const { flags } = this.parse(ListWorkflowCommand); if (flags.active !== undefined && !['true', 'false'].includes(flags.active)) { @@ -46,14 +42,13 @@ export class ListWorkflowCommand extends Command { findQuery.active = flags.active === 'true'; } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const workflows = await Db.collections.Workflow!.find(findQuery); if (flags.onlyId) { - workflows.forEach(workflow => console.log(workflow.id)); + workflows.forEach((workflow) => console.log(workflow.id)); } else { - workflows.forEach(workflow => console.log(workflow.id + "|" + workflow.name)); + workflows.forEach((workflow) => console.log(`${workflow.id}|${workflow.name}`)); } - - } catch (e) { console.error('\nGOT ERROR'); console.log('===================================='); diff --git a/packages/cli/commands/start.ts b/packages/cli/commands/start.ts index ea84064584..78550f7037 100644 --- a/packages/cli/commands/start.ts +++ b/packages/cli/commands/start.ts @@ -1,12 +1,17 @@ +/* eslint-disable @typescript-eslint/await-thenable */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/unbound-method */ +/* eslint-disable no-console */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import * as localtunnel from 'localtunnel'; -import { - TUNNEL_SUBDOMAIN_ENV, - UserSettings, -} from 'n8n-core'; +import { TUNNEL_SUBDOMAIN_ENV, UserSettings } from 'n8n-core'; import { Command, flags } from '@oclif/command'; -const open = require('open'); +// eslint-disable-next-line import/no-extraneous-dependencies import * as Redis from 'ioredis'; +import { IDataObject, LoggerProxy } from 'n8n-workflow'; import * as config from '../config'; import { ActiveExecutions, @@ -17,6 +22,7 @@ import { Db, ExternalHooks, GenericHelpers, + // eslint-disable-next-line @typescript-eslint/no-unused-vars IExecutionsCurrentSummary, LoadNodesAndCredentials, NodeTypes, @@ -24,15 +30,11 @@ import { TestWebhooks, WaitTracker, } from '../src'; -import { IDataObject } from 'n8n-workflow'; -import { - getLogger, -} from '../src/Logger'; +import { getLogger } from '../src/Logger'; -import { - LoggerProxy, -} from 'n8n-workflow'; +// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires +const open = require('open'); let activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner | undefined; let processExistCode = 0; @@ -54,29 +56,32 @@ export class Start extends Command { description: 'opens the UI automatically in browser', }), tunnel: flags.boolean({ - description: 'runs the webhooks via a hooks.n8n.cloud tunnel server. Use only for testing and development!', + description: + 'runs the webhooks via a hooks.n8n.cloud tunnel server. Use only for testing and development!', }), }; - /** * Opens the UI in browser */ + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types static openBrowser() { const editorUrl = GenericHelpers.getBaseUrl(); - open(editorUrl, { wait: true }) - .catch((error: Error) => { - console.log(`\nWas not able to open URL in browser. Please open manually by visiting:\n${editorUrl}\n`); - }); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + open(editorUrl, { wait: true }).catch((error: Error) => { + console.log( + `\nWas not able to open URL in browser. Please open manually by visiting:\n${editorUrl}\n`, + ); + }); } - /** * Stoppes the n8n in a graceful way. * Make for example sure that all the webhooks from third party services * get removed. */ + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types static async stopProcess() { getLogger().info('\nStopping n8n...'); @@ -90,10 +95,12 @@ export class Start extends Command { process.exit(processExistCode); }, 30000); - const skipWebhookDeregistration = config.get('endpoints.skipWebhoooksDeregistrationOnShutdown') as boolean; + const skipWebhookDeregistration = config.get( + 'endpoints.skipWebhoooksDeregistrationOnShutdown', + ) as boolean; const removePromises = []; - if (activeWorkflowRunner !== undefined && skipWebhookDeregistration !== true) { + if (activeWorkflowRunner !== undefined && !skipWebhookDeregistration) { removePromises.push(activeWorkflowRunner.removeAll()); } @@ -105,22 +112,23 @@ export class Start extends Command { // Wait for active workflow executions to finish const activeExecutionsInstance = ActiveExecutions.getInstance(); - let executingWorkflows = activeExecutionsInstance.getActiveExecutions() as IExecutionsCurrentSummary[]; + 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...`); - executingWorkflows.map(execution => { + // eslint-disable-next-line array-callback-return + executingWorkflows.map((execution) => { console.log(` - Execution ID ${execution.id}, workflow ID: ${execution.workflowId}`); }); } + // eslint-disable-next-line no-await-in-loop await new Promise((resolve) => { setTimeout(resolve, 500); }); executingWorkflows = activeExecutionsInstance.getActiveExecutions(); } - } catch (error) { console.error('There was an error shutting down n8n.', error); } @@ -128,12 +136,12 @@ export class Start extends Command { process.exit(processExistCode); } - async run() { // Make sure that n8n shuts down gracefully if possible process.on('SIGTERM', Start.stopProcess); process.on('SIGINT', Start.stopProcess); + // eslint-disable-next-line @typescript-eslint/no-shadow const { flags } = this.parse(Start); // Wrap that the process does not close but we can still use async @@ -144,7 +152,9 @@ export class Start extends Command { logger.info('Initializing n8n process'); // todo remove a few versions after release - logger.info('\nn8n now checks for new versions and security updates. You can turn this off using the environment variable N8N_VERSION_NOTIFICATIONS_ENABLED to "false"\nFor more information, please refer to https://docs.n8n.io/getting-started/installation/advanced/configuration.html\n'); + logger.info( + '\nn8n now checks for new versions and security updates. You can turn this off using the environment variable N8N_VERSION_NOTIFICATIONS_ENABLED to "false"\nFor more information, please refer to https://docs.n8n.io/getting-started/installation/advanced/configuration.html\n', + ); // Start directly with the init of the database to improve startup time const startDbInitPromise = Db.init().catch((error: Error) => { @@ -186,9 +196,11 @@ export class Start extends Command { const redisPort = config.get('queue.bull.redis.port'); const redisDB = config.get('queue.bull.redis.db'); const redisConnectionTimeoutLimit = config.get('queue.bull.redis.timeoutThreshold'); - let lastTimer = 0, cumulativeTimeout = 0; + let lastTimer = 0; + let cumulativeTimeout = 0; const settings = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars retryStrategy: (times: number): number | null => { const now = Date.now(); if (now - lastTimer > 30000) { @@ -199,7 +211,10 @@ export class Start extends Command { cumulativeTimeout += now - lastTimer; lastTimer = now; if (cumulativeTimeout > redisConnectionTimeoutLimit) { - logger.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + ". Exiting process."); + logger.error( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Unable to connect to Redis after ${redisConnectionTimeoutLimit}. Exiting process.`, + ); process.exit(1); } } @@ -235,20 +250,24 @@ export class Start extends Command { }); } - const dbType = await GenericHelpers.getConfigValue('database.type') as DatabaseType; + const dbType = (await GenericHelpers.getConfigValue('database.type')) as DatabaseType; if (dbType === 'sqlite') { const shouldRunVacuum = config.get('database.sqlite.executeVacuumOnStartup') as number; if (shouldRunVacuum) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises, @typescript-eslint/no-non-null-assertion await Db.collections.Execution!.query('VACUUM;'); } } - if (flags.tunnel === true) { + if (flags.tunnel) { this.log('\nWaiting for tunnel ...'); let tunnelSubdomain; - if (process.env[TUNNEL_SUBDOMAIN_ENV] !== undefined && process.env[TUNNEL_SUBDOMAIN_ENV] !== '') { + if ( + process.env[TUNNEL_SUBDOMAIN_ENV] !== undefined && + process.env[TUNNEL_SUBDOMAIN_ENV] !== '' + ) { tunnelSubdomain = process.env[TUNNEL_SUBDOMAIN_ENV]; } else if (userSettings.tunnelSubdomain !== undefined) { tunnelSubdomain = userSettings.tunnelSubdomain; @@ -257,9 +276,13 @@ export class Start extends Command { if (tunnelSubdomain === undefined) { // When no tunnel subdomain did exist yet create a new random one const availableCharacters = 'abcdefghijklmnopqrstuvwxyz0123456789'; - userSettings.tunnelSubdomain = Array.from({ length: 24 }).map(() => { - return availableCharacters.charAt(Math.floor(Math.random() * availableCharacters.length)); - }).join(''); + userSettings.tunnelSubdomain = Array.from({ length: 24 }) + .map(() => { + return availableCharacters.charAt( + Math.floor(Math.random() * availableCharacters.length), + ); + }) + .join(''); await UserSettings.writeUserSettings(userSettings); } @@ -269,14 +292,16 @@ export class Start extends Command { subdomain: tunnelSubdomain, }; - const port = config.get('port') as number; + const port = config.get('port'); // @ts-ignore const webhookTunnel = await localtunnel(port, tunnelSettings); - process.env.WEBHOOK_URL = webhookTunnel.url + '/'; + process.env.WEBHOOK_URL = `${webhookTunnel.url}/`; this.log(`Tunnel URL: ${process.env.WEBHOOK_URL}\n`); - this.log('IMPORTANT! Do not share with anybody as it would give people access to your n8n instance!'); + this.log( + 'IMPORTANT! Do not share with anybody as it would give people access to your n8n instance!', + ); } await Server.start(); @@ -285,6 +310,7 @@ export class Start extends Command { activeWorkflowRunner = ActiveWorkflowRunner.getInstance(); await activeWorkflowRunner.init(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const waitTracker = WaitTracker(); const editorUrl = GenericHelpers.getBaseUrl(); @@ -297,7 +323,7 @@ export class Start extends Command { process.stdin.setEncoding('utf8'); let inputText = ''; - if (flags.open === true) { + if (flags.open) { Start.openBrowser(); } this.log(`\nPress "o" to open in Browser.`); @@ -307,15 +333,18 @@ export class Start extends Command { inputText = ''; } else if (key.charCodeAt(0) === 3) { // Ctrl + c got pressed + // eslint-disable-next-line @typescript-eslint/no-floating-promises Start.stopProcess(); } else { // When anything else got pressed, record it and send it on enter into the child process + // eslint-disable-next-line no-lonely-if if (key.charCodeAt(0) === 13) { // send to child process and print in terminal process.stdout.write('\n'); inputText = ''; } else { // record it and write into terminal + // eslint-disable-next-line @typescript-eslint/no-unused-vars inputText += key; process.stdout.write(key); } @@ -323,6 +352,7 @@ export class Start extends Command { }); } } catch (error) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions this.error(`There was an error: ${error.message}`); processExistCode = 1; diff --git a/packages/cli/commands/update/workflow.ts b/packages/cli/commands/update/workflow.ts index c7d81901aa..b3d777de7d 100644 --- a/packages/cli/commands/update/workflow.ts +++ b/packages/cli/commands/update/workflow.ts @@ -1,26 +1,16 @@ -import { - Command, flags, -} from '@oclif/command'; +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable no-console */ +import { Command, flags } from '@oclif/command'; -import { - IDataObject -} from 'n8n-workflow'; +import { IDataObject, LoggerProxy } from 'n8n-workflow'; -import { - Db, - GenericHelpers, -} from '../../src'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { Db, GenericHelpers } from '../../src'; -import { - getLogger, -} from '../../src/Logger'; - -import { - LoggerProxy, -} from 'n8n-workflow'; +import { getLogger } from '../../src/Logger'; export class UpdateWorkflowCommand extends Command { - static description = '\Update workflows'; + static description = 'Update workflows'; static examples = [ `$ n8n update:workflow --all --active=false`, @@ -40,10 +30,12 @@ export class UpdateWorkflowCommand extends Command { }), }; + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async run() { const logger = getLogger(); LoggerProxy.init(logger); + // eslint-disable-next-line @typescript-eslint/no-shadow const { flags } = this.parse(UpdateWorkflowCommand); if (!flags.all && !flags.id) { @@ -52,7 +44,9 @@ export class UpdateWorkflowCommand extends Command { } if (flags.all && flags.id) { - console.info(`Either something else on top should be "--all" or "--id" can be set never both!`); + console.info( + `Either something else on top should be "--all" or "--id" can be set never both!`, + ); return; } @@ -60,13 +54,12 @@ export class UpdateWorkflowCommand extends Command { if (flags.active === undefined) { console.info(`No update flag like "--active=true" has been set!`); return; - } else { - if (!['false', 'true'].includes(flags.active)) { - console.info(`Valid values for flag "--active" are only "false" or "true"!`); - return; - } - updateQuery.active = flags.active === 'true'; } + if (!['false', 'true'].includes(flags.active)) { + console.info(`Valid values for flag "--active" are only "false" or "true"!`); + return; + } + updateQuery.active = flags.active === 'true'; try { await Db.init(); @@ -80,6 +73,7 @@ export class UpdateWorkflowCommand extends Command { findQuery.active = true; } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion await Db.collections.Workflow!.update(findQuery, updateQuery); console.info('Done'); } catch (e) { diff --git a/packages/cli/commands/webhook.ts b/packages/cli/commands/webhook.ts index e1acc94874..1600877cbe 100644 --- a/packages/cli/commands/webhook.ts +++ b/packages/cli/commands/webhook.ts @@ -1,9 +1,14 @@ -import { - UserSettings, -} from 'n8n-core'; +/* eslint-disable no-console */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/unbound-method */ +import { UserSettings } from 'n8n-core'; import { Command, flags } from '@oclif/command'; +// eslint-disable-next-line import/no-extraneous-dependencies import * as Redis from 'ioredis'; +import { IDataObject, LoggerProxy } from 'n8n-workflow'; import * as config from '../config'; import { ActiveExecutions, @@ -15,29 +20,20 @@ import { GenericHelpers, LoadNodesAndCredentials, NodeTypes, + // eslint-disable-next-line @typescript-eslint/no-unused-vars TestWebhooks, WebhookServer, } from '../src'; -import { IDataObject } from 'n8n-workflow'; -import { - getLogger, -} from '../src/Logger'; - -import { - LoggerProxy, -} from 'n8n-workflow'; +import { getLogger } from '../src/Logger'; let activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner | undefined; let processExistCode = 0; - export class Webhook extends Command { static description = 'Starts n8n webhook process. Intercepts only production URLs.'; - static examples = [ - `$ n8n webhook`, - ]; + static examples = [`$ n8n webhook`]; static flags = { help: flags.help({ char: 'h' }), @@ -48,6 +44,7 @@ export class Webhook extends Command { * Make for example sure that all the webhooks from third party services * get removed. */ + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types static async stopProcess() { LoggerProxy.info(`\nStopping n8n...`); @@ -68,14 +65,16 @@ export class Webhook extends Command { let count = 0; while (executingWorkflows.length !== 0) { if (count++ % 4 === 0) { - LoggerProxy.info(`Waiting for ${executingWorkflows.length} active executions to finish...`); + LoggerProxy.info( + `Waiting for ${executingWorkflows.length} active executions to finish...`, + ); } + // eslint-disable-next-line no-await-in-loop await new Promise((resolve) => { setTimeout(resolve, 500); }); executingWorkflows = activeExecutionsInstance.getActiveExecutions(); } - } catch (error) { LoggerProxy.error('There was an error shutting down n8n.', error); } @@ -83,7 +82,7 @@ export class Webhook extends Command { process.exit(processExistCode); } - + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async run() { const logger = getLogger(); LoggerProxy.init(logger); @@ -92,6 +91,7 @@ export class Webhook extends Command { process.on('SIGTERM', Webhook.stopProcess); process.on('SIGINT', Webhook.stopProcess); + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-shadow const { flags } = this.parse(Webhook); // Wrap that the process does not close but we can still use async @@ -114,7 +114,8 @@ export class Webhook extends Command { try { // Start directly with the init of the database to improve startup time - const startDbInitPromise = Db.init().catch(error => { + const startDbInitPromise = Db.init().catch((error) => { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access logger.error(`There was an error initializing DB: "${error.message}"`); processExistCode = 1; @@ -124,6 +125,7 @@ export class Webhook extends Command { }); // Make sure the settings exist + // eslint-disable-next-line @typescript-eslint/no-unused-vars const userSettings = await UserSettings.prepareUserSettings(); // Load all node and credential types @@ -153,9 +155,11 @@ export class Webhook extends Command { const redisPort = config.get('queue.bull.redis.port'); const redisDB = config.get('queue.bull.redis.db'); const redisConnectionTimeoutLimit = config.get('queue.bull.redis.timeoutThreshold'); - let lastTimer = 0, cumulativeTimeout = 0; + let lastTimer = 0; + let cumulativeTimeout = 0; const settings = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars retryStrategy: (times: number): number | null => { const now = Date.now(); if (now - lastTimer > 30000) { @@ -166,7 +170,10 @@ export class Webhook extends Command { cumulativeTimeout += now - lastTimer; lastTimer = now; if (cumulativeTimeout > redisConnectionTimeoutLimit) { - logger.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + ". Exiting process."); + logger.error( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Unable to connect to Redis after ${redisConnectionTimeoutLimit}. Exiting process.`, + ); process.exit(1); } } @@ -208,11 +215,12 @@ export class Webhook extends Command { activeWorkflowRunner = ActiveWorkflowRunner.getInstance(); await activeWorkflowRunner.initWebhooks(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const editorUrl = GenericHelpers.getBaseUrl(); console.info('Webhook listener waiting for requests.'); - } catch (error) { console.error('Exiting due to error. See log message for details.'); + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions logger.error(`Webhook process cannot continue. "${error.message}"`); processExistCode = 1; diff --git a/packages/cli/commands/worker.ts b/packages/cli/commands/worker.ts index 6e43857d34..32a7d24ccb 100644 --- a/packages/cli/commands/worker.ts +++ b/packages/cli/commands/worker.ts @@ -1,10 +1,16 @@ +/* eslint-disable no-console */ +/* eslint-disable @typescript-eslint/no-shadow */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/unbound-method */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +// eslint-disable-next-line import/no-extraneous-dependencies import * as PCancelable from 'p-cancelable'; import { Command, flags } from '@oclif/command'; -import { - UserSettings, - WorkflowExecute, -} from 'n8n-core'; +import { UserSettings, WorkflowExecute } from 'n8n-core'; import { IDataObject, @@ -13,12 +19,12 @@ import { IWorkflowExecuteHooks, Workflow, WorkflowHooks, + LoggerProxy, } from 'n8n-workflow'; -import { - FindOneOptions, -} from 'typeorm'; +import { FindOneOptions } from 'typeorm'; +import * as Bull from 'bull'; import { ActiveExecutions, CredentialsOverwrites, @@ -37,24 +43,15 @@ import { WorkflowExecuteAdditionalData, } from '../src'; -import { - getLogger, -} from '../src/Logger'; - -import { - LoggerProxy, -} from 'n8n-workflow'; +import { getLogger } from '../src/Logger'; import * as config from '../config'; -import * as Bull from 'bull'; import * as Queue from '../src/Queue'; export class Worker extends Command { static description = '\nStarts a n8n worker'; - static examples = [ - `$ n8n worker --concurrency=5`, - ]; + static examples = [`$ n8n worker --concurrency=5`]; static flags = { help: flags.help({ char: 'h' }), @@ -82,6 +79,7 @@ export class Worker extends Command { LoggerProxy.info(`Stopping n8n...`); // Stop accepting new jobs + // eslint-disable-next-line @typescript-eslint/no-floating-promises Worker.jobQueue.pause(true); try { @@ -103,13 +101,17 @@ export class Worker extends Command { while (Object.keys(Worker.runningJobs).length !== 0) { if (count++ % 4 === 0) { const waitLeft = Math.ceil((stopTime - new Date().getTime()) / 1000); - LoggerProxy.info(`Waiting for ${Object.keys(Worker.runningJobs).length} active executions to finish... (wait ${waitLeft} more seconds)`); + LoggerProxy.info( + `Waiting for ${ + Object.keys(Worker.runningJobs).length + } active executions to finish... (wait ${waitLeft} more seconds)`, + ); } + // eslint-disable-next-line no-await-in-loop await new Promise((resolve) => { setTimeout(resolve, 500); }); } - } catch (error) { LoggerProxy.error('There was an error shutting down n8n.', error); } @@ -119,25 +121,38 @@ export class Worker extends Command { async runJob(job: Bull.Job, nodeTypes: INodeTypes): Promise { const jobData = job.data as IBullJobData; - const executionDb = await Db.collections.Execution!.findOne(jobData.executionId) as IExecutionFlattedDb; - const currentExecutionDb = ResponseHelper.unflattenExecutionData(executionDb) as IExecutionResponse; - LoggerProxy.info(`Start job: ${job.id} (Workflow ID: ${currentExecutionDb.workflowData.id} | Execution: ${jobData.executionId})`); + const executionDb = (await Db.collections.Execution!.findOne( + jobData.executionId, + )) as IExecutionFlattedDb; + const currentExecutionDb = ResponseHelper.unflattenExecutionData(executionDb); + LoggerProxy.info( + `Start job: ${job.id} (Workflow ID: ${currentExecutionDb.workflowData.id} | Execution: ${jobData.executionId})`, + ); - let staticData = currentExecutionDb.workflowData!.staticData; - if (jobData.loadStaticData === true) { + let { staticData } = currentExecutionDb.workflowData; + if (jobData.loadStaticData) { const findOptions = { select: ['id', 'staticData'], } as FindOneOptions; - const workflowData = await Db.collections!.Workflow!.findOne(currentExecutionDb.workflowData.id, findOptions); + const workflowData = await Db.collections.Workflow!.findOne( + currentExecutionDb.workflowData.id, + findOptions, + ); if (workflowData === undefined) { - throw new Error(`The workflow with the ID "${currentExecutionDb.workflowData.id}" could not be found`); + throw new Error( + `The workflow with the ID "${currentExecutionDb.workflowData.id}" could not be found`, + ); } staticData = workflowData.staticData; } let workflowTimeout = config.get('executions.timeout') as number; // initialize with default - if (currentExecutionDb.workflowData.settings && currentExecutionDb.workflowData.settings.executionTimeout) { - workflowTimeout = currentExecutionDb.workflowData.settings!.executionTimeout as number; // preference on workflow setting + if ( + // eslint-disable-next-line @typescript-eslint/prefer-optional-chain + currentExecutionDb.workflowData.settings && + currentExecutionDb.workflowData.settings.executionTimeout + ) { + workflowTimeout = currentExecutionDb.workflowData.settings.executionTimeout as number; // preference on workflow setting } let executionTimeoutTimestamp: number | undefined; @@ -146,16 +161,37 @@ export class Worker extends Command { executionTimeoutTimestamp = Date.now() + workflowTimeout * 1000; } - const workflow = new Workflow({ id: currentExecutionDb.workflowData.id as string, name: currentExecutionDb.workflowData.name, nodes: currentExecutionDb.workflowData!.nodes, connections: currentExecutionDb.workflowData!.connections, active: currentExecutionDb.workflowData!.active, nodeTypes, staticData, settings: currentExecutionDb.workflowData!.settings }); + const workflow = new Workflow({ + id: currentExecutionDb.workflowData.id as string, + name: currentExecutionDb.workflowData.name, + nodes: currentExecutionDb.workflowData.nodes, + connections: currentExecutionDb.workflowData.connections, + active: currentExecutionDb.workflowData.active, + nodeTypes, + staticData, + settings: currentExecutionDb.workflowData.settings, + }); - const additionalData = await WorkflowExecuteAdditionalData.getBase(undefined, executionTimeoutTimestamp); - additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(currentExecutionDb.mode, job.data.executionId, currentExecutionDb.workflowData, { retryOf: currentExecutionDb.retryOf as string }); + const additionalData = await WorkflowExecuteAdditionalData.getBase( + undefined, + executionTimeoutTimestamp, + ); + additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter( + currentExecutionDb.mode, + job.data.executionId, + currentExecutionDb.workflowData, + { retryOf: currentExecutionDb.retryOf as string }, + ); additionalData.executionId = jobData.executionId; let workflowExecute: WorkflowExecute; let workflowRun: PCancelable; if (currentExecutionDb.data !== undefined) { - workflowExecute = new WorkflowExecute(additionalData, currentExecutionDb.mode, currentExecutionDb.data); + workflowExecute = new WorkflowExecute( + additionalData, + currentExecutionDb.mode, + currentExecutionDb.data, + ); workflowRun = workflowExecute.processRunExecutionData(workflow); } else { // Execute all nodes @@ -180,6 +216,7 @@ export class Worker extends Command { const logger = getLogger(); LoggerProxy.init(logger); + // eslint-disable-next-line no-console console.info('Starting n8n worker...'); // Make sure that n8n shuts down gracefully if possible @@ -192,7 +229,7 @@ export class Worker extends Command { const { flags } = this.parse(Worker); // Start directly with the init of the database to improve startup time - const startDbInitPromise = Db.init().catch(error => { + const startDbInitPromise = Db.init().catch((error) => { logger.error(`There was an error initializing DB: "${error.message}"`); Worker.processExistCode = 1; @@ -225,10 +262,12 @@ export class Worker extends Command { // Wait till the database is ready await startDbInitPromise; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const redisConnectionTimeoutLimit = config.get('queue.bull.redis.timeoutThreshold'); Worker.jobQueue = Queue.getInstance().getBullObjectInstance(); - Worker.jobQueue.process(flags.concurrency, (job) => this.runJob(job, nodeTypes)); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + Worker.jobQueue.process(flags.concurrency, async (job) => this.runJob(job, nodeTypes)); const versions = await GenericHelpers.getVersions(); @@ -251,9 +290,10 @@ export class Worker extends Command { } }); - let lastTimer = 0, cumulativeTimeout = 0; + let lastTimer = 0; + let cumulativeTimeout = 0; Worker.jobQueue.on('error', (error: Error) => { - if (error.toString().includes('ECONNREFUSED') === true) { + if (error.toString().includes('ECONNREFUSED')) { const now = Date.now(); if (now - lastTimer > 30000) { // Means we had no timeout at all or last timeout was temporary and we recovered @@ -263,12 +303,14 @@ export class Worker extends Command { cumulativeTimeout += now - lastTimer; lastTimer = now; if (cumulativeTimeout > redisConnectionTimeoutLimit) { - logger.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + ". Exiting process."); + logger.error( + `Unable to connect to Redis after ${redisConnectionTimeoutLimit}. Exiting process.`, + ); process.exit(1); } } logger.warn('Redis unavailable - trying to reconnect...'); - } else if (error.toString().includes('Error initializing Lua scripts') === true) { + } else if (error.toString().includes('Error initializing Lua scripts')) { // This is a non-recoverable error // Happens when worker starts and Redis is unavailable // Even if Redis comes back online, worker will be zombie @@ -287,6 +329,5 @@ export class Worker extends Command { process.exit(1); } })(); - } } diff --git a/packages/cli/config/index.ts b/packages/cli/config/index.ts index 0a5273799b..2ada64eed9 100644 --- a/packages/cli/config/index.ts +++ b/packages/cli/config/index.ts @@ -1,3 +1,6 @@ +/* eslint-disable no-console */ +/* eslint-disable no-restricted-syntax */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import * as convict from 'convict'; import * as dotenv from 'dotenv'; import * as path from 'path'; @@ -6,7 +9,6 @@ import * as core from 'n8n-core'; dotenv.config(); const config = convict({ - database: { type: { doc: 'Type of database to use', @@ -84,7 +86,6 @@ const config = convict({ env: 'DB_POSTGRESDB_SSL_REJECT_UNAUTHORIZED', }, }, - }, mysqldb: { database: { @@ -159,7 +160,6 @@ const config = convict({ }, executions: { - // By default workflows get always executed in their own process. // If this option gets set to "main" it will run them in the // main-process instead. @@ -573,7 +573,6 @@ const config = convict({ throw new Error(); } } - } catch (error) { throw new TypeError(`The Nodes to exclude is not a valid Array of strings.`); } @@ -644,7 +643,6 @@ const config = convict({ env: 'N8N_VERSION_NOTIFICATIONS_INFO_URL', }, }, - }); // Overwrite default configuration with settings which got defined in diff --git a/packages/cli/migrations/ormconfig.ts b/packages/cli/migrations/ormconfig.ts index 2ee9d38025..efcfce2cc3 100644 --- a/packages/cli/migrations/ormconfig.ts +++ b/packages/cli/migrations/ormconfig.ts @@ -3,89 +3,73 @@ import { UserSettings } from 'n8n-core'; import { entities } from '../src/databases/entities'; module.exports = [ - { - "name": "sqlite", - "type": "sqlite", - "logging": true, - "entities": Object.values(entities), - "database": path.join(UserSettings.getUserN8nFolderPath(), 'database.sqlite'), - "migrations": [ - "./src/databases/sqlite/migrations/*.ts" - ], - "subscribers": [ - "./src/databases/sqlite/subscribers/*.ts" - ], - "cli": { - "entitiesDir": "./src/databases/entities", - "migrationsDir": "./src/databases/sqlite/migrations", - "subscribersDir": "./src/databases/sqlite/subscribers" - } - }, - { - "name": "postgres", - "type": "postgres", - "logging": false, - "host": "localhost", - "username": "postgres", - "password": "", - "port": 5432, - "database": "n8n", - "schema": "public", - "entities": Object.values(entities), - "migrations": [ - "./src/databases/postgresdb/migrations/*.ts" - ], - "subscribers": [ - "src/subscriber/**/*.ts" - ], - "cli": { - "entitiesDir": "./src/databases/entities", - "migrationsDir": "./src/databases/postgresdb/migrations", - "subscribersDir": "./src/databases/postgresdb/subscribers" - } - }, - { - "name": "mysql", - "type": "mysql", - "database": "n8n", - "username": "root", - "password": "password", - "host": "localhost", - "port": "3306", - "logging": false, - "entities": Object.values(entities), - "migrations": [ - "./src/databases/mysqldb/migrations/*.ts" - ], - "subscribers": [ - "src/subscriber/**/*.ts" - ], - "cli": { - "entitiesDir": "./src/databases/entities", - "migrationsDir": "./src/databases/mysqldb/migrations", - "subscribersDir": "./src/databases/mysqldb/Subscribers" - } - }, - { - "name": "mariadb", - "type": "mariadb", - "database": "n8n", - "username": "root", - "password": "password", - "host": "localhost", - "port": "3306", - "logging": false, - "entities": Object.values(entities), - "migrations": [ - "./src/databases/mysqldb/migrations/*.ts" - ], - "subscribers": [ - "src/subscriber/**/*.ts" - ], - "cli": { - "entitiesDir": "./src/databases/entities", - "migrationsDir": "./src/databases/mysqldb/migrations", - "subscribersDir": "./src/databases/mysqldb/Subscribers" - } - }, + { + name: 'sqlite', + type: 'sqlite', + logging: true, + entities: Object.values(entities), + database: path.join(UserSettings.getUserN8nFolderPath(), 'database.sqlite'), + migrations: ['./src/databases/sqlite/migrations/*.ts'], + subscribers: ['./src/databases/sqlite/subscribers/*.ts'], + cli: { + entitiesDir: './src/databases/entities', + migrationsDir: './src/databases/sqlite/migrations', + subscribersDir: './src/databases/sqlite/subscribers', + }, + }, + { + name: 'postgres', + type: 'postgres', + logging: false, + host: 'localhost', + username: 'postgres', + password: '', + port: 5432, + database: 'n8n', + schema: 'public', + entities: Object.values(entities), + migrations: ['./src/databases/postgresdb/migrations/*.ts'], + subscribers: ['src/subscriber/**/*.ts'], + cli: { + entitiesDir: './src/databases/entities', + migrationsDir: './src/databases/postgresdb/migrations', + subscribersDir: './src/databases/postgresdb/subscribers', + }, + }, + { + name: 'mysql', + type: 'mysql', + database: 'n8n', + username: 'root', + password: 'password', + host: 'localhost', + port: '3306', + logging: false, + entities: Object.values(entities), + migrations: ['./src/databases/mysqldb/migrations/*.ts'], + subscribers: ['src/subscriber/**/*.ts'], + cli: { + entitiesDir: './src/databases/entities', + migrationsDir: './src/databases/mysqldb/migrations', + subscribersDir: './src/databases/mysqldb/Subscribers', + }, + }, + { + name: 'mariadb', + type: 'mariadb', + database: 'n8n', + username: 'root', + password: 'password', + host: 'localhost', + port: '3306', + logging: false, + entities: Object.values(entities), + migrations: ['./src/databases/mysqldb/migrations/*.ts'], + subscribers: ['src/subscriber/**/*.ts'], + cli: { + entitiesDir: './src/databases/entities', + migrationsDir: './src/databases/mysqldb/migrations', + subscribersDir: './src/databases/mysqldb/Subscribers', + }, + }, ]; diff --git a/packages/cli/package.json b/packages/cli/package.json index 60b2572d05..56b1622959 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -21,14 +21,15 @@ "scripts": { "build": "tsc", "dev": "concurrently -k -n \"TypeScript,Node\" -c \"yellow.bold,cyan.bold\" \"npm run watch\" \"nodemon\"", + "format": "cd ../.. && node_modules/prettier/bin-prettier.js packages/cli/**/**.ts --write", + "lint": "cd ../.. && node_modules/eslint/bin/eslint.js packages/cli", + "lintfix": "cd ../.. && node_modules/eslint/bin/eslint.js packages/cli --fix", "postpack": "rm -f oclif.manifest.json", "prepack": "echo \"Building project...\" && rm -rf dist && tsc -b && oclif-dev manifest", "start": "run-script-os", "start:default": "cd bin && ./n8n", "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" }, @@ -77,7 +78,7 @@ "ts-jest": "^26.3.0", "ts-node": "^8.9.1", "tslint": "^6.1.2", - "typescript": "~3.9.7" + "typescript": "~4.3.5" }, "dependencies": { "@oclif/command": "^1.5.18", diff --git a/packages/cli/src/ActiveExecutions.ts b/packages/cli/src/ActiveExecutions.ts index 3741ccd19e..dac67322c6 100644 --- a/packages/cli/src/ActiveExecutions.ts +++ b/packages/cli/src/ActiveExecutions.ts @@ -1,11 +1,18 @@ -import { - IRun, -} from 'n8n-workflow'; +/* eslint-disable prefer-template */ +/* eslint-disable @typescript-eslint/restrict-plus-operands */ +/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */ +/* eslint-disable no-param-reassign */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { IRun } from 'n8n-workflow'; -import { - createDeferredPromise, -} from 'n8n-core'; +import { createDeferredPromise } from 'n8n-core'; +import { ChildProcess } from 'child_process'; +// eslint-disable-next-line import/no-extraneous-dependencies +import * as PCancelable from 'p-cancelable'; +// eslint-disable-next-line import/no-cycle import { Db, IExecutingWorkflowData, @@ -17,16 +24,11 @@ import { WorkflowHelpers, } from '.'; -import { ChildProcess } from 'child_process'; -import * as PCancelable from 'p-cancelable'; - - export class ActiveExecutions { private activeExecutions: { [index: string]: IExecutingWorkflowData; } = {}; - /** * Add a new active execution * @@ -35,8 +37,11 @@ export class ActiveExecutions { * @returns {string} * @memberof ActiveExecutions */ - async add(executionData: IWorkflowExecutionDataProcess, process?: ChildProcess, executionId?: string): Promise { - + async add( + executionData: IWorkflowExecutionDataProcess, + process?: ChildProcess, + executionId?: string, + ): Promise { if (executionId === undefined) { // Is a new execution so save in DB @@ -52,14 +57,23 @@ export class ActiveExecutions { fullExecutionData.retryOf = executionData.retryOf.toString(); } - if (executionData.workflowData.id !== undefined && WorkflowHelpers.isWorkflowIdValid(executionData.workflowData.id.toString()) === true) { + if ( + executionData.workflowData.id !== undefined && + WorkflowHelpers.isWorkflowIdValid(executionData.workflowData.id.toString()) + ) { fullExecutionData.workflowId = executionData.workflowData.id.toString(); } const execution = ResponseHelper.flattenExecutionData(fullExecutionData); - const executionResult = await Db.collections.Execution!.save(execution as IExecutionFlattedDb); - executionId = typeof executionResult.id === "object" ? executionResult.id!.toString() : executionResult.id + ""; + const executionResult = await Db.collections.Execution!.save( + execution as IExecutionFlattedDb, + ); + executionId = + typeof executionResult.id === 'object' + ? // @ts-ignore + executionResult.id!.toString() + : executionResult.id + ''; } else { // Is an existing execution we want to finish so update in DB @@ -72,6 +86,7 @@ export class ActiveExecutions { await Db.collections.Execution!.update(executionId, execution); } + // @ts-ignore this.activeExecutions[executionId] = { executionData, process, @@ -79,10 +94,10 @@ export class ActiveExecutions { postExecutePromises: [], }; + // @ts-ignore return executionId; } - /** * Attaches an execution * @@ -90,15 +105,17 @@ export class ActiveExecutions { * @param {PCancelable} workflowExecution * @memberof ActiveExecutions */ + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types attachWorkflowExecution(executionId: string, workflowExecution: PCancelable) { if (this.activeExecutions[executionId] === undefined) { - throw new Error(`No active execution with id "${executionId}" got found to attach to workflowExecution to!`); + throw new Error( + `No active execution with id "${executionId}" got found to attach to workflowExecution to!`, + ); } this.activeExecutions[executionId].workflowExecution = workflowExecution; } - /** * Remove an active execution * @@ -113,6 +130,7 @@ export class ActiveExecutions { } // Resolve all the waiting promises + // eslint-disable-next-line no-restricted-syntax for (const promise of this.activeExecutions[executionId].postExecutePromises) { promise.resolve(fullRunData); } @@ -121,7 +139,6 @@ export class ActiveExecutions { delete this.activeExecutions[executionId]; } - /** * Forces an execution to stop * @@ -142,9 +159,10 @@ export class ActiveExecutions { // Workflow is running in subprocess if (this.activeExecutions[executionId].process!.connected) { setTimeout(() => { - // execute on next event loop tick; + // execute on next event loop tick; this.activeExecutions[executionId].process!.send({ - type: timeout ? timeout : 'stopExecution', + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + type: timeout || 'stopExecution', }); }, 1); } @@ -153,10 +171,10 @@ export class ActiveExecutions { this.activeExecutions[executionId].workflowExecution!.cancel(); } + // eslint-disable-next-line consistent-return return this.getPostExecutePromise(executionId); } - /** * Returns a promise which will resolve with the data of the execution * with the given id @@ -178,7 +196,6 @@ export class ActiveExecutions { return waitPromise.promise(); } - /** * Returns all the currently active executions * @@ -189,25 +206,22 @@ export class ActiveExecutions { const returnData: IExecutionsCurrentSummary[] = []; let data; + // eslint-disable-next-line no-restricted-syntax for (const id of Object.keys(this.activeExecutions)) { data = this.activeExecutions[id]; - returnData.push( - { - id, - retryOf: data.executionData.retryOf as string | undefined, - startedAt: data.startedAt, - mode: data.executionData.executionMode, - workflowId: data.executionData.workflowData.id! as string, - } - ); + returnData.push({ + id, + retryOf: data.executionData.retryOf as string | undefined, + startedAt: data.startedAt, + mode: data.executionData.executionMode, + workflowId: data.executionData.workflowData.id! as string, + }); } return returnData; } } - - let activeExecutionsInstance: ActiveExecutions | undefined; export function getInstance(): ActiveExecutions { diff --git a/packages/cli/src/ActiveWorkflowRunner.ts b/packages/cli/src/ActiveWorkflowRunner.ts index 211af6fa1a..f1bad36a8b 100644 --- a/packages/cli/src/ActiveWorkflowRunner.ts +++ b/packages/cli/src/ActiveWorkflowRunner.ts @@ -1,23 +1,15 @@ -import { - Db, - IActivationError, - IResponseCallbackData, - IWebhookDb, - IWorkflowDb, - IWorkflowExecutionDataProcess, - NodeTypes, - ResponseHelper, - WebhookHelpers, - WorkflowCredentials, - WorkflowExecuteAdditionalData, - WorkflowHelpers, - WorkflowRunner, -} from './'; - -import { - ActiveWorkflows, - NodeExecuteFunctions, -} from 'n8n-core'; +/* eslint-disable prefer-spread */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable no-param-reassign */ +/* eslint-disable no-console */ +/* eslint-disable no-await-in-loop */ +/* eslint-disable no-restricted-syntax */ +/* eslint-disable @typescript-eslint/no-floating-promises */ +/* eslint-disable @typescript-eslint/no-shadow */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { ActiveWorkflows, NodeExecuteFunctions } from 'n8n-core'; import { IExecuteData, @@ -32,12 +24,28 @@ import { Workflow, WorkflowActivateMode, WorkflowExecuteMode, + LoggerProxy as Logger, } from 'n8n-workflow'; import * as express from 'express'; + +// eslint-disable-next-line import/no-cycle import { - LoggerProxy as Logger, -} from 'n8n-workflow'; + Db, + IActivationError, + IResponseCallbackData, + IWebhookDb, + IWorkflowDb, + IWorkflowExecutionDataProcess, + NodeTypes, + ResponseHelper, + WebhookHelpers, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + WorkflowCredentials, + WorkflowExecuteAdditionalData, + WorkflowHelpers, + WorkflowRunner, +} from '.'; const WEBHOOK_PROD_UNREGISTERED_HINT = `The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)`; @@ -48,14 +56,16 @@ export class ActiveWorkflowRunner { [key: string]: IActivationError; } = {}; + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async init() { - // Get the active workflows from database // NOTE // Here I guess we can have a flag on the workflow table like hasTrigger // so intead of pulling all the active wehhooks just pull the actives that have a trigger - const workflowsData: IWorkflowDb[] = await Db.collections.Workflow!.find({ active: true }) as IWorkflowDb[]; + const workflowsData: IWorkflowDb[] = (await Db.collections.Workflow!.find({ + active: true, + })) as IWorkflowDb[]; // Clear up active workflow table await Db.collections.Webhook?.clear(); @@ -69,21 +79,32 @@ export class ActiveWorkflowRunner { for (const workflowData of workflowsData) { console.log(` - ${workflowData.name}`); - Logger.debug(`Initializing active workflow "${workflowData.name}" (startup)`, { workflowName: workflowData.name, workflowId: workflowData.id }); + Logger.debug(`Initializing active workflow "${workflowData.name}" (startup)`, { + workflowName: workflowData.name, + workflowId: workflowData.id, + }); try { await this.add(workflowData.id.toString(), 'init', workflowData); - Logger.verbose(`Successfully started workflow "${workflowData.name}"`, { workflowName: workflowData.name, workflowId: workflowData.id }); + Logger.verbose(`Successfully started workflow "${workflowData.name}"`, { + workflowName: workflowData.name, + workflowId: workflowData.id, + }); console.log(` => Started`); } catch (error) { console.log(` => ERROR: Workflow could not be activated:`); + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions console.log(` ${error.message}`); - Logger.error(`Unable to initialize workflow "${workflowData.name}" (startup)`, { workflowName: workflowData.name, workflowId: workflowData.id }); + Logger.error(`Unable to initialize workflow "${workflowData.name}" (startup)`, { + workflowName: workflowData.name, + workflowId: workflowData.id, + }); } } Logger.verbose('Finished initializing active workflows (startup)'); } } + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async initWebhooks() { this.activeWorkflows = new ActiveWorkflows(); } @@ -104,7 +125,10 @@ export class ActiveWorkflowRunner { } const activeWorkflows = await this.getActiveWorkflows(); - activeWorkflowId.push.apply(activeWorkflowId, activeWorkflows.map(workflow => workflow.id)); + activeWorkflowId.push.apply( + activeWorkflowId, + activeWorkflows.map((workflow) => workflow.id), + ); const removePromises = []; for (const workflowId of activeWorkflowId) { @@ -112,7 +136,6 @@ export class ActiveWorkflowRunner { } await Promise.all(removePromises); - return; } /** @@ -125,10 +148,19 @@ export class ActiveWorkflowRunner { * @returns {Promise} * @memberof ActiveWorkflowRunner */ - async executeWebhook(httpMethod: WebhookHttpMethod, path: string, req: express.Request, res: express.Response): Promise { + async executeWebhook( + httpMethod: WebhookHttpMethod, + path: string, + req: express.Request, + res: express.Response, + ): Promise { Logger.debug(`Received webhoook "${httpMethod}" for path "${path}"`); if (this.activeWorkflows === null) { - throw new ResponseHelper.ResponseError('The "activeWorkflows" instance did not get initialized yet.', 404, 404); + throw new ResponseHelper.ResponseError( + 'The "activeWorkflows" instance did not get initialized yet.', + 404, + 404, + ); } // Reset request parameters @@ -139,7 +171,10 @@ export class ActiveWorkflowRunner { path = path.slice(0, -1); } - let webhook = await Db.collections.Webhook?.findOne({ webhookPath: path, method: httpMethod }) as IWebhookDb; + let webhook = (await Db.collections.Webhook?.findOne({ + webhookPath: path, + method: httpMethod, + })) as IWebhookDb; let webhookId: string | undefined; // check if path is dynamic @@ -147,19 +182,30 @@ export class ActiveWorkflowRunner { // check if a dynamic webhook path exists const pathElements = path.split('/'); webhookId = pathElements.shift(); - const dynamicWebhooks = await Db.collections.Webhook?.find({ webhookId, method: httpMethod, pathLength: pathElements.length }); + const dynamicWebhooks = await Db.collections.Webhook?.find({ + webhookId, + method: httpMethod, + pathLength: pathElements.length, + }); if (dynamicWebhooks === undefined || dynamicWebhooks.length === 0) { // The requested webhook is not registered - throw new ResponseHelper.ResponseError(`The requested webhook "${httpMethod} ${path}" is not registered.`, 404, 404, WEBHOOK_PROD_UNREGISTERED_HINT); + throw new ResponseHelper.ResponseError( + `The requested webhook "${httpMethod} ${path}" is not registered.`, + 404, + 404, + WEBHOOK_PROD_UNREGISTERED_HINT, + ); } let maxMatches = 0; const pathElementsSet = new Set(pathElements); // check if static elements match in path // if more results have been returned choose the one with the most static-route matches - dynamicWebhooks.forEach(dynamicWebhook => { - const staticElements = dynamicWebhook.webhookPath.split('/').filter(ele => !ele.startsWith(':')); - const allStaticExist = staticElements.every(staticEle => pathElementsSet.has(staticEle)); + dynamicWebhooks.forEach((dynamicWebhook) => { + const staticElements = dynamicWebhook.webhookPath + .split('/') + .filter((ele) => !ele.startsWith(':')); + const allStaticExist = staticElements.every((staticEle) => pathElementsSet.has(staticEle)); if (allStaticExist && staticElements.length > maxMatches) { maxMatches = staticElements.length; @@ -171,12 +217,20 @@ export class ActiveWorkflowRunner { } }); if (webhook === undefined) { - throw new ResponseHelper.ResponseError(`The requested webhook "${httpMethod} ${path}" is not registered.`, 404, 404, WEBHOOK_PROD_UNREGISTERED_HINT); + throw new ResponseHelper.ResponseError( + `The requested webhook "${httpMethod} ${path}" is not registered.`, + 404, + 404, + WEBHOOK_PROD_UNREGISTERED_HINT, + ); } - path = webhook!.webhookPath; + // @ts-ignore + // eslint-disable-next-line no-param-reassign + path = webhook.webhookPath; // extracting params from path - webhook!.webhookPath.split('/').forEach((ele, index) => { + // @ts-ignore + webhook.webhookPath.split('/').forEach((ele, index) => { if (ele.startsWith(':')) { // write params to req.params req.params[ele.slice(1)] = pathElements[index]; @@ -186,16 +240,33 @@ export class ActiveWorkflowRunner { const workflowData = await Db.collections.Workflow!.findOne(webhook.workflowId); if (workflowData === undefined) { - throw new ResponseHelper.ResponseError(`Could not find workflow with id "${webhook.workflowId}"`, 404, 404); + throw new ResponseHelper.ResponseError( + `Could not find workflow with id "${webhook.workflowId}"`, + 404, + 404, + ); } const nodeTypes = NodeTypes(); - const workflow = new Workflow({ id: webhook.workflowId.toString(), name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, nodeTypes, staticData: workflowData.staticData, settings: workflowData.settings }); + const workflow = new Workflow({ + id: webhook.workflowId.toString(), + name: workflowData.name, + nodes: workflowData.nodes, + connections: workflowData.connections, + active: workflowData.active, + nodeTypes, + staticData: workflowData.staticData, + settings: workflowData.settings, + }); const additionalData = await WorkflowExecuteAdditionalData.getBase(); - const webhookData = NodeHelpers.getNodeWebhooks(workflow, workflow.getNode(webhook.node as string) as INode, additionalData).filter((webhook) => { - return (webhook.httpMethod === httpMethod && webhook.path === path); + const webhookData = NodeHelpers.getNodeWebhooks( + workflow, + workflow.getNode(webhook.node) as INode, + additionalData, + ).filter((webhook) => { + return webhook.httpMethod === httpMethod && webhook.path === path; })[0]; // Get the node which has the webhook defined to know where to start from and to @@ -208,13 +279,26 @@ export class ActiveWorkflowRunner { return new Promise((resolve, reject) => { const executionMode = 'webhook'; - //@ts-ignore - WebhookHelpers.executeWebhook(workflow, webhookData, workflowData, workflowStartNode, executionMode, undefined, undefined, undefined, req, res, (error: Error | null, data: object) => { - if (error !== null) { - return reject(error); - } - resolve(data); - }); + // @ts-ignore + WebhookHelpers.executeWebhook( + workflow, + webhookData, + workflowData, + workflowStartNode, + executionMode, + undefined, + undefined, + undefined, + req, + res, + // eslint-disable-next-line consistent-return + (error: Error | null, data: object) => { + if (error !== null) { + return reject(error); + } + resolve(data); + }, + ); }); } @@ -226,10 +310,10 @@ export class ActiveWorkflowRunner { * @memberof ActiveWorkflowRunner */ async getWebhookMethods(path: string): Promise { - const webhooks = await Db.collections.Webhook?.find({ webhookPath: path }) as IWebhookDb[]; + const webhooks = (await Db.collections.Webhook?.find({ webhookPath: path })) as IWebhookDb[]; // Gather all request methods in string array - const webhookMethods: string[] = webhooks.map(webhook => webhook.method); + const webhookMethods: string[] = webhooks.map((webhook) => webhook.method); return webhookMethods; } @@ -240,11 +324,15 @@ export class ActiveWorkflowRunner { * @memberof ActiveWorkflowRunner */ 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); + const activeWorkflows = (await Db.collections.Workflow?.find({ + where: { active: true }, + select: ['id'], + })) as IWorkflowDb[]; + return activeWorkflows.filter( + (workflow) => this.activationErrors[workflow.id.toString()] === undefined, + ); } - /** * Returns if the workflow is active * @@ -253,8 +341,8 @@ export class ActiveWorkflowRunner { * @memberof ActiveWorkflowRunner */ async isActive(id: string): Promise { - const workflow = await Db.collections.Workflow?.findOne({ id: Number(id) }) as IWorkflowDb; - return workflow?.active as boolean; + const workflow = (await Db.collections.Workflow?.findOne({ id: Number(id) })) as IWorkflowDb; + return workflow?.active; } /** @@ -281,12 +369,16 @@ export class ActiveWorkflowRunner { * @returns {Promise} * @memberof ActiveWorkflowRunner */ - async addWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): Promise { + async addWorkflowWebhooks( + workflow: Workflow, + additionalData: IWorkflowExecuteAdditionalDataWorkflow, + mode: WorkflowExecuteMode, + activation: WorkflowActivateMode, + ): Promise { const webhooks = WebhookHelpers.getWorkflowWebhooks(workflow, additionalData, undefined, true); let path = '' as string | undefined; for (const webhookData of webhooks) { - const node = workflow.getNode(webhookData.node) as INode; node.name = webhookData.node; @@ -312,18 +404,35 @@ export class ActiveWorkflowRunner { } try { + // eslint-disable-next-line no-await-in-loop await Db.collections.Webhook?.insert(webhook); - const webhookExists = await workflow.runWebhookMethod('checkExists', webhookData, NodeExecuteFunctions, mode, activation, false); + const webhookExists = await workflow.runWebhookMethod( + 'checkExists', + webhookData, + NodeExecuteFunctions, + mode, + activation, + false, + ); if (webhookExists !== true) { // If webhook does not exist yet create it - await workflow.runWebhookMethod('create', webhookData, NodeExecuteFunctions, mode, activation, false); + await workflow.runWebhookMethod( + 'create', + webhookData, + NodeExecuteFunctions, + mode, + activation, + 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}"`); + console.error( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Could not remove webhooks of workflow "${workflow.id}" because of error: "${error.message}"`, + ); } let errorMessage = ''; @@ -337,6 +446,7 @@ export class ActiveWorkflowRunner { // it's a error runnig the webhook methods (checkExists, create) errorMessage = error.detail; } else { + // eslint-disable-next-line @typescript-eslint/no-unused-vars errorMessage = error.message; } @@ -347,7 +457,6 @@ export class ActiveWorkflowRunner { await WorkflowHelpers.saveStaticData(workflow); } - /** * Remove all the webhooks of the workflow * @@ -362,7 +471,16 @@ export class ActiveWorkflowRunner { } const nodeTypes = NodeTypes(); - const workflow = new Workflow({ id: workflowId, name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, nodeTypes, staticData: workflowData.staticData, settings: workflowData.settings }); + const workflow = new Workflow({ + id: workflowId, + name: workflowData.name, + nodes: workflowData.nodes, + connections: workflowData.connections, + active: workflowData.active, + nodeTypes, + staticData: workflowData.staticData, + settings: workflowData.settings, + }); const mode = 'internal'; @@ -371,7 +489,14 @@ export class ActiveWorkflowRunner { const webhooks = WebhookHelpers.getWorkflowWebhooks(workflow, additionalData, undefined, true); for (const webhookData of webhooks) { - await workflow.runWebhookMethod('delete', webhookData, NodeExecuteFunctions, mode, 'update', false); + await workflow.runWebhookMethod( + 'delete', + webhookData, + NodeExecuteFunctions, + mode, + 'update', + false, + ); } await WorkflowHelpers.saveStaticData(workflow); @@ -394,7 +519,14 @@ export class ActiveWorkflowRunner { * @returns * @memberof ActiveWorkflowRunner */ - runWorkflow(workflowData: IWorkflowDb, node: INode, data: INodeExecutionData[][], additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode) { + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + async runWorkflow( + workflowData: IWorkflowDb, + node: INode, + data: INodeExecutionData[][], + additionalData: IWorkflowExecuteAdditionalDataWorkflow, + mode: WorkflowExecuteMode, + ) { const nodeExecutionStack: IExecuteData[] = [ { node, @@ -427,7 +559,6 @@ export class ActiveWorkflowRunner { return workflowRunner.run(runData, true); } - /** * Return poll function which gets the global functions from n8n-core * and overwrites the __emit to be able to start it in subprocess @@ -438,18 +569,30 @@ export class ActiveWorkflowRunner { * @returns {IGetExecutePollFunctions} * @memberof ActiveWorkflowRunner */ - getExecutePollFunctions(workflowData: IWorkflowDb, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): IGetExecutePollFunctions { - return ((workflow: Workflow, node: INode) => { - const returnFunctions = NodeExecuteFunctions.getExecutePollFunctions(workflow, node, additionalData, mode, activation); + getExecutePollFunctions( + workflowData: IWorkflowDb, + additionalData: IWorkflowExecuteAdditionalDataWorkflow, + mode: WorkflowExecuteMode, + activation: WorkflowActivateMode, + ): IGetExecutePollFunctions { + return (workflow: Workflow, node: INode) => { + const returnFunctions = NodeExecuteFunctions.getExecutePollFunctions( + workflow, + node, + additionalData, + mode, + activation, + ); + // eslint-disable-next-line no-underscore-dangle returnFunctions.__emit = (data: INodeExecutionData[][]): void => { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions Logger.debug(`Received event to trigger execution for workflow "${workflow.name}"`); this.runWorkflow(workflowData, node, data, additionalData, mode); }; return returnFunctions; - }); + }; } - /** * Return trigger function which gets the global functions from n8n-core * and overwrites the emit to be able to start it in subprocess @@ -460,16 +603,31 @@ export class ActiveWorkflowRunner { * @returns {IGetExecuteTriggerFunctions} * @memberof ActiveWorkflowRunner */ - getExecuteTriggerFunctions(workflowData: IWorkflowDb, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): IGetExecuteTriggerFunctions { - return ((workflow: Workflow, node: INode) => { - const returnFunctions = NodeExecuteFunctions.getExecuteTriggerFunctions(workflow, node, additionalData, mode, activation); + getExecuteTriggerFunctions( + workflowData: IWorkflowDb, + additionalData: IWorkflowExecuteAdditionalDataWorkflow, + mode: WorkflowExecuteMode, + activation: WorkflowActivateMode, + ): IGetExecuteTriggerFunctions { + return (workflow: Workflow, node: INode) => { + const returnFunctions = NodeExecuteFunctions.getExecuteTriggerFunctions( + workflow, + node, + additionalData, + mode, + activation, + ); returnFunctions.emit = (data: INodeExecutionData[][]): void => { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions Logger.debug(`Received trigger for workflow "${workflow.name}"`); WorkflowHelpers.saveStaticData(workflow); - this.runWorkflow(workflowData, node, data, additionalData, mode).catch((err) => console.error(err)); + // eslint-disable-next-line id-denylist + this.runWorkflow(workflowData, node, data, additionalData, mode).catch((err) => + console.error(err), + ); }; return returnFunctions; - }); + }; } /** @@ -480,7 +638,11 @@ export class ActiveWorkflowRunner { * @returns {Promise} * @memberof ActiveWorkflowRunner */ - async add(workflowId: string, activation: WorkflowActivateMode, workflowData?: IWorkflowDb): Promise { + async add( + workflowId: string, + activation: WorkflowActivateMode, + workflowData?: IWorkflowDb, + ): Promise { if (this.activeWorkflows === null) { throw new Error(`The "activeWorkflows" instance did not get initialized yet.`); } @@ -488,33 +650,69 @@ export class ActiveWorkflowRunner { let workflowInstance: Workflow; try { if (workflowData === undefined) { - workflowData = await Db.collections.Workflow!.findOne(workflowId) as IWorkflowDb; + workflowData = (await Db.collections.Workflow!.findOne(workflowId)) as IWorkflowDb; } if (!workflowData) { throw new Error(`Could not find workflow with id "${workflowId}".`); } const nodeTypes = NodeTypes(); - workflowInstance = new Workflow({ id: workflowId, name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, nodeTypes, staticData: workflowData.staticData, settings: workflowData.settings }); + workflowInstance = new Workflow({ + id: workflowId, + name: workflowData.name, + nodes: workflowData.nodes, + connections: workflowData.connections, + active: workflowData.active, + nodeTypes, + staticData: workflowData.staticData, + settings: workflowData.settings, + }); - const canBeActivated = workflowInstance.checkIfWorkflowCanBeActivated(['n8n-nodes-base.start']); - if (canBeActivated === false) { + const canBeActivated = workflowInstance.checkIfWorkflowCanBeActivated([ + 'n8n-nodes-base.start', + ]); + if (!canBeActivated) { Logger.error(`Unable to activate workflow "${workflowData.name}"`); - throw new Error(`The workflow can not be activated because it does not contain any nodes which could start the workflow. Only workflows which have trigger or webhook nodes can be activated.`); + throw new Error( + `The workflow can not be activated because it does not contain any nodes which could start the workflow. Only workflows which have trigger or webhook nodes can be activated.`, + ); } const mode = 'trigger'; const additionalData = await WorkflowExecuteAdditionalData.getBase(); - const getTriggerFunctions = this.getExecuteTriggerFunctions(workflowData, additionalData, mode, activation); - const getPollFunctions = this.getExecutePollFunctions(workflowData, additionalData, mode, activation); + const getTriggerFunctions = this.getExecuteTriggerFunctions( + workflowData, + additionalData, + mode, + activation, + ); + const getPollFunctions = this.getExecutePollFunctions( + workflowData, + additionalData, + mode, + activation, + ); // Add the workflows which have webhooks defined await this.addWorkflowWebhooks(workflowInstance, additionalData, mode, activation); - if (workflowInstance.getTriggerNodes().length !== 0 - || workflowInstance.getPollNodes().length !== 0) { - await this.activeWorkflows.add(workflowId, workflowInstance, additionalData, mode, activation, getTriggerFunctions, getPollFunctions); - Logger.verbose(`Successfully activated workflow "${workflowData.name}"`, { workflowId, workflowName: workflowData.name }); + if ( + workflowInstance.getTriggerNodes().length !== 0 || + workflowInstance.getPollNodes().length !== 0 + ) { + await this.activeWorkflows.add( + workflowId, + workflowInstance, + additionalData, + mode, + activation, + getTriggerFunctions, + getPollFunctions, + ); + Logger.verbose(`Successfully activated workflow "${workflowData.name}"`, { + workflowId, + workflowName: workflowData.name, + }); } if (this.activationErrors[workflowId] !== undefined) { @@ -548,13 +746,15 @@ export class ActiveWorkflowRunner { * @memberof ActiveWorkflowRunner */ async remove(workflowId: string): Promise { - if (this.activeWorkflows !== null) { // Remove all the webhooks of the workflow try { await this.removeWorkflowWebhooks(workflowId); } catch (error) { - console.error(`Could not remove webhooks of workflow "${workflowId}" because of error: "${error.message}"`); + console.error( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Could not remove webhooks of workflow "${workflowId}" because of error: "${error.message}"`, + ); } if (this.activationErrors[workflowId] !== undefined) { @@ -576,8 +776,6 @@ export class ActiveWorkflowRunner { } } - - let workflowRunnerInstance: ActiveWorkflowRunner | undefined; export function getInstance(): ActiveWorkflowRunner { diff --git a/packages/cli/src/CredentialTypes.ts b/packages/cli/src/CredentialTypes.ts index 4eb8739ad1..f9c3504377 100644 --- a/packages/cli/src/CredentialTypes.ts +++ b/packages/cli/src/CredentialTypes.ts @@ -1,32 +1,30 @@ -import { - ICredentialType, - ICredentialTypes as ICredentialTypesInterface, -} from 'n8n-workflow'; +import { ICredentialType, ICredentialTypes as ICredentialTypesInterface } from 'n8n-workflow'; -import { - CredentialsOverwrites, - ICredentialsTypeData, -} from './'; +// eslint-disable-next-line import/no-cycle +import { CredentialsOverwrites, ICredentialsTypeData } from '.'; class CredentialTypesClass implements ICredentialTypesInterface { - credentialTypes: ICredentialsTypeData = {}; - async init(credentialTypes: ICredentialsTypeData): Promise { this.credentialTypes = credentialTypes; // Load the credentials overwrites if any exist const credentialsOverwrites = CredentialsOverwrites().getAll(); + // eslint-disable-next-line no-restricted-syntax for (const credentialType of Object.keys(credentialsOverwrites)) { if (credentialTypes[credentialType] === undefined) { + // eslint-disable-next-line no-continue continue; } // Add which properties got overwritten that the Editor-UI knows // which properties it should hide - credentialTypes[credentialType].__overwrittenProperties = Object.keys(credentialsOverwrites[credentialType]); + // eslint-disable-next-line no-underscore-dangle, no-param-reassign + credentialTypes[credentialType].__overwrittenProperties = Object.keys( + credentialsOverwrites[credentialType], + ); } } @@ -39,10 +37,9 @@ class CredentialTypesClass implements ICredentialTypesInterface { } } - - let credentialTypesInstance: CredentialTypesClass | undefined; +// eslint-disable-next-line @typescript-eslint/naming-convention export function CredentialTypes(): CredentialTypesClass { if (credentialTypesInstance === undefined) { credentialTypesInstance = new CredentialTypesClass(); diff --git a/packages/cli/src/CredentialsHelper.ts b/packages/cli/src/CredentialsHelper.ts index fab915f73e..7f7330b75f 100644 --- a/packages/cli/src/CredentialsHelper.ts +++ b/packages/cli/src/CredentialsHelper.ts @@ -1,6 +1,4 @@ -import { - Credentials, -} from 'n8n-core'; +import { Credentials } from 'n8n-core'; import { ICredentialDataDecryptedObject, @@ -17,29 +15,24 @@ import { WorkflowExecuteMode, } from 'n8n-workflow'; -import { - CredentialsOverwrites, - CredentialTypes, - Db, - ICredentialsDb, -} from './'; - +// eslint-disable-next-line import/no-cycle +import { CredentialsOverwrites, CredentialTypes, Db, ICredentialsDb } from '.'; const mockNodeTypes: INodeTypes = { nodeTypes: {}, - init: async (nodeTypes?: INodeTypeData): Promise => { }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + init: async (nodeTypes?: INodeTypeData): Promise => {}, getAll: (): INodeType[] => { // Does not get used in Workflow so no need to return it return []; }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars getByName: (nodeType: string): INodeType | undefined => { return undefined; }, }; - export class CredentialsHelper extends ICredentialsHelper { - /** * Returns the credentials instance * @@ -49,22 +42,26 @@ export class CredentialsHelper extends ICredentialsHelper { * @memberof CredentialsHelper */ async getCredentials(name: string, type: string): Promise { - - const credentialsDb = await Db.collections.Credentials?.find({type}); + const credentialsDb = await Db.collections.Credentials?.find({ type }); if (credentialsDb === undefined || credentialsDb.length === 0) { throw new Error(`No credentials of type "${type}" exist.`); } - const credential = credentialsDb.find(credential => credential.name === name); + // eslint-disable-next-line @typescript-eslint/no-shadow + const credential = credentialsDb.find((credential) => credential.name === name); if (credential === undefined) { throw new Error(`No credentials with name "${name}" exist for type "${type}".`); } - - return new Credentials(credential.name, credential.type, credential.nodesAccess, credential.data); - } + return new Credentials( + credential.name, + credential.type, + credential.nodesAccess, + credential.data, + ); + } /** * Returns all the properties of the credentials with the given name @@ -86,6 +83,7 @@ export class CredentialsHelper extends ICredentialsHelper { } const combineProperties = [] as INodeProperties[]; + // eslint-disable-next-line no-restricted-syntax for (const credentialsTypeName of credentialTypeData.extends) { const mergeCredentialProperties = this.getCredentialsProperties(credentialsTypeName); NodeHelpers.mergeNodeProperties(combineProperties, mergeCredentialProperties); @@ -97,7 +95,6 @@ export class CredentialsHelper extends ICredentialsHelper { return combineProperties; } - /** * Returns the decrypted credential data with applied overwrites * @@ -107,7 +104,13 @@ export class CredentialsHelper extends ICredentialsHelper { * @returns {ICredentialDataDecryptedObject} * @memberof CredentialsHelper */ - async getDecrypted(name: string, type: string, mode: WorkflowExecuteMode, raw?: boolean, expressionResolveValues?: ICredentialsExpressionResolveValues): Promise { + async getDecrypted( + name: string, + type: string, + mode: WorkflowExecuteMode, + raw?: boolean, + expressionResolveValues?: ICredentialsExpressionResolveValues, + ): Promise { const credentials = await this.getCredentials(name, type); const decryptedDataOriginal = credentials.getData(this.encryptionKey); @@ -116,10 +119,14 @@ export class CredentialsHelper extends ICredentialsHelper { return decryptedDataOriginal; } - return this.applyDefaultsAndOverwrites(decryptedDataOriginal, type, mode, expressionResolveValues); + return this.applyDefaultsAndOverwrites( + decryptedDataOriginal, + type, + mode, + expressionResolveValues, + ); } - /** * Applies credential default data and overwrites * @@ -128,11 +135,21 @@ export class CredentialsHelper extends ICredentialsHelper { * @returns {ICredentialDataDecryptedObject} * @memberof CredentialsHelper */ - applyDefaultsAndOverwrites(decryptedDataOriginal: ICredentialDataDecryptedObject, type: string, mode: WorkflowExecuteMode, expressionResolveValues?: ICredentialsExpressionResolveValues): ICredentialDataDecryptedObject { + applyDefaultsAndOverwrites( + decryptedDataOriginal: ICredentialDataDecryptedObject, + type: string, + mode: WorkflowExecuteMode, + expressionResolveValues?: ICredentialsExpressionResolveValues, + ): ICredentialDataDecryptedObject { const credentialsProperties = this.getCredentialsProperties(type); // Add the default credential values - let 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 @@ -142,9 +159,26 @@ export class CredentialsHelper extends ICredentialsHelper { if (expressionResolveValues) { try { - const workflow = new Workflow({ nodes: Object.values(expressionResolveValues.workflow.nodes), connections: expressionResolveValues.workflow.connectionsBySourceNode, active: false, nodeTypes: expressionResolveValues.workflow.nodeTypes }); - decryptedData = workflow.expression.getParameterValue(decryptedData as INodeParameters, expressionResolveValues.runExecutionData, expressionResolveValues.runIndex, expressionResolveValues.itemIndex, expressionResolveValues.node.name, expressionResolveValues.connectionInputData, mode, {}, false, decryptedData) as ICredentialDataDecryptedObject; + const workflow = new Workflow({ + nodes: Object.values(expressionResolveValues.workflow.nodes), + connections: expressionResolveValues.workflow.connectionsBySourceNode, + active: false, + nodeTypes: expressionResolveValues.workflow.nodeTypes, + }); + decryptedData = workflow.expression.getParameterValue( + decryptedData as INodeParameters, + expressionResolveValues.runExecutionData, + expressionResolveValues.runIndex, + expressionResolveValues.itemIndex, + expressionResolveValues.node.name, + expressionResolveValues.connectionInputData, + mode, + {}, + false, + decryptedData, + ) as ICredentialDataDecryptedObject; } catch (e) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access e.message += ' [Error resolving credentials]'; throw e; } @@ -157,18 +191,30 @@ export class CredentialsHelper extends ICredentialsHelper { parameters: {} as INodeParameters, } as INode; - const workflow = new Workflow({ nodes: [node!], connections: {}, active: false, nodeTypes: mockNodeTypes }); + const workflow = new Workflow({ + nodes: [node], + connections: {}, + active: false, + nodeTypes: mockNodeTypes, + }); // Resolve expressions if any are set - decryptedData = workflow.expression.getComplexParameterValue(node!, decryptedData as INodeParameters, mode, {}, undefined, decryptedData) as ICredentialDataDecryptedObject; + decryptedData = workflow.expression.getComplexParameterValue( + node, + decryptedData as INodeParameters, + mode, + {}, + undefined, + decryptedData, + ) as ICredentialDataDecryptedObject; } // Load and apply the credentials overwrites if any exist const credentialsOverwrites = CredentialsOverwrites(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return credentialsOverwrites.applyOverwrite(type, decryptedData); } - /** * Updates credentials in the database * @@ -178,10 +224,15 @@ export class CredentialsHelper extends ICredentialsHelper { * @returns {Promise} * @memberof CredentialsHelper */ - async updateCredentials(name: string, type: string, data: ICredentialDataDecryptedObject): Promise { + async updateCredentials( + name: string, + type: string, + data: ICredentialDataDecryptedObject, + ): Promise { + // eslint-disable-next-line @typescript-eslint/await-thenable const credentials = await this.getCredentials(name, type); - if (Db.collections!.Credentials === null) { + if (Db.collections.Credentials === null) { // The first time executeWorkflow gets called the Database has // to get initialized first await Db.init(); @@ -201,7 +252,7 @@ export class CredentialsHelper extends ICredentialsHelper { type, }; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion await Db.collections.Credentials!.update(findQuery, newCredentialsData); } - } diff --git a/packages/cli/src/CredentialsOverwrites.ts b/packages/cli/src/CredentialsOverwrites.ts index 5d202ce6b6..4fbdbf3e61 100644 --- a/packages/cli/src/CredentialsOverwrites.ts +++ b/packages/cli/src/CredentialsOverwrites.ts @@ -1,20 +1,15 @@ -import { - ICredentialDataDecryptedObject, -} from 'n8n-workflow'; - -import { - CredentialTypes, - GenericHelpers, - ICredentialsOverwrite, -} from './'; +/* eslint-disable no-underscore-dangle */ +import { ICredentialDataDecryptedObject } from 'n8n-workflow'; +// eslint-disable-next-line import/no-cycle +import { CredentialTypes, GenericHelpers, ICredentialsOverwrite } from '.'; class CredentialsOverwritesClass { - private credentialTypes = CredentialTypes(); - private overwriteData: ICredentialsOverwrite = {}; - private resolvedTypes: string[] = []; + private overwriteData: ICredentialsOverwrite = {}; + + private resolvedTypes: string[] = []; async init(overwriteData?: ICredentialsOverwrite) { if (overwriteData !== undefined) { @@ -24,9 +19,10 @@ class CredentialsOverwritesClass { return; } - const data = await GenericHelpers.getConfigValue('credentials.overwrite.data') as string; + const data = (await GenericHelpers.getConfigValue('credentials.overwrite.data')) as string; try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-shadow const overwriteData = JSON.parse(data); this.__setData(overwriteData); } catch (error) { @@ -34,10 +30,10 @@ class CredentialsOverwritesClass { } } - __setData(overwriteData: ICredentialsOverwrite) { this.overwriteData = overwriteData; + // eslint-disable-next-line no-restricted-syntax for (const credentialTypeData of this.credentialTypes.getAll()) { const type = credentialTypeData.name; @@ -49,29 +45,30 @@ class CredentialsOverwritesClass { } } - applyOverwrite(type: string, data: ICredentialDataDecryptedObject) { - const overwrites = this.get(type); if (overwrites === undefined) { return data; } + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const returnData = JSON.parse(JSON.stringify(data)); // Overwrite only if there is currently no data set + // eslint-disable-next-line no-restricted-syntax for (const key of Object.keys(overwrites)) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if ([null, undefined, ''].includes(returnData[key])) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access returnData[key] = overwrites[key]; } } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return 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]; @@ -89,6 +86,7 @@ class CredentialsOverwritesClass { } const overwrites: ICredentialDataDecryptedObject = {}; + // eslint-disable-next-line no-restricted-syntax for (const credentialsTypeName of credentialTypeData.extends) { Object.assign(overwrites, this.__getExtended(credentialsTypeName)); } @@ -102,20 +100,18 @@ class CredentialsOverwritesClass { return overwrites; } - get(type: string): ICredentialDataDecryptedObject | undefined { return this.overwriteData[type]; } - getAll(): ICredentialsOverwrite { return this.overwriteData; } } - let credentialsOverwritesInstance: CredentialsOverwritesClass | undefined; +// eslint-disable-next-line @typescript-eslint/naming-convention export function CredentialsOverwrites(): CredentialsOverwritesClass { if (credentialsOverwritesInstance === undefined) { credentialsOverwritesInstance = new CredentialsOverwritesClass(); diff --git a/packages/cli/src/Db.ts b/packages/cli/src/Db.ts index c812524b1d..c8b6a624e0 100644 --- a/packages/cli/src/Db.ts +++ b/packages/cli/src/Db.ts @@ -1,26 +1,24 @@ -import { - DatabaseType, - GenericHelpers, - IDatabaseCollections, -} from './'; - -import { - UserSettings, -} from 'n8n-core'; - -import { - ConnectionOptions, - createConnection, - getRepository, -} from 'typeorm'; - +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable no-case-declarations */ +/* eslint-disable @typescript-eslint/naming-convention */ +import { UserSettings } from 'n8n-core'; +import { ConnectionOptions, createConnection, getRepository } from 'typeorm'; import { TlsOptions } from 'tls'; +import * as path from 'path'; +// eslint-disable-next-line import/no-cycle +import { DatabaseType, GenericHelpers, IDatabaseCollections } from '.'; import * as config from '../config'; +// eslint-disable-next-line import/no-cycle import { entities } from './databases/entities'; -export let collections: IDatabaseCollections = { +import { postgresMigrations } from './databases/postgresdb/migrations'; +import { mysqlMigrations } from './databases/mysqldb/migrations'; +import { sqliteMigrations } from './databases/sqlite/migrations'; + +export const collections: IDatabaseCollections = { Credentials: null, Execution: null, Workflow: null, @@ -28,14 +26,8 @@ export let collections: IDatabaseCollections = { Tag: null, }; -import { postgresMigrations } from './databases/postgresdb/migrations'; -import { mysqlMigrations } from './databases/mysqldb/migrations'; -import { sqliteMigrations } from './databases/sqlite/migrations'; - -import * as path from 'path'; - export async function init(): Promise { - const dbType = await GenericHelpers.getConfigValue('database.type') as DatabaseType; + const dbType = (await GenericHelpers.getConfigValue('database.type')) as DatabaseType; const n8nFolder = UserSettings.getUserN8nFolderPath(); let connectionOptions: ConnectionOptions; @@ -44,13 +36,17 @@ export async function init(): Promise { switch (dbType) { case 'postgresdb': - const sslCa = await GenericHelpers.getConfigValue('database.postgresdb.ssl.ca') as string; - const sslCert = await GenericHelpers.getConfigValue('database.postgresdb.ssl.cert') as string; - const sslKey = await GenericHelpers.getConfigValue('database.postgresdb.ssl.key') as string; - const sslRejectUnauthorized = await GenericHelpers.getConfigValue('database.postgresdb.ssl.rejectUnauthorized') as boolean; + const sslCa = (await GenericHelpers.getConfigValue('database.postgresdb.ssl.ca')) as string; + const sslCert = (await GenericHelpers.getConfigValue( + 'database.postgresdb.ssl.cert', + )) as string; + const sslKey = (await GenericHelpers.getConfigValue('database.postgresdb.ssl.key')) as string; + const sslRejectUnauthorized = (await GenericHelpers.getConfigValue( + 'database.postgresdb.ssl.rejectUnauthorized', + )) as boolean; - let ssl: TlsOptions | undefined = undefined; - if (sslCa !== '' || sslCert !== '' || sslKey !== '' || sslRejectUnauthorized !== true) { + let ssl: TlsOptions | undefined; + if (sslCa !== '' || sslCert !== '' || sslKey !== '' || !sslRejectUnauthorized) { ssl = { ca: sslCa || undefined, cert: sslCert || undefined, @@ -62,11 +58,11 @@ export async function init(): Promise { connectionOptions = { type: 'postgres', entityPrefix, - database: await GenericHelpers.getConfigValue('database.postgresdb.database') as string, - host: await GenericHelpers.getConfigValue('database.postgresdb.host') as string, - password: await GenericHelpers.getConfigValue('database.postgresdb.password') as string, - port: await GenericHelpers.getConfigValue('database.postgresdb.port') as number, - username: await GenericHelpers.getConfigValue('database.postgresdb.user') as string, + database: (await GenericHelpers.getConfigValue('database.postgresdb.database')) as string, + host: (await GenericHelpers.getConfigValue('database.postgresdb.host')) as string, + password: (await GenericHelpers.getConfigValue('database.postgresdb.password')) as string, + port: (await GenericHelpers.getConfigValue('database.postgresdb.port')) as number, + username: (await GenericHelpers.getConfigValue('database.postgresdb.user')) as string, schema: config.get('database.postgresdb.schema'), migrations: postgresMigrations, migrationsRun: true, @@ -80,12 +76,12 @@ export async function init(): Promise { case 'mysqldb': connectionOptions = { type: dbType === 'mysqldb' ? 'mysql' : 'mariadb', - database: await GenericHelpers.getConfigValue('database.mysqldb.database') as string, + database: (await GenericHelpers.getConfigValue('database.mysqldb.database')) as string, entityPrefix, - host: await GenericHelpers.getConfigValue('database.mysqldb.host') as string, - password: await GenericHelpers.getConfigValue('database.mysqldb.password') as string, - port: await GenericHelpers.getConfigValue('database.mysqldb.port') as number, - username: await GenericHelpers.getConfigValue('database.mysqldb.user') as string, + host: (await GenericHelpers.getConfigValue('database.mysqldb.host')) as string, + password: (await GenericHelpers.getConfigValue('database.mysqldb.password')) as string, + port: (await GenericHelpers.getConfigValue('database.mysqldb.port')) as number, + username: (await GenericHelpers.getConfigValue('database.mysqldb.user')) as string, migrations: mysqlMigrations, migrationsRun: true, migrationsTableName: `${entityPrefix}migrations`, @@ -106,7 +102,7 @@ export async function init(): Promise { default: throw new Error(`The database "${dbType}" is currently not supported!`); - } + } Object.assign(connectionOptions, { entities: Object.values(entities), @@ -122,8 +118,10 @@ export async function init(): Promise { // n8n knows it has changed. Happens only on sqlite. let migrations = []; try { - migrations = await connection.query(`SELECT id FROM ${entityPrefix}migrations where name = "MakeStoppedAtNullable1607431743769"`); - } catch(error) { + migrations = await connection.query( + `SELECT id FROM ${entityPrefix}migrations where name = "MakeStoppedAtNullable1607431743769"`, + ); + } catch (error) { // Migration table does not exist yet - it will be created after migrations run for the first time. } @@ -133,6 +131,7 @@ export async function init(): Promise { transaction: 'none', }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (migrations.length === 0) { await connection.close(); connection = await createConnection(connectionOptions); diff --git a/packages/cli/src/ExternalHooks.ts b/packages/cli/src/ExternalHooks.ts index aa14662a60..8578f61bc8 100644 --- a/packages/cli/src/ExternalHooks.ts +++ b/packages/cli/src/ExternalHooks.ts @@ -1,23 +1,20 @@ -import { - Db, - IExternalHooksClass, - IExternalHooksFileData, - IExternalHooksFunctions, -} from './'; +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable import/no-dynamic-require */ +/* eslint-disable no-restricted-syntax */ +// eslint-disable-next-line import/no-cycle +import { Db, IExternalHooksClass, IExternalHooksFileData, IExternalHooksFunctions } from '.'; import * as config from '../config'; - class ExternalHooksClass implements IExternalHooksClass { - externalHooks: { - [key: string]: Array<() => {}> + [key: string]: Array<() => {}>; } = {}; + initDidRun = false; - async init(): Promise { - if (this.initDidRun === true) { + if (this.initDidRun) { return; } @@ -26,7 +23,6 @@ class ExternalHooksClass implements IExternalHooksClass { this.initDidRun = true; } - async reload(externalHooks?: IExternalHooksFileData) { this.externalHooks = {}; @@ -37,7 +33,6 @@ class ExternalHooksClass implements IExternalHooksClass { } } - async loadHooksFiles(reload = false) { const externalHookFiles = config.get('externalHookFiles').split(':'); @@ -46,21 +41,22 @@ class ExternalHooksClass implements IExternalHooksClass { hookFilePath = hookFilePath.trim(); if (hookFilePath !== '') { try { - - if (reload === true) { + if (reload) { delete require.cache[require.resolve(hookFilePath)]; } + // eslint-disable-next-line import/no-dynamic-require + // eslint-disable-next-line global-require const hookFile = require(hookFilePath) as IExternalHooksFileData; this.loadHooks(hookFile); } catch (error) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access throw new Error(`Problem loading external hook file "${hookFilePath}": ${error.message}`); } } } } - loadHooks(hookFileData: IExternalHooksFileData) { for (const resource of Object.keys(hookFileData)) { for (const operation of Object.keys(hookFileData[resource])) { @@ -71,13 +67,17 @@ class ExternalHooksClass implements IExternalHooksClass { this.externalHooks[hookString] = []; } - this.externalHooks[hookString].push.apply(this.externalHooks[hookString], hookFileData[resource][operation]); + // eslint-disable-next-line prefer-spread + this.externalHooks[hookString].push.apply( + this.externalHooks[hookString], + hookFileData[resource][operation], + ); } } } - - async run(hookName: string, hookParameters?: any[]): Promise { // tslint:disable-line:no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async run(hookName: string, hookParameters?: any[]): Promise { const externalHookFunctions: IExternalHooksFunctions = { dbCollections: Db.collections, }; @@ -86,22 +86,20 @@ class ExternalHooksClass implements IExternalHooksClass { return; } - for(const externalHookFunction of this.externalHooks[hookName]) { + for (const externalHookFunction of this.externalHooks[hookName]) { + // eslint-disable-next-line no-await-in-loop await externalHookFunction.apply(externalHookFunctions, hookParameters); } } - exists(hookName: string): boolean { return !!this.externalHooks[hookName]; } - } - - let externalHooksInstance: ExternalHooksClass | undefined; +// eslint-disable-next-line @typescript-eslint/naming-convention export function ExternalHooks(): ExternalHooksClass { if (externalHooksInstance === undefined) { externalHooksInstance = new ExternalHooksClass(); diff --git a/packages/cli/src/GenericHelpers.ts b/packages/cli/src/GenericHelpers.ts index d6da9e87da..82cf992b07 100644 --- a/packages/cli/src/GenericHelpers.ts +++ b/packages/cli/src/GenericHelpers.ts @@ -1,11 +1,17 @@ -import * as config from '../config'; +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable no-param-reassign */ +/* eslint-disable no-underscore-dangle */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import * as express from 'express'; import { join as pathJoin } from 'path'; import { readFile as fsReadFile } from 'fs/promises'; import { readFileSync as fsReadFileSync } from 'fs'; import { IDataObject } from 'n8n-workflow'; +import * as config from '../config'; -import { IPackageVersions } from './'; +// eslint-disable-next-line import/no-cycle +import { IPackageVersions } from '.'; let versionCache: IPackageVersions | undefined; @@ -16,18 +22,17 @@ let versionCache: IPackageVersions | undefined; * @returns {string} */ export function getBaseUrl(): string { - const protocol = config.get('protocol') as string; - const host = config.get('host') as string; - const port = config.get('port') as number; - const path = config.get('path') as string; + const protocol = config.get('protocol'); + const host = config.get('host'); + const port = config.get('port'); + const path = config.get('path'); - if (protocol === 'http' && port === 80 || protocol === 'https' && port === 443) { + if ((protocol === 'http' && port === 80) || (protocol === 'https' && port === 443)) { return `${protocol}://${host}${path}`; } return `${protocol}://${host}:${port}${path}`; } - /** * Returns the session id if one is set * @@ -39,7 +44,6 @@ export function getSessionId(req: express.Request): string | undefined { return req.headers.sessionid as string | undefined; } - /** * Returns information which version of the packages are installed * @@ -51,10 +55,12 @@ export async function getVersions(): Promise { return versionCache; } - const packageFile = await fsReadFile(pathJoin(__dirname, '../../package.json'), 'utf8') as string; + const packageFile = await fsReadFile(pathJoin(__dirname, '../../package.json'), 'utf8'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const packageData = JSON.parse(packageFile); versionCache = { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment cli: packageData.version, }; @@ -71,9 +77,11 @@ export async function getVersions(): Promise { function extractSchemaForKey(configKey: string, configSchema: IDataObject): IDataObject { const configKeyParts = configKey.split('.'); + // eslint-disable-next-line no-restricted-syntax for (const key of configKeyParts) { if (configSchema[key] === undefined) { throw new Error(`Key "${key}" of ConfigKey "${configKey}" does not exist`); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion } else if ((configSchema[key]! as IDataObject)._cvtProperties === undefined) { configSchema = configSchema[key] as IDataObject; } else { @@ -90,7 +98,9 @@ function extractSchemaForKey(configKey: string, configSchema: IDataObject): IDat * @param {string} configKey The key of the config data to get * @returns {(Promise)} */ -export async function getConfigValue(configKey: string): Promise { +export async function getConfigValue( + configKey: string, +): Promise { // Get the environment variable const configSchema = config.getSchema(); // @ts-ignore @@ -102,7 +112,7 @@ export async function getConfigValue(configKey: string): Promise; }> - delete?: Array<{ (this: IExternalHooksFunctions, credentialId: string): Promise; }> - update?: Array<{ (this: IExternalHooksFunctions, credentialsData: ICredentialsDb): Promise; }> + create?: Array<{ + (this: IExternalHooksFunctions, credentialsData: ICredentialsEncrypted): Promise; + }>; + delete?: Array<{ (this: IExternalHooksFunctions, credentialId: string): Promise }>; + update?: Array<{ + (this: IExternalHooksFunctions, credentialsData: ICredentialsDb): Promise; + }>; }; workflow?: { - activate?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb): Promise; }> - create?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowBase): Promise; }> - delete?: Array<{ (this: IExternalHooksFunctions, workflowId: string): Promise; }> - execute?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb, mode: WorkflowExecuteMode): Promise; }> - update?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb): Promise; }> + activate?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb): Promise }>; + create?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowBase): Promise }>; + delete?: Array<{ (this: IExternalHooksFunctions, workflowId: string): Promise }>; + execute?: Array<{ + ( + this: IExternalHooksFunctions, + workflowData: IWorkflowDb, + mode: WorkflowExecuteMode, + ): Promise; + }>; + update?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb): Promise }>; }; } export interface IExternalHooksFileData { [key: string]: { - [key: string]: Array<(...args: any[]) => Promise>; //tslint:disable-line:no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: Array<(...args: any[]) => Promise>; }; } @@ -265,7 +276,8 @@ export interface IExternalHooksFunctions { export interface IExternalHooksClass { init(): Promise; - run(hookName: string, hookParameters?: any[]): Promise; // tslint:disable-line:no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any + run(hookName: string, hookParameters?: any[]): Promise; } export interface IN8nConfig { @@ -295,12 +307,14 @@ export interface IN8nConfigEndpoints { webhookTest: string; } +// eslint-disable-next-line import/export export interface IN8nConfigExecutions { saveDataOnError: SaveExecutionDataType; saveDataOnSuccess: SaveExecutionDataType; saveDataManualExecutions: boolean; } +// eslint-disable-next-line import/export export interface IN8nConfigExecutions { saveDataOnError: SaveExecutionDataType; saveDataOnSuccess: SaveExecutionDataType; @@ -409,13 +423,11 @@ export interface IPushDataNodeExecuteAfter { nodeName: string; } - export interface IPushDataNodeExecuteBefore { executionId: string; nodeName: string; } - export interface IPushDataTestWebhook { executionId: string; workflowId: string; @@ -432,7 +444,6 @@ export interface IResponseCallbackData { responseCode?: number; } - export interface ITransferNodeTypes { [key: string]: { className: string; @@ -440,7 +451,6 @@ export interface ITransferNodeTypes { }; } - export interface IWorkflowErrorData { [key: string]: IDataObject | string | number | ExecutionError; execution: { @@ -457,7 +467,8 @@ export interface IWorkflowErrorData { export interface IProcessMessageDataHook { hook: string; - parameters: any[]; // tslint:disable-line:no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any + parameters: any[]; } export interface IWorkflowExecutionDataProcess { @@ -471,7 +482,6 @@ export interface IWorkflowExecutionDataProcess { workflowData: IWorkflowBase; } - export interface IWorkflowExecutionDataProcessWithExecution extends IWorkflowExecutionDataProcess { credentialsOverwrite: ICredentialsOverwrite; credentialsTypeData: ICredentialsTypeData; diff --git a/packages/cli/src/LoadNodesAndCredentials.ts b/packages/cli/src/LoadNodesAndCredentials.ts index 037293dfc2..d39453fec8 100644 --- a/packages/cli/src/LoadNodesAndCredentials.ts +++ b/packages/cli/src/LoadNodesAndCredentials.ts @@ -1,7 +1,14 @@ -import { - CUSTOM_EXTENSION_ENV, - UserSettings, -} from 'n8n-core'; +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable no-prototype-builtins */ +/* eslint-disable no-param-reassign */ +/* eslint-disable @typescript-eslint/prefer-optional-chain */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable no-await-in-loop */ +/* eslint-disable no-continue */ +/* eslint-disable no-restricted-syntax */ +import { CUSTOM_EXTENSION_ENV, UserSettings } from 'n8n-core'; import { CodexData, ICredentialType, @@ -11,12 +18,6 @@ import { LoggerProxy, } from 'n8n-workflow'; -import * as config from '../config'; - -import { - getLogger, -} from '../src/Logger'; - import { access as fsAccess, readdir as fsReaddir, @@ -25,18 +26,20 @@ import { } from 'fs/promises'; import * as glob from 'fast-glob'; import * as path from 'path'; +import { getLogger } from './Logger'; +import * as config from '../config'; const CUSTOM_NODES_CATEGORY = 'Custom Nodes'; - class LoadNodesAndCredentialsClass { nodeTypes: INodeTypeData = {}; credentialTypes: { - [key: string]: ICredentialType + [key: string]: ICredentialType; } = {}; excludeNodes: string[] | undefined = undefined; + includeNodes: string[] | undefined = undefined; nodeModulesPath = ''; @@ -64,6 +67,7 @@ class LoadNodesAndCredentialsClass { break; } catch (error) { // Folder does not exist so get next one + // eslint-disable-next-line no-continue continue; } } @@ -90,7 +94,9 @@ class LoadNodesAndCredentialsClass { // Add folders from special environment variable if (process.env[CUSTOM_EXTENSION_ENV] !== undefined) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const customExtensionFolders = process.env[CUSTOM_EXTENSION_ENV]!.split(';'); + // eslint-disable-next-line prefer-spread customDirectories.push.apply(customDirectories, customExtensionFolders); } @@ -99,7 +105,6 @@ class LoadNodesAndCredentialsClass { } } - /** * Returns all the names of the packages which could * contain n8n nodes @@ -120,9 +125,11 @@ class LoadNodesAndCredentialsClass { if (!(await fsStat(nodeModulesPath)).isDirectory()) { continue; } - if (isN8nNodesPackage) { results.push(`${relativePath}${file}`); } + if (isN8nNodesPackage) { + results.push(`${relativePath}${file}`); + } if (isNpmScopedPackage) { - results.push(...await getN8nNodePackagesRecursive(`${relativePath}${file}/`)); + results.push(...(await getN8nNodePackagesRecursive(`${relativePath}${file}/`))); } } return results; @@ -138,6 +145,7 @@ class LoadNodesAndCredentialsClass { * @returns {Promise} */ async loadCredentialsFromFile(credentialName: string, filePath: string): Promise { + // eslint-disable-next-line import/no-dynamic-require, global-require, @typescript-eslint/no-var-requires const tempModule = require(filePath); let tempCredential: ICredentialType; @@ -145,7 +153,9 @@ class LoadNodesAndCredentialsClass { tempCredential = new tempModule[credentialName]() as ICredentialType; } catch (e) { if (e instanceof TypeError) { - throw new Error(`Class with name "${credentialName}" could not be found. Please check if the class is named correctly!`); + throw new Error( + `Class with name "${credentialName}" could not be found. Please check if the class is named correctly!`, + ); } else { throw e; } @@ -154,7 +164,6 @@ class LoadNodesAndCredentialsClass { this.credentialTypes[tempCredential.name] = tempCredential; } - /** * Loads a node from a file * @@ -167,26 +176,34 @@ class LoadNodesAndCredentialsClass { let tempNode: INodeType; let fullNodeName: string; + // eslint-disable-next-line import/no-dynamic-require, global-require, @typescript-eslint/no-var-requires const tempModule = require(filePath); try { tempNode = new tempModule[nodeName]() as INodeType; this.addCodex({ node: tempNode, filePath, isCustom: packageName === 'CUSTOM' }); } catch (error) { + // eslint-disable-next-line no-console console.error(`Error loading node "${nodeName}" from: "${filePath}"`); throw error; } - fullNodeName = packageName + '.' + tempNode.description.name; + // eslint-disable-next-line prefer-const + fullNodeName = `${packageName}.${tempNode.description.name}`; tempNode.description.name = fullNodeName; - if (tempNode.description.icon !== undefined && - tempNode.description.icon.startsWith('file:')) { + if (tempNode.description.icon !== undefined && tempNode.description.icon.startsWith('file:')) { // If a file icon gets used add the full path - tempNode.description.icon = 'file:' + path.join(path.dirname(filePath), tempNode.description.icon.substr(5)); + tempNode.description.icon = `file:${path.join( + path.dirname(filePath), + tempNode.description.icon.substr(5), + )}`; } if (tempNode.executeSingle) { - this.logger.warn(`"executeSingle" will get deprecated soon. Please update the code of node "${packageName}.${nodeName}" to use "execute" instead!`, { filePath }); + this.logger.warn( + `"executeSingle" will get deprecated soon. Please update the code of node "${packageName}.${nodeName}" to use "execute" instead!`, + { filePath }, + ); } if (this.includeNodes !== undefined && !this.includeNodes.includes(fullNodeName)) { @@ -212,7 +229,9 @@ class LoadNodesAndCredentialsClass { * @returns {CodexData} */ getCodex(filePath: string): CodexData { + // eslint-disable-next-line global-require, import/no-dynamic-require, @typescript-eslint/no-var-requires const { categories, subcategories, alias } = require(`${filePath}on`); // .js to .json + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return { ...(categories && { categories }), ...(subcategories && { subcategories }), @@ -230,11 +249,7 @@ class LoadNodesAndCredentialsClass { * @param obj.isCustom Whether the node is custom * @returns {void} */ - addCodex({ node, filePath, isCustom }: { - node: INodeType; - filePath: string; - isCustom: boolean; - }) { + addCodex({ node, filePath, isCustom }: { node: INodeType; filePath: string; isCustom: boolean }) { try { const codex = this.getCodex(filePath); @@ -246,6 +261,7 @@ class LoadNodesAndCredentialsClass { node.description.codex = codex; } catch (_) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions this.logger.debug(`No codex available for: ${filePath.split('/').pop()}`); if (isCustom) { @@ -264,7 +280,7 @@ class LoadNodesAndCredentialsClass { * @returns {Promise} */ async loadDataFromDirectory(setPackageName: string, directory: string): Promise { - const files = await glob(path.join(directory, '**/*\.@(node|credentials)\.js')); + const files = await glob(path.join(directory, '**/*.@(node|credentials).js')); let fileName: string; let type: string; @@ -283,7 +299,6 @@ class LoadNodesAndCredentialsClass { await Promise.all(loadPromises); } - /** * Loads nodes and credentials from the package with the given name * @@ -301,10 +316,12 @@ class LoadNodesAndCredentialsClass { return; } - let tempPath: string, filePath: string; + let tempPath: string; + let filePath: string; // Read all node types - let fileName: string, type: string; + let fileName: string; + let type: string; if (packageFile.n8n.hasOwnProperty('nodes') && Array.isArray(packageFile.n8n.nodes)) { for (filePath of packageFile.n8n.nodes) { tempPath = path.join(packagePath, filePath); @@ -314,18 +331,21 @@ class LoadNodesAndCredentialsClass { } // Read all credential types - if (packageFile.n8n.hasOwnProperty('credentials') && Array.isArray(packageFile.n8n.credentials)) { + if ( + packageFile.n8n.hasOwnProperty('credentials') && + Array.isArray(packageFile.n8n.credentials) + ) { for (filePath of packageFile.n8n.credentials) { tempPath = path.join(packagePath, filePath); + // eslint-disable-next-line @typescript-eslint/no-unused-vars [fileName, type] = path.parse(filePath).name.split('.'); + // eslint-disable-next-line @typescript-eslint/no-floating-promises this.loadCredentialsFromFile(fileName, tempPath); } } } } - - let packagesInformationInstance: LoadNodesAndCredentialsClass | undefined; export function LoadNodesAndCredentials(): LoadNodesAndCredentialsClass { diff --git a/packages/cli/src/Logger.ts b/packages/cli/src/Logger.ts index 3bbc61e6a9..b5c7844378 100644 --- a/packages/cli/src/Logger.ts +++ b/packages/cli/src/Logger.ts @@ -1,23 +1,23 @@ -import config = require('../config'); +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import * as winston from 'winston'; -import { - IDataObject, - ILogger, - LogTypes, -} from 'n8n-workflow'; +import { IDataObject, ILogger, LogTypes } from 'n8n-workflow'; import * as callsites from 'callsites'; import { basename } from 'path'; +import config = require('../config'); class Logger implements ILogger { private logger: winston.Logger; constructor() { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const level = config.get('logs.level'); - const output = (config.get('logs.output') as string).split(',').map(output => output.trim()); + // eslint-disable-next-line @typescript-eslint/no-shadow + const output = (config.get('logs.output') as string).split(',').map((output) => output.trim()); this.logger = winston.createLogger({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment level, }); @@ -28,18 +28,22 @@ class Logger implements ILogger { winston.format.metadata(), winston.format.timestamp(), winston.format.colorize({ all: true }), + // eslint-disable-next-line @typescript-eslint/no-shadow winston.format.printf(({ level, message, timestamp, metadata }) => { - return `${timestamp} | ${level.padEnd(18)} | ${message}` + (Object.keys(metadata).length ? ` ${JSON.stringify(metadata)}` : ''); - }) as winston.Logform.Format + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + return `${timestamp} | ${level.padEnd(18)} | ${message}${ + Object.keys(metadata).length ? ` ${JSON.stringify(metadata)}` : '' + }`; + }), ); } else { - format = winston.format.printf(({ message }) => message) as winston.Logform.Format; + format = winston.format.printf(({ message }) => message); } this.logger.add( new winston.transports.Console({ format, - }) + }), ); } @@ -47,15 +51,15 @@ class Logger implements ILogger { const fileLogFormat = winston.format.combine( winston.format.timestamp(), winston.format.metadata(), - winston.format.json() + winston.format.json(), ); this.logger.add( new winston.transports.File({ filename: config.get('logs.file.location'), format: fileLogFormat, - maxsize: config.get('logs.file.fileSizeMax') as number * 1048576, // config * 1mb + maxsize: (config.get('logs.file.fileSizeMax') as number) * 1048576, // config * 1mb maxFiles: config.get('logs.file.fileCountMax'), - }) + }), ); } } @@ -70,13 +74,14 @@ class Logger implements ILogger { // We are in runtime, so it means we are looking at compiled js files const logDetails = {} as IDataObject; if (callsite[2] !== undefined) { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing logDetails.file = basename(callsite[2].getFileName() || ''); const functionName = callsite[2].getFunctionName(); if (functionName) { logDetails.function = functionName; } } - this.logger.log(type, message, {...meta, ...logDetails}); + this.logger.log(type, message, { ...meta, ...logDetails }); } // Convenience methods below @@ -100,11 +105,11 @@ class Logger implements ILogger { warn(message: string, meta: object = {}) { this.log('warn', message, meta); } - } let activeLoggerInstance: Logger | undefined; +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export function getLogger() { if (activeLoggerInstance === undefined) { activeLoggerInstance = new Logger(); diff --git a/packages/cli/src/NodeTypes.ts b/packages/cli/src/NodeTypes.ts index 0901ab7ed7..ce74fe5a0a 100644 --- a/packages/cli/src/NodeTypes.ts +++ b/packages/cli/src/NodeTypes.ts @@ -1,24 +1,21 @@ -import { - INodeType, - INodeTypeData, - INodeTypes, - NodeHelpers, -} from 'n8n-workflow'; - +import { INodeType, INodeTypeData, INodeTypes, NodeHelpers } from 'n8n-workflow'; class NodeTypesClass implements INodeTypes { - nodeTypes: INodeTypeData = {}; - async init(nodeTypes: INodeTypeData): Promise { // Some nodeTypes need to get special parameters applied like the // polling nodes the polling times + // eslint-disable-next-line no-restricted-syntax for (const nodeTypeData of Object.values(nodeTypes)) { const applyParameters = NodeHelpers.getSpecialNodeParameters(nodeTypeData.type); if (applyParameters.length) { - nodeTypeData.type.description.properties.unshift.apply(nodeTypeData.type.description.properties, applyParameters); + // eslint-disable-next-line prefer-spread + nodeTypeData.type.description.properties.unshift.apply( + nodeTypeData.type.description.properties, + applyParameters, + ); } } this.nodeTypes = nodeTypes; @@ -36,10 +33,9 @@ class NodeTypesClass implements INodeTypes { } } - - let nodeTypesInstance: NodeTypesClass | undefined; +// eslint-disable-next-line @typescript-eslint/naming-convention export function NodeTypes(): NodeTypesClass { if (nodeTypesInstance === undefined) { nodeTypesInstance = new NodeTypesClass(); diff --git a/packages/cli/src/Push.ts b/packages/cli/src/Push.ts index 78a20fde57..2ebac7768b 100644 --- a/packages/cli/src/Push.ts +++ b/packages/cli/src/Push.ts @@ -1,24 +1,22 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ // @ts-ignore import * as sseChannel from 'sse-channel'; import * as express from 'express'; -import { - IPushData, - IPushDataType, -} from '.'; - -import { - LoggerProxy as Logger, -} from 'n8n-workflow'; +import { LoggerProxy as Logger } from 'n8n-workflow'; +// eslint-disable-next-line import/no-cycle +import { IPushData, IPushDataType } from '.'; export class Push { private channel: sseChannel; + private connections: { [key: string]: express.Response; } = {}; - constructor() { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, new-cap this.channel = new sseChannel({ cors: { // Allow access also from frontend when developing @@ -26,6 +24,7 @@ export class Push { }, }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call this.channel.on('disconnect', (channel: string, res: express.Response) => { if (res.req !== undefined) { Logger.debug(`Remove editor-UI session`, { sessionId: res.req.query.sessionId }); @@ -34,7 +33,6 @@ export class Push { }); } - /** * Adds a new push connection * @@ -43,6 +41,7 @@ export class Push { * @param {express.Response} res The response * @memberof Push */ + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types add(sessionId: string, req: express.Request, res: express.Response) { Logger.debug(`Add editor-UI session`, { sessionId }); @@ -57,7 +56,6 @@ export class Push { this.channel.addClient(req, res); } - /** * Sends data to the client which is connected via a specific session * @@ -67,9 +65,8 @@ export class Push { * @memberof Push */ - - - send(type: IPushDataType, data: any, sessionId?: string) { // tslint:disable-line:no-any + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any + send(type: IPushDataType, data: any, sessionId?: string) { if (sessionId !== undefined && this.connections[sessionId] === undefined) { Logger.error(`The session "${sessionId}" is not registred.`, { sessionId }); return; @@ -79,6 +76,7 @@ export class Push { const sendData: IPushData = { type, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment data, }; @@ -89,7 +87,6 @@ export class Push { // Send only to a specific client this.channel.send(JSON.stringify(sendData), [this.connections[sessionId]]); } - } } diff --git a/packages/cli/src/Queue.ts b/packages/cli/src/Queue.ts index 1b85fe4bc4..9143c59ee4 100644 --- a/packages/cli/src/Queue.ts +++ b/packages/cli/src/Queue.ts @@ -1,14 +1,15 @@ import * as Bull from 'bull'; import * as config from '../config'; +// eslint-disable-next-line import/no-cycle import { IBullJobData } from './Interfaces'; export class Queue { private jobQueue: Bull.Queue; - + constructor() { const prefix = config.get('queue.bull.prefix') as string; const redisOptions = config.get('queue.bull.redis') as object; - // Disabling ready check is necessary as it allows worker to + // Disabling ready check is necessary as it allows worker to // quickly reconnect to Redis if Redis crashes or is unreachable // for some time. With it enabled, worker might take minutes to realize // redis is back up and resume working. @@ -16,25 +17,25 @@ export class Queue { // @ts-ignore this.jobQueue = new Bull('jobs', { prefix, redis: redisOptions, enableReadyCheck: false }); } - + async add(jobData: IBullJobData, jobOptions: object): Promise { - return await this.jobQueue.add(jobData,jobOptions); + return this.jobQueue.add(jobData, jobOptions); } - + async getJob(jobId: Bull.JobId): Promise { - return await this.jobQueue.getJob(jobId); + return this.jobQueue.getJob(jobId); } - + async getJobs(jobTypes: Bull.JobStatus[]): Promise { - return await this.jobQueue.getJobs(jobTypes); + return this.jobQueue.getJobs(jobTypes); } - + getBullObjectInstance(): Bull.Queue { return this.jobQueue; } /** - * + * * @param job A Bull.Job instance * @returns boolean true if we were able to securely stop the job */ @@ -43,15 +44,15 @@ export class Queue { // Job is already running so tell it to stop await job.progress(-1); return true; - } else { - // Job did not get started yet so remove from queue - try { - await job.remove(); - return true; - } catch (e) { - await job.progress(-1); - } } + // Job did not get started yet so remove from queue + try { + await job.remove(); + return true; + } catch (e) { + await job.progress(-1); + } + return false; } } @@ -62,6 +63,6 @@ export function getInstance(): Queue { if (activeQueueInstance === undefined) { activeQueueInstance = new Queue(); } - + return activeQueueInstance; } diff --git a/packages/cli/src/ResponseHelper.ts b/packages/cli/src/ResponseHelper.ts index bb447a91ba..02c431b64b 100644 --- a/packages/cli/src/ResponseHelper.ts +++ b/packages/cli/src/ResponseHelper.ts @@ -1,13 +1,19 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-console */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable no-param-reassign */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { Request, Response } from 'express'; import { parse, stringify } from 'flatted'; +// eslint-disable-next-line import/no-cycle import { IExecutionDb, IExecutionFlatted, IExecutionFlattedDb, IExecutionResponse, IWorkflowDb, -} from './'; +} from '.'; /** * Special Error which allows to return also an error code and http status code @@ -17,7 +23,6 @@ import { * @extends {Error} */ export class ResponseError extends Error { - // The HTTP status code of response httpStatusCode?: number; @@ -35,7 +40,7 @@ export class ResponseError extends Error { * @param {string} [hint] The error hint to provide a context (webhook related) * @memberof ResponseError */ - constructor(message: string, errorCode?: number, httpStatusCode?: number, hint?:string) { + constructor(message: string, errorCode?: number, httpStatusCode?: number, hint?: string) { super(message); this.name = 'ResponseError'; @@ -51,21 +56,23 @@ export class ResponseError extends Error { } } - - export function basicAuthAuthorizationError(resp: Response, realm: string, message?: string) { resp.statusCode = 401; resp.setHeader('WWW-Authenticate', `Basic realm="${realm}"`); - resp.json({code: resp.statusCode, message}); + resp.json({ code: resp.statusCode, message }); } export function jwtAuthAuthorizationError(resp: Response, message?: string) { resp.statusCode = 403; - resp.json({code: resp.statusCode, message}); + resp.json({ code: resp.statusCode, message }); } - -export function sendSuccessResponse(res: Response, data: any, raw?: boolean, responseCode?: number) { // tslint:disable-line:no-any +export function sendSuccessResponse( + res: Response, + data: any, + raw?: boolean, + responseCode?: number, +) { if (responseCode !== undefined) { res.status(responseCode); } @@ -83,7 +90,6 @@ export function sendSuccessResponse(res: Response, data: any, raw?: boolean, res } } - export function sendErrorResponse(res: Response, error: ResponseError) { let httpStatusCode = 500; if (error.httpStatusCode) { @@ -122,7 +128,6 @@ export function sendErrorResponse(res: Response, error: ResponseError) { res.status(httpStatusCode).json(response); } - /** * A helper function which does not just allow to return Promises it also makes sure that * all the responses have the same format @@ -133,8 +138,7 @@ export function sendErrorResponse(res: Response, error: ResponseError) { * @returns */ -export function send(processFunction: (req: Request, res: Response) => Promise) { // tslint:disable-line:no-any - +export function send(processFunction: (req: Request, res: Response) => Promise) { return async (req: Request, res: Response) => { try { const data = await processFunction(req, res); @@ -148,7 +152,6 @@ export function send(processFunction: (req: Request, res: Response) => Promise Promise { - const enableMetrics = config.get('endpoints.metrics.enable') as boolean; let register: Registry; - if (enableMetrics === true) { + if (enableMetrics) { const prefix = config.get('endpoints.metrics.prefix') as string; register = new promClient.Registry(); register.setDefaultLabels({ prefix }); @@ -238,119 +272,161 @@ class App { this.versions = await GenericHelpers.getVersions(); this.frontendSettings.versionCli = this.versions.cli; - this.frontendSettings.instanceId = await generateInstanceId() as string; + this.frontendSettings.instanceId = (await generateInstanceId()) as string; await this.externalHooks.run('frontend.settings', [this.frontendSettings]); const excludeEndpoints = config.get('security.excludeEndpoints') as string; - const ignoredEndpoints = ['healthz', 'metrics', this.endpointWebhook, this.endpointWebhookTest, this.endpointPresetCredentials]; + const ignoredEndpoints = [ + 'healthz', + 'metrics', + this.endpointWebhook, + this.endpointWebhookTest, + this.endpointPresetCredentials, + ]; + // eslint-disable-next-line prefer-spread ignoredEndpoints.push.apply(ignoredEndpoints, excludeEndpoints.split(':')); + // eslint-disable-next-line no-useless-escape const authIgnoreRegex = new RegExp(`^\/(${_(ignoredEndpoints).compact().join('|')})\/?.*$`); // Check for basic auth credentials if activated const basicAuthActive = config.get('security.basicAuth.active') as boolean; - if (basicAuthActive === true) { - const basicAuthUser = await GenericHelpers.getConfigValue('security.basicAuth.user') as string; + if (basicAuthActive) { + const basicAuthUser = (await GenericHelpers.getConfigValue( + 'security.basicAuth.user', + )) as string; if (basicAuthUser === '') { throw new Error('Basic auth is activated but no user got defined. Please set one!'); } - const basicAuthPassword = await GenericHelpers.getConfigValue('security.basicAuth.password') as string; + const basicAuthPassword = (await GenericHelpers.getConfigValue( + 'security.basicAuth.password', + )) as string; if (basicAuthPassword === '') { throw new Error('Basic auth is activated but no password got defined. Please set one!'); } - const basicAuthHashEnabled = await GenericHelpers.getConfigValue('security.basicAuth.hash') as boolean; + 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(); - } - const realm = 'n8n - Editor UI'; - const basicAuthData = basicAuth(req); + this.app.use( + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + if (authIgnoreRegex.exec(req.url)) { + return next(); + } + const realm = 'n8n - Editor UI'; + const basicAuthData = basicAuth(req); - if (basicAuthData === undefined) { - // Authorization data is missing - return ResponseHelper.basicAuthAuthorizationError(res, realm, 'Authorization is required!'); - } + if (basicAuthData === undefined) { + // Authorization data is missing + return ResponseHelper.basicAuthAuthorizationError( + res, + realm, + 'Authorization is required!', + ); + } - 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 (basicAuthData.name === basicAuthUser) { + if (basicAuthHashEnabled) { + 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) { + if (validPassword === basicAuthData.pass && validPassword !== null) { + // Provided hash is correct + return next(); + } + } else if (basicAuthData.pass === basicAuthPassword) { // Provided password is correct return next(); } } - } - // Provided authentication data is wrong - return ResponseHelper.basicAuthAuthorizationError(res, realm, 'Authorization data is wrong!'); - }); + // Provided authentication data is wrong + return ResponseHelper.basicAuthAuthorizationError( + res, + realm, + 'Authorization data is wrong!', + ); + }, + ); } // Check for and validate JWT if configured const jwtAuthActive = config.get('security.jwtAuth.active') as boolean; - if (jwtAuthActive === true) { - const jwtAuthHeader = await GenericHelpers.getConfigValue('security.jwtAuth.jwtHeader') as string; + if (jwtAuthActive) { + const jwtAuthHeader = (await GenericHelpers.getConfigValue( + 'security.jwtAuth.jwtHeader', + )) as string; if (jwtAuthHeader === '') { throw new Error('JWT auth is activated but no request header was defined. Please set one!'); } - const jwksUri = await GenericHelpers.getConfigValue('security.jwtAuth.jwksUri') as string; + const jwksUri = (await GenericHelpers.getConfigValue('security.jwtAuth.jwksUri')) as string; if (jwksUri === '') { throw new Error('JWT auth is activated but no JWK Set URI was defined. Please set one!'); } - const jwtHeaderValuePrefix = await GenericHelpers.getConfigValue('security.jwtAuth.jwtHeaderValuePrefix') as string; - const jwtIssuer = await GenericHelpers.getConfigValue('security.jwtAuth.jwtIssuer') as string; - const jwtNamespace = await GenericHelpers.getConfigValue('security.jwtAuth.jwtNamespace') as string; - const jwtAllowedTenantKey = await GenericHelpers.getConfigValue('security.jwtAuth.jwtAllowedTenantKey') as string; - const jwtAllowedTenant = await GenericHelpers.getConfigValue('security.jwtAuth.jwtAllowedTenant') as string; + const jwtHeaderValuePrefix = (await GenericHelpers.getConfigValue( + 'security.jwtAuth.jwtHeaderValuePrefix', + )) as string; + const jwtIssuer = (await GenericHelpers.getConfigValue( + 'security.jwtAuth.jwtIssuer', + )) as string; + const jwtNamespace = (await GenericHelpers.getConfigValue( + 'security.jwtAuth.jwtNamespace', + )) as string; + const jwtAllowedTenantKey = (await GenericHelpers.getConfigValue( + 'security.jwtAuth.jwtAllowedTenantKey', + )) as string; + const jwtAllowedTenant = (await GenericHelpers.getConfigValue( + 'security.jwtAuth.jwtAllowedTenant', + )) as string; + // eslint-disable-next-line no-inner-declarations function isTenantAllowed(decodedToken: object): boolean { - if (jwtNamespace === '' || jwtAllowedTenantKey === '' || jwtAllowedTenant === '') return true; - else { - for (const [k, v] of Object.entries(decodedToken)) { - if (k === jwtNamespace) { - for (const [kn, kv] of Object.entries(v)) { - if (kn === jwtAllowedTenantKey && kv === jwtAllowedTenant) { - return true; - } + if (jwtNamespace === '' || jwtAllowedTenantKey === '' || jwtAllowedTenant === '') + return true; + + for (const [k, v] of Object.entries(decodedToken)) { + if (k === jwtNamespace) { + for (const [kn, kv] of Object.entries(v)) { + if (kn === jwtAllowedTenantKey && kv === jwtAllowedTenant) { + return true; } } } } + return false; } + // eslint-disable-next-line consistent-return this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { - if (req.url.match(authIgnoreRegex)) { + if (authIgnoreRegex.exec(req.url)) { return next(); } let token = req.header(jwtAuthHeader) as string; if (token === undefined || token === '') { - return ResponseHelper.jwtAuthAuthorizationError(res, "Missing token"); + return ResponseHelper.jwtAuthAuthorizationError(res, 'Missing token'); } if (jwtHeaderValuePrefix !== '' && token.startsWith(jwtHeaderValuePrefix)) { - token = token.replace(jwtHeaderValuePrefix + ' ', '').trimLeft(); + token = token.replace(`${jwtHeaderValuePrefix} `, '').trimLeft(); } const jwkClient = jwks({ cache: true, jwksUri }); - function getKey(header: any, callback: Function) { // tslint:disable-line:no-any - jwkClient.getSigningKey(header.kid, (err: Error, key: any) => { // tslint:disable-line:no-any + // eslint-disable-next-line @typescript-eslint/ban-types + function getKey(header: any, callback: Function) { + jwkClient.getSigningKey(header.kid, (err: Error, key: any) => { + // eslint-disable-next-line @typescript-eslint/no-throw-literal if (err) throw ResponseHelper.jwtAuthAuthorizationError(res, err.message); const signingKey = key.publicKey || key.rsaPublicKey; @@ -365,7 +441,8 @@ class App { jwt.verify(token, getKey, jwtVerifyOptions, (err: jwt.VerifyErrors, decoded: object) => { if (err) ResponseHelper.jwtAuthAuthorizationError(res, 'Invalid token'); - else if (!isTenantAllowed(decoded)) ResponseHelper.jwtAuthAuthorizationError(res, 'Tenant not allowed'); + else if (!isTenantAllowed(decoded)) + ResponseHelper.jwtAuthAuthorizationError(res, 'Tenant not allowed'); else next(); }); }); @@ -398,62 +475,81 @@ class App { }); // Support application/json type post data - this.app.use(bodyParser.json({ - limit: this.payloadSizeMax + 'mb', verify: (req, res, buf) => { - // @ts-ignore - req.rawBody = buf; - }, - })); + this.app.use( + bodyParser.json({ + limit: `${this.payloadSizeMax}mb`, + verify: (req, res, buf) => { + // @ts-ignore + req.rawBody = buf; + }, + }), + ); // Support application/xml type post data - // @ts-ignore - this.app.use(bodyParser.xml({ - limit: this.payloadSizeMax + 'mb', xmlParseOptions: { - 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( + // @ts-ignore + bodyParser.xml({ + limit: `${this.payloadSizeMax}mb`, + xmlParseOptions: { + 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: this.payloadSizeMax + 'mb', verify: (req, res, buf) => { - // @ts-ignore - req.rawBody = buf; - }, - })); + this.app.use( + bodyParser.text({ + limit: `${this.payloadSizeMax}mb`, + verify: (req, res, buf) => { + // @ts-ignore + req.rawBody = buf; + }, + }), + ); // Make sure that Vue history mode works properly - this.app.use(history({ - rewrites: [ - { - from: new RegExp(`^\/(${this.restEndpoint}|healthz|metrics|css|js|${this.endpointWebhook}|${this.endpointWebhookTest})\/?.*$`), - to: (context) => { - return context.parsedUrl!.pathname!.toString(); + this.app.use( + history({ + rewrites: [ + { + from: new RegExp( + // eslint-disable-next-line no-useless-escape + `^\/(${this.restEndpoint}|healthz|metrics|css|js|${this.endpointWebhook}|${this.endpointWebhookTest})\/?.*$`, + ), + to: (context) => { + return context.parsedUrl.pathname!.toString(); + }, }, + ], + }), + ); + + // support application/x-www-form-urlencoded post data + this.app.use( + bodyParser.urlencoded({ + extended: false, + verify: (req, res, buf) => { + // @ts-ignore + req.rawBody = buf; }, - ], - })); + }), + ); - //support application/x-www-form-urlencoded post data - this.app.use(bodyParser.urlencoded({ - extended: false, - verify: (req, res, buf) => { - // @ts-ignore - req.rawBody = buf; - }, - })); - - if (process.env['NODE_ENV'] !== 'production') { + if (process.env.NODE_ENV !== 'production') { this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { // Allow access also from frontend when developing res.header('Access-Control-Allow-Origin', 'http://localhost:8080'); res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); - res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, sessionid'); + res.header( + 'Access-Control-Allow-Headers', + 'Origin, X-Requested-With, Content-Type, Accept, sessionid', + ); next(); }); } - + // eslint-disable-next-line consistent-return this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { if (Db.collections.Workflow === null) { const error = new ResponseHelper.ResponseError('Database is not ready!', undefined, 503); @@ -463,20 +559,16 @@ class App { next(); }); - - // ---------------------------------------- // Healthcheck // ---------------------------------------- - // Does very basic health check this.app.get('/healthz', async (req: express.Request, res: express.Response) => { - const connection = getConnectionManager().get(); try { - if (connection.isConnected === false) { + if (!connection.isConnected) { // Connection is not active throw new Error('No active database connection!'); } @@ -499,7 +591,7 @@ class App { // ---------------------------------------- // Metrics // ---------------------------------------- - if (enableMetrics === true) { + if (enableMetrics) { this.app.get('/metrics', async (req: express.Request, res: express.Response) => { const response = await register.metrics(); res.setHeader('Content-Type', register.contentType); @@ -511,1502 +603,1881 @@ class App { // Workflow // ---------------------------------------- - // Creates a new workflow - this.app.post(`/${this.restEndpoint}/workflows`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - delete req.body.id; // ignore if sent by mistake - const incomingData = req.body; + this.app.post( + `/${this.restEndpoint}/workflows`, + ResponseHelper.send( + async (req: express.Request, res: express.Response): Promise => { + delete req.body.id; // ignore if sent by mistake + const incomingData = req.body; - const newWorkflow = new WorkflowEntity(); + const newWorkflow = new WorkflowEntity(); - Object.assign(newWorkflow, incomingData); - newWorkflow.name = incomingData.name.trim(); + Object.assign(newWorkflow, incomingData); + newWorkflow.name = incomingData.name.trim(); - const incomingTagOrder = incomingData.tags.slice(); + const incomingTagOrder = incomingData.tags.slice(); - if (incomingData.tags.length) { - newWorkflow.tags = await Db.collections.Tag!.findByIds(incomingData.tags, { select: ['id', 'name'] }); - } + if (incomingData.tags.length) { + newWorkflow.tags = await Db.collections.Tag!.findByIds(incomingData.tags, { + select: ['id', 'name'], + }); + } - await this.externalHooks.run('workflow.create', [newWorkflow]); + await this.externalHooks.run('workflow.create', [newWorkflow]); - await WorkflowHelpers.validateWorkflow(newWorkflow); - const savedWorkflow = await Db.collections.Workflow!.save(newWorkflow).catch(WorkflowHelpers.throwDuplicateEntryError) as WorkflowEntity; - savedWorkflow.tags = TagHelpers.sortByRequestOrder(savedWorkflow.tags, incomingTagOrder); - - // @ts-ignore - savedWorkflow.id = savedWorkflow.id.toString(); - return savedWorkflow; - })); + await WorkflowHelpers.validateWorkflow(newWorkflow); + const savedWorkflow = (await Db.collections + .Workflow!.save(newWorkflow) + .catch(WorkflowHelpers.throwDuplicateEntryError)) as WorkflowEntity; + savedWorkflow.tags = TagHelpers.sortByRequestOrder(savedWorkflow.tags, incomingTagOrder); + // @ts-ignore + savedWorkflow.id = savedWorkflow.id.toString(); + return savedWorkflow; + }, + ), + ); // Reads and returns workflow data from an URL - this.app.get(`/${this.restEndpoint}/workflows/from-url`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - if (req.query.url === undefined) { - throw new ResponseHelper.ResponseError(`The parameter "url" is missing!`, undefined, 400); - } - if (!(req.query.url as string).match(/^http[s]?:\/\/.*\.json$/i)) { - throw new ResponseHelper.ResponseError(`The parameter "url" is not valid! It does not seem to be a URL pointing to a n8n workflow JSON file.`, undefined, 400); - } - const data = await requestPromise.get(req.query.url as string); + this.app.get( + `/${this.restEndpoint}/workflows/from-url`, + ResponseHelper.send( + async (req: express.Request, res: express.Response): Promise => { + if (req.query.url === undefined) { + throw new ResponseHelper.ResponseError( + `The parameter "url" is missing!`, + undefined, + 400, + ); + } + if (!/^http[s]?:\/\/.*\.json$/i.exec(req.query.url as string)) { + throw new ResponseHelper.ResponseError( + `The parameter "url" is not valid! It does not seem to be a URL pointing to a n8n workflow JSON file.`, + undefined, + 400, + ); + } + const data = await requestPromise.get(req.query.url as string); - let workflowData: IWorkflowResponse | undefined; - try { - workflowData = JSON.parse(data); - } catch (error) { - throw new ResponseHelper.ResponseError(`The URL does not point to valid JSON file!`, undefined, 400); - } + let workflowData: IWorkflowResponse | undefined; + try { + workflowData = JSON.parse(data); + } catch (error) { + throw new ResponseHelper.ResponseError( + `The URL does not point to valid JSON file!`, + undefined, + 400, + ); + } - // Do a very basic check if it is really a n8n-workflow-json - if (workflowData === undefined || workflowData.nodes === undefined || !Array.isArray(workflowData.nodes) || - workflowData.connections === undefined || typeof workflowData.connections !== 'object' || - Array.isArray(workflowData.connections)) { - throw new ResponseHelper.ResponseError(`The data in the file does not seem to be a n8n workflow JSON file!`, undefined, 400); - } - - return workflowData; - })); + // Do a very basic check if it is really a n8n-workflow-json + if ( + workflowData === undefined || + workflowData.nodes === undefined || + !Array.isArray(workflowData.nodes) || + workflowData.connections === undefined || + typeof workflowData.connections !== 'object' || + Array.isArray(workflowData.connections) + ) { + throw new ResponseHelper.ResponseError( + `The data in the file does not seem to be a n8n workflow JSON file!`, + undefined, + 400, + ); + } + return workflowData; + }, + ), + ); // Returns workflows - this.app.get(`/${this.restEndpoint}/workflows`, ResponseHelper.send(async (req: express.Request, res: express.Response) => { - const findQuery: FindManyOptions = { - select: ['id', 'name', 'active', 'createdAt', 'updatedAt'], - relations: ['tags'], - }; + this.app.get( + `/${this.restEndpoint}/workflows`, + ResponseHelper.send(async (req: express.Request, res: express.Response) => { + const findQuery: FindManyOptions = { + select: ['id', 'name', 'active', 'createdAt', 'updatedAt'], + relations: ['tags'], + }; - if (req.query.filter) { - findQuery.where = JSON.parse(req.query.filter as string); - } - - const workflows = await Db.collections.Workflow!.find(findQuery); - - workflows.forEach(workflow => { - // @ts-ignore - workflow.id = workflow.id.toString(); - // @ts-ignore - workflow.tags = workflow.tags.map(({ id, name }) => ({ id: id.toString(), name })); - }); - return workflows; - })); - - - this.app.get(`/${this.restEndpoint}/workflows/new`, ResponseHelper.send(async (req: WorkflowNameRequest, res: express.Response): Promise<{ name: string }> => { - const nameToReturn = req.query.name && req.query.name !== '' - ? req.query.name - : this.defaultWorkflowName; - - const workflows = await Db.collections.Workflow!.find({ - select: ['name'], - where: { name: Like(`${nameToReturn}%`) }, - }); - - // name is unique - if (workflows.length === 0) { - return { name: nameToReturn }; - } - - const maxSuffix = workflows.reduce((acc: number, { name }) => { - const parts = name.split(`${nameToReturn} `); - - if (parts.length > 2) return acc; - - const suffix = Number(parts[1]); - - if (!isNaN(suffix) && Math.ceil(suffix) > acc) { - acc = Math.ceil(suffix); + if (req.query.filter) { + findQuery.where = JSON.parse(req.query.filter as string); } - return acc; - }, 0); + const workflows = await Db.collections.Workflow!.find(findQuery); - // name is duplicate but no numeric suffixes exist yet - if (maxSuffix === 0) { - return { name: `${nameToReturn} 2` }; - } + workflows.forEach((workflow) => { + // @ts-ignore + workflow.id = workflow.id.toString(); + // @ts-ignore + workflow.tags = workflow.tags.map(({ id, name }) => ({ id: id.toString(), name })); + }); + return workflows; + }), + ); - return { name: `${nameToReturn} ${maxSuffix + 1}` }; - })); + this.app.get( + `/${this.restEndpoint}/workflows/new`, + ResponseHelper.send( + async (req: WorkflowNameRequest, res: express.Response): Promise<{ name: string }> => { + const nameToReturn = + req.query.name && req.query.name !== '' ? req.query.name : this.defaultWorkflowName; + + const workflows = await Db.collections.Workflow!.find({ + select: ['name'], + where: { name: Like(`${nameToReturn}%`) }, + }); + + // name is unique + if (workflows.length === 0) { + return { name: nameToReturn }; + } + + const maxSuffix = workflows.reduce((acc: number, { name }) => { + const parts = name.split(`${nameToReturn} `); + + if (parts.length > 2) return acc; + + const suffix = Number(parts[1]); + + // eslint-disable-next-line no-restricted-globals + if (!isNaN(suffix) && Math.ceil(suffix) > acc) { + acc = Math.ceil(suffix); + } + + return acc; + }, 0); + + // name is duplicate but no numeric suffixes exist yet + if (maxSuffix === 0) { + return { name: `${nameToReturn} 2` }; + } + + return { name: `${nameToReturn} ${maxSuffix + 1}` }; + }, + ), + ); // Returns a specific workflow - this.app.get(`/${this.restEndpoint}/workflows/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - const workflow = await Db.collections.Workflow!.findOne(req.params.id, { relations: ['tags'] }); + this.app.get( + `/${this.restEndpoint}/workflows/:id`, + ResponseHelper.send( + async ( + req: express.Request, + res: express.Response, + ): Promise => { + const workflow = await Db.collections.Workflow!.findOne(req.params.id, { + relations: ['tags'], + }); - if (workflow === undefined) { - return undefined; - } - - // @ts-ignore - workflow.id = workflow.id.toString(); - // @ts-ignore - workflow.tags.forEach(tag => tag.id = tag.id.toString()); - return workflow; - })); + if (workflow === undefined) { + return undefined; + } + // @ts-ignore + workflow.id = workflow.id.toString(); + // @ts-ignore + workflow.tags.forEach((tag) => (tag.id = tag.id.toString())); + return workflow; + }, + ), + ); // Updates an existing workflow - this.app.patch(`/${this.restEndpoint}/workflows/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - const { tags, ...updateData } = req.body; + this.app.patch( + `/${this.restEndpoint}/workflows/:id`, + ResponseHelper.send( + async (req: express.Request, res: express.Response): Promise => { + const { tags, ...updateData } = req.body; - const id = req.params.id; - updateData.id = id; + const { id } = req.params; + updateData.id = id; - await this.externalHooks.run('workflow.update', [updateData]); + await this.externalHooks.run('workflow.update', [updateData]); - const isActive = await this.activeWorkflowRunner.isActive(id); + const isActive = await this.activeWorkflowRunner.isActive(id); - if (isActive) { - // When workflow gets saved always remove it as the triggers could have been - // changed and so the changes would not take effect - await this.activeWorkflowRunner.remove(id); - } + if (isActive) { + // When workflow gets saved always remove it as the triggers could have been + // changed and so the changes would not take effect + await this.activeWorkflowRunner.remove(id); + } - if (updateData.settings) { - if (updateData.settings.timezone === 'DEFAULT') { - // Do not save the default timezone - delete updateData.settings.timezone; - } - if (updateData.settings.saveDataErrorExecution === 'DEFAULT') { - // Do not save when default got set - delete updateData.settings.saveDataErrorExecution; - } - if (updateData.settings.saveDataSuccessExecution === 'DEFAULT') { - // Do not save when default got set - delete updateData.settings.saveDataSuccessExecution; - } - if (updateData.settings.saveManualExecutions === 'DEFAULT') { - // Do not save when default got set - delete updateData.settings.saveManualExecutions; - } - if (parseInt(updateData.settings.executionTimeout as string, 10) === this.executionTimeout) { - // Do not save when default got set - delete updateData.settings.executionTimeout; - } - } + if (updateData.settings) { + if (updateData.settings.timezone === 'DEFAULT') { + // Do not save the default timezone + delete updateData.settings.timezone; + } + if (updateData.settings.saveDataErrorExecution === 'DEFAULT') { + // Do not save when default got set + delete updateData.settings.saveDataErrorExecution; + } + if (updateData.settings.saveDataSuccessExecution === 'DEFAULT') { + // Do not save when default got set + delete updateData.settings.saveDataSuccessExecution; + } + if (updateData.settings.saveManualExecutions === 'DEFAULT') { + // Do not save when default got set + delete updateData.settings.saveManualExecutions; + } + if ( + parseInt(updateData.settings.executionTimeout as string, 10) === this.executionTimeout + ) { + // Do not save when default got set + delete updateData.settings.executionTimeout; + } + } - // required due to atomic update - updateData.updatedAt = this.getCurrentDate(); + // required due to atomic update + updateData.updatedAt = this.getCurrentDate(); - await WorkflowHelpers.validateWorkflow(updateData); - await Db.collections.Workflow!.update(id, updateData).catch(WorkflowHelpers.throwDuplicateEntryError); + await WorkflowHelpers.validateWorkflow(updateData); + await Db.collections + .Workflow!.update(id, updateData) + .catch(WorkflowHelpers.throwDuplicateEntryError); - if (tags) { - const tablePrefix = config.get('database.tablePrefix'); - await TagHelpers.removeRelations(req.params.id, tablePrefix); + if (tags) { + const tablePrefix = config.get('database.tablePrefix'); + await TagHelpers.removeRelations(req.params.id, tablePrefix); - if (tags.length) { - await TagHelpers.createRelations(req.params.id, tags, tablePrefix); - } - } + if (tags.length) { + await TagHelpers.createRelations(req.params.id, tags, tablePrefix); + } + } - // We sadly get nothing back from "update". Neither if it updated a record - // nor the new value. So query now the hopefully updated entry. - const workflow = await Db.collections.Workflow!.findOne(id, { relations: ['tags'] }); + // We sadly get nothing back from "update". Neither if it updated a record + // nor the new value. So query now the hopefully updated entry. + const workflow = await Db.collections.Workflow!.findOne(id, { relations: ['tags'] }); - if (workflow === undefined) { - throw new ResponseHelper.ResponseError(`Workflow with id "${id}" could not be found to be updated.`, undefined, 400); - } + if (workflow === undefined) { + throw new ResponseHelper.ResponseError( + `Workflow with id "${id}" could not be found to be updated.`, + undefined, + 400, + ); + } - if (tags?.length) { - workflow.tags = TagHelpers.sortByRequestOrder(workflow.tags, tags); - } + if (tags?.length) { + workflow.tags = TagHelpers.sortByRequestOrder(workflow.tags, tags); + } - await this.externalHooks.run('workflow.afterUpdate', [workflow]); + await this.externalHooks.run('workflow.afterUpdate', [workflow]); - if (workflow.active === true) { - // When the workflow is supposed to be active add it again - try { - await this.externalHooks.run('workflow.activate', [workflow]); + if (workflow.active) { + // When the workflow is supposed to be active add it again + try { + await this.externalHooks.run('workflow.activate', [workflow]); + + await this.activeWorkflowRunner.add(id, isActive ? 'update' : 'activate'); + } catch (error) { + // If workflow could not be activated set it again to inactive + updateData.active = false; + // @ts-ignore + await Db.collections.Workflow!.update(id, updateData); + + // Also set it in the returned data + workflow.active = false; + + // Now return the original error for UI to display + throw error; + } + } - await this.activeWorkflowRunner.add(id, isActive ? 'update' : 'activate'); - } catch (error) { - // If workflow could not be activated set it again to inactive - updateData.active = false; // @ts-ignore - await Db.collections.Workflow!.update(id, updateData); - - // Also set it in the returned data - workflow.active = false; - - // Now return the original error for UI to display - throw error; - } - } - - // @ts-ignore - workflow.id = workflow.id.toString(); - return workflow; - })); - + workflow.id = workflow.id.toString(); + return workflow; + }, + ), + ); // Deletes a specific workflow - this.app.delete(`/${this.restEndpoint}/workflows/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - const id = req.params.id; + this.app.delete( + `/${this.restEndpoint}/workflows/:id`, + ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + const { id } = req.params; - await this.externalHooks.run('workflow.delete', [id]); + await this.externalHooks.run('workflow.delete', [id]); - const isActive = await this.activeWorkflowRunner.isActive(id); + const isActive = await this.activeWorkflowRunner.isActive(id); - if (isActive) { - // Before deleting a workflow deactivate it - await this.activeWorkflowRunner.remove(id); - } - - await Db.collections.Workflow!.delete(id); - await this.externalHooks.run('workflow.afterDelete', [id]); - - return true; - })); - - this.app.post(`/${this.restEndpoint}/workflows/run`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - const workflowData = req.body.workflowData; - const runData: IRunData | undefined = req.body.runData; - const startNodes: string[] | undefined = req.body.startNodes; - const destinationNode: string | undefined = req.body.destinationNode; - const executionMode = 'manual'; - const activationMode = 'manual'; - - const sessionId = GenericHelpers.getSessionId(req); - - // If webhooks nodes exist and are active we have to wait for till we receive a call - if (runData === undefined || startNodes === undefined || startNodes.length === 0 || destinationNode === undefined) { - const additionalData = await WorkflowExecuteAdditionalData.getBase(); - const nodeTypes = NodeTypes(); - const workflowInstance = new Workflow({ id: workflowData.id, name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: false, nodeTypes, staticData: undefined, settings: workflowData.settings }); - const needsWebhook = await this.testWebhooks.needsWebhookData(workflowData, workflowInstance, additionalData, executionMode, activationMode, sessionId, destinationNode); - if (needsWebhook === true) { - return { - waitingForWebhook: true, - }; + if (isActive) { + // Before deleting a workflow deactivate it + await this.activeWorkflowRunner.remove(id); } - } - // For manual testing always set to not active - workflowData.active = false; + await Db.collections.Workflow!.delete(id); + await this.externalHooks.run('workflow.afterDelete', [id]); - // Start the workflow - const data: IWorkflowExecutionDataProcess = { - destinationNode, - executionMode, - runData, - sessionId, - startNodes, - workflowData, - }; - const workflowRunner = new WorkflowRunner(); - const executionId = await workflowRunner.run(data); + return true; + }), + ); - return { - executionId, - }; - })); + this.app.post( + `/${this.restEndpoint}/workflows/run`, + ResponseHelper.send( + async (req: express.Request, res: express.Response): Promise => { + const { workflowData } = req.body; + const { runData } = req.body; + const { startNodes } = req.body; + const { destinationNode } = req.body; + const executionMode = 'manual'; + const activationMode = 'manual'; + + const sessionId = GenericHelpers.getSessionId(req); + + // If webhooks nodes exist and are active we have to wait for till we receive a call + if ( + runData === undefined || + startNodes === undefined || + startNodes.length === 0 || + destinationNode === undefined + ) { + const additionalData = await WorkflowExecuteAdditionalData.getBase(); + const nodeTypes = NodeTypes(); + const workflowInstance = new Workflow({ + id: workflowData.id, + name: workflowData.name, + nodes: workflowData.nodes, + connections: workflowData.connections, + active: false, + nodeTypes, + staticData: undefined, + settings: workflowData.settings, + }); + const needsWebhook = await this.testWebhooks.needsWebhookData( + workflowData, + workflowInstance, + additionalData, + executionMode, + activationMode, + sessionId, + destinationNode, + ); + if (needsWebhook) { + return { + waitingForWebhook: true, + }; + } + } + + // For manual testing always set to not active + workflowData.active = false; + + // Start the workflow + const data: IWorkflowExecutionDataProcess = { + destinationNode, + executionMode, + runData, + sessionId, + startNodes, + workflowData, + }; + const workflowRunner = new WorkflowRunner(); + const executionId = await workflowRunner.run(data); + + return { + executionId, + }; + }, + ), + ); // Retrieves all tags, with or without usage count - this.app.get(`/${this.restEndpoint}/tags`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - if (req.query.withUsageCount === 'true') { - const tablePrefix = config.get('database.tablePrefix'); - return TagHelpers.getTagsWithCountDb(tablePrefix); - } + this.app.get( + `/${this.restEndpoint}/tags`, + ResponseHelper.send( + async ( + req: express.Request, + res: express.Response, + ): Promise => { + if (req.query.withUsageCount === 'true') { + const tablePrefix = config.get('database.tablePrefix'); + return TagHelpers.getTagsWithCountDb(tablePrefix); + } - const tags = await Db.collections.Tag!.find({ select: ['id', 'name'] }); - // @ts-ignore - tags.forEach(tag => tag.id = tag.id.toString()); - return tags; - })); + const tags = await Db.collections.Tag!.find({ select: ['id', 'name'] }); + // @ts-ignore + tags.forEach((tag) => (tag.id = tag.id.toString())); + return tags; + }, + ), + ); // Creates a tag - this.app.post(`/${this.restEndpoint}/tags`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - const newTag = new TagEntity(); - newTag.name = req.body.name.trim(); + this.app.post( + `/${this.restEndpoint}/tags`, + ResponseHelper.send( + async (req: express.Request, res: express.Response): Promise => { + const newTag = new TagEntity(); + newTag.name = req.body.name.trim(); - await this.externalHooks.run('tag.beforeCreate', [newTag]); + await this.externalHooks.run('tag.beforeCreate', [newTag]); - await TagHelpers.validateTag(newTag); - const tag = await Db.collections.Tag!.save(newTag).catch(TagHelpers.throwDuplicateEntryError); + await TagHelpers.validateTag(newTag); + const tag = await Db.collections + .Tag!.save(newTag) + .catch(TagHelpers.throwDuplicateEntryError); - await this.externalHooks.run('tag.afterCreate', [tag]); + await this.externalHooks.run('tag.afterCreate', [tag]); - // @ts-ignore - tag.id = tag.id.toString(); - return tag; - })); + // @ts-ignore + tag.id = tag.id.toString(); + return tag; + }, + ), + ); // Updates a tag - this.app.patch(`/${this.restEndpoint}/tags/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - const { name } = req.body; - const { id } = req.params; + this.app.patch( + `/${this.restEndpoint}/tags/:id`, + ResponseHelper.send( + async (req: express.Request, res: express.Response): Promise => { + const { name } = req.body; + const { id } = req.params; - const newTag = new TagEntity(); - newTag.id = Number(id); - newTag.name = name.trim(); + const newTag = new TagEntity(); + newTag.id = Number(id); + newTag.name = name.trim(); - await this.externalHooks.run('tag.beforeUpdate', [newTag]); + await this.externalHooks.run('tag.beforeUpdate', [newTag]); - await TagHelpers.validateTag(newTag); - const tag = await Db.collections.Tag!.save(newTag).catch(TagHelpers.throwDuplicateEntryError); + await TagHelpers.validateTag(newTag); + const tag = await Db.collections + .Tag!.save(newTag) + .catch(TagHelpers.throwDuplicateEntryError); - await this.externalHooks.run('tag.afterUpdate', [tag]); + await this.externalHooks.run('tag.afterUpdate', [tag]); - // @ts-ignore - tag.id = tag.id.toString(); - return tag; - })); + // @ts-ignore + tag.id = tag.id.toString(); + return tag; + }, + ), + ); // Deletes a tag - this.app.delete(`/${this.restEndpoint}/tags/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - const id = Number(req.params.id); + this.app.delete( + `/${this.restEndpoint}/tags/:id`, + ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + const id = Number(req.params.id); - await this.externalHooks.run('tag.beforeDelete', [id]); + await this.externalHooks.run('tag.beforeDelete', [id]); - await Db.collections.Tag!.delete({ id }); + await Db.collections.Tag!.delete({ id }); - await this.externalHooks.run('tag.afterDelete', [id]); + await this.externalHooks.run('tag.afterDelete', [id]); - return true; - })); + return true; + }), + ); // Returns parameter values which normally get loaded from an external API or // get generated dynamically - this.app.get(`/${this.restEndpoint}/node-parameter-options`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - const nodeType = req.query.nodeType as string; - const path = req.query.path as string; - let credentials: INodeCredentials | undefined = undefined; - const currentNodeParameters = JSON.parse('' + req.query.currentNodeParameters) as INodeParameters; - if (req.query.credentials !== undefined) { - credentials = JSON.parse(req.query.credentials as string); - } - const methodName = req.query.methodName as string; + this.app.get( + `/${this.restEndpoint}/node-parameter-options`, + ResponseHelper.send( + async (req: express.Request, res: express.Response): Promise => { + const nodeType = req.query.nodeType as string; + const path = req.query.path as string; + let credentials: INodeCredentials | undefined; + const currentNodeParameters = JSON.parse( + `${req.query.currentNodeParameters}`, + ) as INodeParameters; + if (req.query.credentials !== undefined) { + credentials = JSON.parse(req.query.credentials as string); + } + const methodName = req.query.methodName as string; - const nodeTypes = NodeTypes(); + const nodeTypes = NodeTypes(); - // @ts-ignore - const loadDataInstance = new LoadNodeParameterOptions(nodeType, nodeTypes, path, JSON.parse('' + req.query.currentNodeParameters), credentials!); + // @ts-ignore + const loadDataInstance = new LoadNodeParameterOptions( + nodeType, + nodeTypes, + path, + JSON.parse(`${req.query.currentNodeParameters}`), + credentials, + ); - const additionalData = await WorkflowExecuteAdditionalData.getBase(currentNodeParameters); - - return loadDataInstance.getOptions(methodName, additionalData); - })); + const additionalData = await WorkflowExecuteAdditionalData.getBase(currentNodeParameters); + return loadDataInstance.getOptions(methodName, additionalData); + }, + ), + ); // Returns all the node-types - this.app.get(`/${this.restEndpoint}/node-types`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.get( + `/${this.restEndpoint}/node-types`, + ResponseHelper.send( + async (req: express.Request, res: express.Response): Promise => { + const returnData: INodeTypeDescription[] = []; - const returnData: INodeTypeDescription[] = []; + const nodeTypes = NodeTypes(); - const nodeTypes = NodeTypes(); + const allNodes = nodeTypes.getAll(); - const allNodes = nodeTypes.getAll(); - - allNodes.forEach((nodeData) => { - // 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; - })); + allNodes.forEach((nodeData) => { + // 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); - })); - + 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 // ---------------------------------------- - // Returns the node icon - this.app.get([`/${this.restEndpoint}/node-icon/:nodeType`, `/${this.restEndpoint}/node-icon/:scope/:nodeType`], async (req: express.Request, res: express.Response): Promise => { - try { - const nodeTypeName = `${req.params.scope ? `${req.params.scope}/` : ''}${req.params.nodeType}`; + this.app.get( + [ + `/${this.restEndpoint}/node-icon/:nodeType`, + `/${this.restEndpoint}/node-icon/:scope/:nodeType`, + ], + async (req: express.Request, res: express.Response): Promise => { + try { + const nodeTypeName = `${req.params.scope ? `${req.params.scope}/` : ''}${ + req.params.nodeType + }`; - const nodeTypes = NodeTypes(); - const nodeType = nodeTypes.getByName(nodeTypeName); + const nodeTypes = NodeTypes(); + const nodeType = nodeTypes.getByName(nodeTypeName); - if (nodeType === undefined) { - res.status(404).send('The nodeType is not known.'); - return; + if (nodeType === undefined) { + res.status(404).send('The nodeType is not known.'); + return; + } + + if (nodeType.description.icon === undefined) { + res.status(404).send('No icon found for node.'); + return; + } + + if (!nodeType.description.icon.startsWith('file:')) { + res.status(404).send('Node does not have a file icon.'); + return; + } + + const filepath = nodeType.description.icon.substr(5); + + const maxAge = 7 * 24 * 60 * 60 * 1000; // 7 days + res.setHeader('Cache-control', `private max-age=${maxAge}`); + + res.sendFile(filepath); + } catch (error) { + // Error response + return ResponseHelper.sendErrorResponse(res, error); } - - if (nodeType.description.icon === undefined) { - res.status(404).send('No icon found for node.'); - return; - } - - if (!nodeType.description.icon.startsWith('file:')) { - res.status(404).send('Node does not have a file icon.'); - return; - } - - const filepath = nodeType.description.icon.substr(5); - - const maxAge = 7 * 24 * 60 * 60 * 1000; // 7 days - res.setHeader('Cache-control', `private max-age=${maxAge}`); - - res.sendFile(filepath); - } catch (error) { - // Error response - return ResponseHelper.sendErrorResponse(res, error); - } - }); - - + }, + ); // ---------------------------------------- // Active Workflows // ---------------------------------------- - // Returns the active workflow ids - this.app.get(`/${this.restEndpoint}/active`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - const activeWorkflows = await this.activeWorkflowRunner.getActiveWorkflows(); - return activeWorkflows.map(workflow => workflow.id.toString()) as string[]; - })); - + this.app.get( + `/${this.restEndpoint}/active`, + ResponseHelper.send( + async (req: express.Request, res: express.Response): Promise => { + const activeWorkflows = await this.activeWorkflowRunner.getActiveWorkflows(); + return activeWorkflows.map((workflow) => workflow.id.toString()); + }, + ), + ); // Returns if the workflow with the given id had any activation errors - this.app.get(`/${this.restEndpoint}/active/error/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - const id = req.params.id; - return this.activeWorkflowRunner.getActivationError(id); - })); - - + this.app.get( + `/${this.restEndpoint}/active/error/:id`, + ResponseHelper.send( + async ( + req: express.Request, + res: express.Response, + ): Promise => { + const { id } = req.params; + return this.activeWorkflowRunner.getActivationError(id); + }, + ), + ); // ---------------------------------------- // Credentials // ---------------------------------------- - // Deletes a specific credential - this.app.delete(`/${this.restEndpoint}/credentials/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - const id = req.params.id; + this.app.delete( + `/${this.restEndpoint}/credentials/:id`, + ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + const { id } = req.params; - await this.externalHooks.run('credentials.delete', [id]); + await this.externalHooks.run('credentials.delete', [id]); - await Db.collections.Credentials!.delete({ id }); + await Db.collections.Credentials!.delete({ id }); - return true; - })); + return true; + }), + ); // Creates new credentials - this.app.post(`/${this.restEndpoint}/credentials`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - const incomingData = req.body; + this.app.post( + `/${this.restEndpoint}/credentials`, + ResponseHelper.send( + async (req: express.Request, res: express.Response): Promise => { + const incomingData = req.body; - if (!incomingData.name || incomingData.name.length < 3) { - throw new ResponseHelper.ResponseError(`Credentials name must be at least 3 characters long.`, undefined, 400); - } + if (!incomingData.name || incomingData.name.length < 3) { + throw new ResponseHelper.ResponseError( + `Credentials name must be at least 3 characters long.`, + undefined, + 400, + ); + } - // Add the added date for node access permissions - for (const nodeAccess of incomingData.nodesAccess) { - nodeAccess.date = this.getCurrentDate(); - } + // Add the added date for node access permissions + for (const nodeAccess of incomingData.nodesAccess) { + nodeAccess.date = this.getCurrentDate(); + } - const encryptionKey = await UserSettings.getEncryptionKey(); - if (encryptionKey === undefined) { - throw new Error('No encryption key got found to encrypt the credentials!'); - } + const encryptionKey = await UserSettings.getEncryptionKey(); + if (encryptionKey === undefined) { + throw new Error('No encryption key got found to encrypt the credentials!'); + } - if (incomingData.name === '') { - throw new Error('Credentials have to have a name set!'); - } + if (incomingData.name === '') { + throw new Error('Credentials have to have a name set!'); + } - // Check if credentials with the same name and type exist already - const findQuery = { - where: { - name: incomingData.name, - type: incomingData.type, + // Check if credentials with the same name and type exist already + const findQuery = { + where: { + name: incomingData.name, + type: incomingData.type, + }, + } as FindOneOptions; + + const checkResult = await Db.collections.Credentials!.findOne(findQuery); + if (checkResult !== undefined) { + throw new ResponseHelper.ResponseError( + `Credentials with the same type and name exist already.`, + undefined, + 400, + ); + } + + // Encrypt the data + const credentials = new Credentials( + incomingData.name, + incomingData.type, + incomingData.nodesAccess, + ); + credentials.setData(incomingData.data, encryptionKey); + const newCredentialsData = credentials.getDataToSave() as ICredentialsDb; + + await this.externalHooks.run('credentials.create', [newCredentialsData]); + + // Add special database related data + + // TODO: also add user automatically depending on who is logged in, if anybody is logged in + + // Save the credentials in DB + const result = await Db.collections.Credentials!.save(newCredentialsData); + result.data = incomingData.data; + + // Convert to response format in which the id is a string + (result as unknown as ICredentialsResponse).id = result.id.toString(); + return result as unknown as ICredentialsResponse; }, - } as FindOneOptions; - - const checkResult = await Db.collections.Credentials!.findOne(findQuery); - if (checkResult !== undefined) { - throw new ResponseHelper.ResponseError(`Credentials with the same type and name exist already.`, undefined, 400); - } - - // Encrypt the data - const credentials = new Credentials(incomingData.name, incomingData.type, incomingData.nodesAccess); - credentials.setData(incomingData.data, encryptionKey); - const newCredentialsData = credentials.getDataToSave() as ICredentialsDb; - - await this.externalHooks.run('credentials.create', [newCredentialsData]); - - // Add special database related data - - // TODO: also add user automatically depending on who is logged in, if anybody is logged in - - // Save the credentials in DB - const result = await Db.collections.Credentials!.save(newCredentialsData); - result.data = incomingData.data; - - // Convert to response format in which the id is a string - (result as unknown as ICredentialsResponse).id = result.id.toString(); - return result as unknown as ICredentialsResponse; - })); - + ), + ); // Updates existing credentials - this.app.patch(`/${this.restEndpoint}/credentials/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - const incomingData = req.body; + this.app.patch( + `/${this.restEndpoint}/credentials/:id`, + ResponseHelper.send( + async (req: express.Request, res: express.Response): Promise => { + const incomingData = req.body; - const id = req.params.id; + const { id } = req.params; - if (incomingData.name === '') { - throw new Error('Credentials have to have a name set!'); - } + if (incomingData.name === '') { + throw new Error('Credentials have to have a name set!'); + } - // Add the date for newly added node access permissions - for (const nodeAccess of incomingData.nodesAccess) { - if (!nodeAccess.date) { - nodeAccess.date = this.getCurrentDate(); - } - } + // Add the date for newly added node access permissions + for (const nodeAccess of incomingData.nodesAccess) { + if (!nodeAccess.date) { + nodeAccess.date = this.getCurrentDate(); + } + } - // Check if credentials with the same name and type exist already - const findQuery = { - where: { - id: Not(id), - name: incomingData.name, - type: incomingData.type, + // Check if credentials with the same name and type exist already + const findQuery = { + where: { + id: Not(id), + name: incomingData.name, + type: incomingData.type, + }, + } as FindOneOptions; + + const checkResult = await Db.collections.Credentials!.findOne(findQuery); + if (checkResult !== undefined) { + throw new ResponseHelper.ResponseError( + `Credentials with the same type and name exist already.`, + undefined, + 400, + ); + } + + const encryptionKey = await UserSettings.getEncryptionKey(); + if (encryptionKey === undefined) { + throw new Error('No encryption key got found to encrypt the credentials!'); + } + + // Load the currently saved credentials to be able to persist some of the data if + const result = await Db.collections.Credentials!.findOne(id); + if (result === undefined) { + throw new ResponseHelper.ResponseError( + `Credentials with the id "${id}" do not exist.`, + undefined, + 400, + ); + } + + const currentlySavedCredentials = new Credentials( + result.name, + result.type, + result.nodesAccess, + result.data, + ); + const decryptedData = currentlySavedCredentials.getData(encryptionKey); + + // Do not overwrite the oauth data else data like the access or refresh token would get lost + // everytime anybody changes anything on the credentials even if it is just the name. + if (decryptedData.oauthTokenData) { + incomingData.data.oauthTokenData = decryptedData.oauthTokenData; + } + + // Encrypt the data + const credentials = new Credentials( + incomingData.name, + incomingData.type, + incomingData.nodesAccess, + ); + credentials.setData(incomingData.data, encryptionKey); + const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb; + + // Add special database related data + newCredentialsData.updatedAt = this.getCurrentDate(); + + await this.externalHooks.run('credentials.update', [newCredentialsData]); + + // Update the credentials in DB + await Db.collections.Credentials!.update(id, newCredentialsData); + + // We sadly get nothing back from "update". Neither if it updated a record + // nor the new value. So query now the hopefully updated entry. + const responseData = await Db.collections.Credentials!.findOne(id); + + if (responseData === undefined) { + throw new ResponseHelper.ResponseError( + `Credentials with id "${id}" could not be found to be updated.`, + undefined, + 400, + ); + } + + // Remove the encrypted data as it is not needed in the frontend + responseData.data = ''; + + // Convert to response format in which the id is a string + (responseData as unknown as ICredentialsResponse).id = responseData.id.toString(); + return responseData as unknown as ICredentialsResponse; }, - } as FindOneOptions; - - const checkResult = await Db.collections.Credentials!.findOne(findQuery); - if (checkResult !== undefined) { - throw new ResponseHelper.ResponseError(`Credentials with the same type and name exist already.`, undefined, 400); - } - - const encryptionKey = await UserSettings.getEncryptionKey(); - if (encryptionKey === undefined) { - throw new Error('No encryption key got found to encrypt the credentials!'); - } - - // Load the currently saved credentials to be able to persist some of the data if - const result = await Db.collections.Credentials!.findOne(id); - if (result === undefined) { - throw new ResponseHelper.ResponseError(`Credentials with the id "${id}" do not exist.`, undefined, 400); - } - - const currentlySavedCredentials = new Credentials(result.name, result.type, result.nodesAccess, result.data); - const decryptedData = currentlySavedCredentials.getData(encryptionKey!); - - // Do not overwrite the oauth data else data like the access or refresh token would get lost - // everytime anybody changes anything on the credentials even if it is just the name. - if (decryptedData.oauthTokenData) { - incomingData.data.oauthTokenData = decryptedData.oauthTokenData; - } - - // Encrypt the data - const credentials = new Credentials(incomingData.name, incomingData.type, incomingData.nodesAccess); - credentials.setData(incomingData.data, encryptionKey); - const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb; - - // Add special database related data - newCredentialsData.updatedAt = this.getCurrentDate(); - - await this.externalHooks.run('credentials.update', [newCredentialsData]); - - // Update the credentials in DB - await Db.collections.Credentials!.update(id, newCredentialsData); - - // We sadly get nothing back from "update". Neither if it updated a record - // nor the new value. So query now the hopefully updated entry. - const responseData = await Db.collections.Credentials!.findOne(id); - - if (responseData === undefined) { - throw new ResponseHelper.ResponseError(`Credentials with id "${id}" could not be found to be updated.`, undefined, 400); - } - - // Remove the encrypted data as it is not needed in the frontend - responseData.data = ''; - - // Convert to response format in which the id is a string - (responseData as unknown as ICredentialsResponse).id = responseData.id.toString(); - return responseData as unknown as ICredentialsResponse; - })); - + ), + ); // Returns specific credentials - this.app.get(`/${this.restEndpoint}/credentials/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - const findQuery = {} as FindManyOptions; + this.app.get( + `/${this.restEndpoint}/credentials/:id`, + ResponseHelper.send( + async ( + req: express.Request, + res: express.Response, + ): Promise => { + const findQuery = {} as FindManyOptions; - // Make sure the variable has an expected value - const includeData = ['true', true].includes(req.query.includeData as string); + // Make sure the variable has an expected value + const includeData = ['true', true].includes(req.query.includeData as string); - if (includeData !== true) { - // Return only the fields we need - findQuery.select = ['id', 'name', 'type', 'nodesAccess', 'createdAt', 'updatedAt']; - } + if (!includeData) { + // Return only the fields we need + findQuery.select = ['id', 'name', 'type', 'nodesAccess', 'createdAt', 'updatedAt']; + } - const result = await Db.collections.Credentials!.findOne(req.params.id); + const result = await Db.collections.Credentials!.findOne(req.params.id); - if (result === undefined) { - return result; - } + if (result === undefined) { + return result; + } - let encryptionKey = undefined; - if (includeData === true) { - encryptionKey = await UserSettings.getEncryptionKey(); - if (encryptionKey === undefined) { - throw new Error('No encryption key got found to decrypt the credentials!'); - } + let encryptionKey; + if (includeData) { + encryptionKey = await UserSettings.getEncryptionKey(); + if (encryptionKey === undefined) { + throw new Error('No encryption key got found to decrypt the credentials!'); + } - const credentials = new Credentials(result.name, result.type, result.nodesAccess, result.data); - (result as ICredentialsDecryptedDb).data = credentials.getData(encryptionKey!); - } + const credentials = new Credentials( + result.name, + result.type, + result.nodesAccess, + result.data, + ); + (result as ICredentialsDecryptedDb).data = credentials.getData(encryptionKey); + } - (result as ICredentialsDecryptedResponse).id = result.id.toString(); - - return result as ICredentialsDecryptedResponse; - })); + (result as ICredentialsDecryptedResponse).id = result.id.toString(); + return result as ICredentialsDecryptedResponse; + }, + ), + ); // Returns all the saved credentials - this.app.get(`/${this.restEndpoint}/credentials`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - const findQuery = {} as FindManyOptions; - if (req.query.filter) { - findQuery.where = JSON.parse(req.query.filter as string); - if ((findQuery.where! as IDataObject).id !== undefined) { - // No idea if multiple where parameters make db search - // slower but to be sure that that is not the case we - // remove all unnecessary fields in case the id is defined. - findQuery.where = { id: (findQuery.where! as IDataObject).id }; - } - } + this.app.get( + `/${this.restEndpoint}/credentials`, + ResponseHelper.send( + async (req: express.Request, res: express.Response): Promise => { + const findQuery = {} as FindManyOptions; + if (req.query.filter) { + findQuery.where = JSON.parse(req.query.filter as string); + if ((findQuery.where! as IDataObject).id !== undefined) { + // No idea if multiple where parameters make db search + // slower but to be sure that that is not the case we + // remove all unnecessary fields in case the id is defined. + findQuery.where = { id: (findQuery.where! as IDataObject).id }; + } + } - findQuery.select = ['id', 'name', 'type', 'nodesAccess', 'createdAt', 'updatedAt']; + findQuery.select = ['id', 'name', 'type', 'nodesAccess', 'createdAt', 'updatedAt']; - const results = await Db.collections.Credentials!.find(findQuery) as unknown as ICredentialsResponse[]; + const results = (await Db.collections.Credentials!.find( + findQuery, + )) as unknown as ICredentialsResponse[]; - let encryptionKey = undefined; + let encryptionKey; - const includeData = ['true', true].includes(req.query.includeData as string); - if (includeData === true) { - encryptionKey = await UserSettings.getEncryptionKey(); - if (encryptionKey === undefined) { - throw new Error('No encryption key got found to decrypt the credentials!'); - } - } - - let result; - for (result of results) { - (result as ICredentialsDecryptedResponse).id = result.id.toString(); - } - - return results; - })); + const includeData = ['true', true].includes(req.query.includeData as string); + if (includeData) { + encryptionKey = await UserSettings.getEncryptionKey(); + if (encryptionKey === undefined) { + throw new Error('No encryption key got found to decrypt the credentials!'); + } + } + let result; + for (result of results) { + (result as ICredentialsDecryptedResponse).id = result.id.toString(); + } + return results; + }, + ), + ); // ---------------------------------------- // Credential-Types // ---------------------------------------- - // Returns all the credential types which are defined in the loaded n8n-modules - this.app.get(`/${this.restEndpoint}/credential-types`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.get( + `/${this.restEndpoint}/credential-types`, + ResponseHelper.send( + async (req: express.Request, res: express.Response): Promise => { + const returnData: ICredentialType[] = []; - const returnData: ICredentialType[] = []; + const credentialTypes = CredentialTypes(); - const credentialTypes = CredentialTypes(); + credentialTypes.getAll().forEach((credentialData) => { + returnData.push(credentialData); + }); - credentialTypes.getAll().forEach((credentialData) => { - returnData.push(credentialData); - }); - - return returnData; - })); + return returnData; + }, + ), + ); // ---------------------------------------- // OAuth1-Credential/Auth // ---------------------------------------- // Authorize OAuth Data - this.app.get(`/${this.restEndpoint}/oauth1-credential/auth`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - if (req.query.id === undefined) { - res.status(500).send('Required credential id is missing!'); - return ''; - } - - const result = await Db.collections.Credentials!.findOne(req.query.id as string); - if (result === undefined) { - res.status(404).send('The credential is not known.'); - return ''; - } - - let encryptionKey = undefined; - encryptionKey = await UserSettings.getEncryptionKey(); - if (encryptionKey === undefined) { - res.status(500).send('No encryption key got found to decrypt the credentials!'); - return ''; - } - - const mode: WorkflowExecuteMode = 'internal'; - const credentialsHelper = new CredentialsHelper(encryptionKey); - const decryptedDataOriginal = await credentialsHelper.getDecrypted(result.name, result.type, mode, true); - const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(decryptedDataOriginal, result.type, mode); - - const signatureMethod = _.get(oauthCredentials, 'signatureMethod') as string; - - const oAuthOptions: clientOAuth1.Options = { - consumer: { - key: _.get(oauthCredentials, 'consumerKey') as string, - secret: _.get(oauthCredentials, 'consumerSecret') as string, - }, - signature_method: signatureMethod, - hash_function(base, key) { - const algorithm = (signatureMethod === 'HMAC-SHA1') ? 'sha1' : 'sha256'; - return createHmac(algorithm, key) - .update(base) - .digest('base64'); - }, - }; - - 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: oauthRequestData, - }; - - const data = oauth.toHeader(oauth.authorize(options as RequestOptions)); - - //@ts-ignore - options.headers = data; - - const response = await requestPromise(options); - - // Response comes as x-www-form-urlencoded string so convert it to JSON - - const responseJson = querystring.parse(response); - - const returnUri = `${_.get(oauthCredentials, 'authUrl')}?oauth_token=${responseJson.oauth_token}`; - - // Encrypt the data - const credentials = new Credentials(result.name, result.type, result.nodesAccess); - - credentials.setData(decryptedDataOriginal, encryptionKey); - const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb; - - // Add special database related data - newCredentialsData.updatedAt = this.getCurrentDate(); - - // Update the credentials in DB - await Db.collections.Credentials!.update(req.query.id as string, newCredentialsData); - - return returnUri; - })); - - // Verify and store app code. Generate access tokens and store for respective credential. - this.app.get(`/${this.restEndpoint}/oauth1-credential/callback`, async (req: express.Request, res: express.Response) => { - try { - const { oauth_verifier, oauth_token, cid } = req.query; - - if (oauth_verifier === undefined || oauth_token === undefined) { - const errorResponse = new ResponseHelper.ResponseError('Insufficient parameters for OAuth1 callback. Received following query parameters: ' + JSON.stringify(req.query), undefined, 503); - return ResponseHelper.sendErrorResponse(res, errorResponse); + this.app.get( + `/${this.restEndpoint}/oauth1-credential/auth`, + ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + if (req.query.id === undefined) { + res.status(500).send('Required credential id is missing!'); + return ''; } - const result = await Db.collections.Credentials!.findOne(cid as any); // tslint:disable-line:no-any + const result = await Db.collections.Credentials!.findOne(req.query.id as string); if (result === undefined) { - const errorResponse = new ResponseHelper.ResponseError('The credential is not known.', undefined, 404); - return ResponseHelper.sendErrorResponse(res, errorResponse); + res.status(404).send('The credential is not known.'); + return ''; } - let encryptionKey = undefined; + let encryptionKey; encryptionKey = await UserSettings.getEncryptionKey(); if (encryptionKey === undefined) { - const errorResponse = new ResponseHelper.ResponseError('No encryption key got found to decrypt the credentials!', undefined, 503); - return ResponseHelper.sendErrorResponse(res, errorResponse); + res.status(500).send('No encryption key got found to decrypt the credentials!'); + return ''; } - - // Decrypt the currently saved credentials - const workflowCredentials: IWorkflowCredentials = { - [result.type as string]: { - [result.name as string]: result as ICredentialsEncrypted, - }, - }; const mode: WorkflowExecuteMode = 'internal'; const credentialsHelper = new CredentialsHelper(encryptionKey); - const decryptedDataOriginal = await credentialsHelper.getDecrypted(result.name, result.type, mode, true); - const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(decryptedDataOriginal, result.type, mode); - - const options: OptionsWithUrl = { - method: 'POST', - url: _.get(oauthCredentials, 'accessTokenUrl') as string, - qs: { - oauth_token, - oauth_verifier, + const decryptedDataOriginal = await credentialsHelper.getDecrypted( + result.name, + result.type, + mode, + true, + ); + const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites( + decryptedDataOriginal, + result.type, + mode, + ); + + const signatureMethod = _.get(oauthCredentials, 'signatureMethod') as string; + + const oAuthOptions: clientOAuth1.Options = { + consumer: { + key: _.get(oauthCredentials, 'consumerKey') as string, + secret: _.get(oauthCredentials, 'consumerSecret') as string, + }, + signature_method: signatureMethod, + // eslint-disable-next-line @typescript-eslint/naming-convention + hash_function(base, key) { + const algorithm = signatureMethod === 'HMAC-SHA1' ? 'sha1' : 'sha256'; + return createHmac(algorithm, key).update(base).digest('base64'); }, }; - let oauthToken; + const oauthRequestData = { + oauth_callback: `${WebhookHelpers.getWebhookBaseUrl()}${ + this.restEndpoint + }/oauth1-credential/callback?cid=${req.query.id}`, + }; - try { - oauthToken = await requestPromise(options); - } catch (error) { - const errorResponse = new ResponseHelper.ResponseError('Unable to get access tokens!', undefined, 404); - return ResponseHelper.sendErrorResponse(res, errorResponse); - } + await this.externalHooks.run('oauth1.authenticate', [oAuthOptions, oauthRequestData]); + + // eslint-disable-next-line new-cap + const oauth = new clientOAuth1(oAuthOptions); + + const options: RequestOptions = { + method: 'POST', + url: _.get(oauthCredentials, 'requestTokenUrl') as string, + data: oauthRequestData, + }; + + const data = oauth.toHeader(oauth.authorize(options)); + + // @ts-ignore + options.headers = data; + + const response = await requestPromise(options); // Response comes as x-www-form-urlencoded string so convert it to JSON - const oauthTokenJson = querystring.parse(oauthToken); + const responseJson = querystring.parse(response); - decryptedDataOriginal.oauthTokenData = oauthTokenJson; + const returnUri = `${_.get(oauthCredentials, 'authUrl')}?oauth_token=${ + responseJson.oauth_token + }`; + // Encrypt the data const credentials = new Credentials(result.name, result.type, result.nodesAccess); + credentials.setData(decryptedDataOriginal, encryptionKey); const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb; + // Add special database related data newCredentialsData.updatedAt = this.getCurrentDate(); - // Save the credentials in DB - await Db.collections.Credentials!.update(cid as any, newCredentialsData); // tslint:disable-line:no-any - res.sendFile(pathResolve(__dirname, '../../templates/oauth-callback.html')); - } catch (error) { - // Error response - return ResponseHelper.sendErrorResponse(res, error); - } - }); + // Update the credentials in DB + await Db.collections.Credentials!.update(req.query.id as string, newCredentialsData); + return returnUri; + }), + ); + + // Verify and store app code. Generate access tokens and store for respective credential. + this.app.get( + `/${this.restEndpoint}/oauth1-credential/callback`, + async (req: express.Request, res: express.Response) => { + try { + const { oauth_verifier, oauth_token, cid } = req.query; + + if (oauth_verifier === undefined || oauth_token === undefined) { + const errorResponse = new ResponseHelper.ResponseError( + `Insufficient parameters for OAuth1 callback. Received following query parameters: ${JSON.stringify( + req.query, + )}`, + undefined, + 503, + ); + return ResponseHelper.sendErrorResponse(res, errorResponse); + } + + const result = await Db.collections.Credentials!.findOne(cid as any); + if (result === undefined) { + const errorResponse = new ResponseHelper.ResponseError( + 'The credential is not known.', + undefined, + 404, + ); + return ResponseHelper.sendErrorResponse(res, errorResponse); + } + + let encryptionKey; + encryptionKey = await UserSettings.getEncryptionKey(); + if (encryptionKey === undefined) { + const errorResponse = new ResponseHelper.ResponseError( + 'No encryption key got found to decrypt the credentials!', + undefined, + 503, + ); + return ResponseHelper.sendErrorResponse(res, errorResponse); + } + + // Decrypt the currently saved credentials + const workflowCredentials: IWorkflowCredentials = { + [result.type]: { + [result.name]: result as ICredentialsEncrypted, + }, + }; + const mode: WorkflowExecuteMode = 'internal'; + const credentialsHelper = new CredentialsHelper(encryptionKey); + const decryptedDataOriginal = await credentialsHelper.getDecrypted( + result.name, + result.type, + mode, + true, + ); + const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites( + decryptedDataOriginal, + result.type, + mode, + ); + + const options: OptionsWithUrl = { + method: 'POST', + url: _.get(oauthCredentials, 'accessTokenUrl') as string, + qs: { + oauth_token, + oauth_verifier, + }, + }; + + let oauthToken; + + try { + oauthToken = await requestPromise(options); + } catch (error) { + const errorResponse = new ResponseHelper.ResponseError( + 'Unable to get access tokens!', + undefined, + 404, + ); + return ResponseHelper.sendErrorResponse(res, errorResponse); + } + + // Response comes as x-www-form-urlencoded string so convert it to JSON + + const oauthTokenJson = querystring.parse(oauthToken); + + decryptedDataOriginal.oauthTokenData = oauthTokenJson; + + const credentials = new Credentials(result.name, result.type, result.nodesAccess); + credentials.setData(decryptedDataOriginal, encryptionKey); + const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb; + // Add special database related data + newCredentialsData.updatedAt = this.getCurrentDate(); + // Save the credentials in DB + await Db.collections.Credentials!.update(cid as any, newCredentialsData); + + res.sendFile(pathResolve(__dirname, '../../templates/oauth-callback.html')); + } catch (error) { + // Error response + return ResponseHelper.sendErrorResponse(res, error); + } + }, + ); // ---------------------------------------- // OAuth2-Credential/Auth // ---------------------------------------- - // Authorize OAuth Data - this.app.get(`/${this.restEndpoint}/oauth2-credential/auth`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - if (req.query.id === undefined) { - res.status(500).send('Required credential id is missing.'); - return ''; - } + this.app.get( + `/${this.restEndpoint}/oauth2-credential/auth`, + ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + if (req.query.id === undefined) { + res.status(500).send('Required credential id is missing.'); + return ''; + } - const result = await Db.collections.Credentials!.findOne(req.query.id as string); - if (result === undefined) { - res.status(404).send('The credential is not known.'); - return ''; - } + const result = await Db.collections.Credentials!.findOne(req.query.id as string); + if (result === undefined) { + res.status(404).send('The credential is not known.'); + return ''; + } - let encryptionKey = undefined; - encryptionKey = await UserSettings.getEncryptionKey(); - if (encryptionKey === undefined) { - res.status(500).send('No encryption key got found to decrypt the credentials!'); - return ''; - } + let encryptionKey; + encryptionKey = await UserSettings.getEncryptionKey(); + if (encryptionKey === undefined) { + res.status(500).send('No encryption key got found to decrypt the credentials!'); + return ''; + } - const mode: WorkflowExecuteMode = 'internal'; - const credentialsHelper = new CredentialsHelper(encryptionKey); - const decryptedDataOriginal = await credentialsHelper.getDecrypted(result.name, result.type, mode, true); - const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(decryptedDataOriginal, result.type, mode); + const mode: WorkflowExecuteMode = 'internal'; + const credentialsHelper = new CredentialsHelper(encryptionKey); + const decryptedDataOriginal = await credentialsHelper.getDecrypted( + result.name, + result.type, + mode, + true, + ); + const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites( + decryptedDataOriginal, + result.type, + mode, + ); - const token = new csrf(); - // Generate a CSRF prevention token and send it as a OAuth2 state stringma/ERR - const csrfSecret = token.secretSync(); - const state = { - token: token.create(csrfSecret), - cid: req.query.id, - }; - const stateEncodedStr = Buffer.from(JSON.stringify(state)).toString('base64') as string; + const token = new csrf(); + // Generate a CSRF prevention token and send it as a OAuth2 state stringma/ERR + const csrfSecret = token.secretSync(); + const state = { + token: token.create(csrfSecret), + cid: req.query.id, + }; + const stateEncodedStr = Buffer.from(JSON.stringify(state)).toString('base64'); - const oAuthOptions: clientOAuth2.Options = { - clientId: _.get(oauthCredentials, 'clientId') as string, - clientSecret: _.get(oauthCredentials, 'clientSecret', '') as string, - 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, ','), - state: stateEncodedStr, - }; + const oAuthOptions: clientOAuth2.Options = { + clientId: _.get(oauthCredentials, 'clientId') as string, + clientSecret: _.get(oauthCredentials, 'clientSecret', '') as string, + 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, ','), + state: stateEncodedStr, + }; - await this.externalHooks.run('oauth2.authenticate', [oAuthOptions]); + await this.externalHooks.run('oauth2.authenticate', [oAuthOptions]); - const oAuthObj = new clientOAuth2(oAuthOptions); + const oAuthObj = new clientOAuth2(oAuthOptions); - // Encrypt the data - const credentials = new Credentials(result.name, result.type, result.nodesAccess); - decryptedDataOriginal.csrfSecret = csrfSecret; + // Encrypt the data + const credentials = new Credentials(result.name, result.type, result.nodesAccess); + decryptedDataOriginal.csrfSecret = csrfSecret; - credentials.setData(decryptedDataOriginal, encryptionKey); - const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb; + credentials.setData(decryptedDataOriginal, encryptionKey); + const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb; - // Add special database related data - newCredentialsData.updatedAt = this.getCurrentDate(); + // Add special database related data + newCredentialsData.updatedAt = this.getCurrentDate(); - // Update the credentials in DB - await Db.collections.Credentials!.update(req.query.id as string, newCredentialsData); + // Update the credentials in DB + await Db.collections.Credentials!.update(req.query.id as string, newCredentialsData); - const authQueryParameters = _.get(oauthCredentials, 'authQueryParameters', '') as string; - let returnUri = oAuthObj.code.getUri(); + const authQueryParameters = _.get(oauthCredentials, 'authQueryParameters', '') as string; + let returnUri = oAuthObj.code.getUri(); - // if scope uses comma, change it as the library always return then with spaces - if ((_.get(oauthCredentials, 'scope') as string).includes(',')) { - const data = querystring.parse(returnUri.split('?')[1] as string); - data.scope = _.get(oauthCredentials, 'scope') as string; - returnUri = `${_.get(oauthCredentials, 'authUrl', '')}?${querystring.stringify(data)}`; - } + // if scope uses comma, change it as the library always return then with spaces + if ((_.get(oauthCredentials, 'scope') as string).includes(',')) { + const data = querystring.parse(returnUri.split('?')[1]); + data.scope = _.get(oauthCredentials, 'scope') as string; + returnUri = `${_.get(oauthCredentials, 'authUrl', '')}?${querystring.stringify(data)}`; + } - if (authQueryParameters) { - returnUri += '&' + authQueryParameters; - } + if (authQueryParameters) { + returnUri += `&${authQueryParameters}`; + } - return returnUri; - })); + return returnUri; + }), + ); // ---------------------------------------- // OAuth2-Credential/Callback // ---------------------------------------- // Verify and store app code. Generate access tokens and store for respective credential. - this.app.get(`/${this.restEndpoint}/oauth2-credential/callback`, async (req: express.Request, res: express.Response) => { - try { - - // realmId it's currently just use for the quickbook OAuth2 flow - const { code, state: stateEncoded } = req.query; - - if (code === undefined || stateEncoded === undefined) { - const errorResponse = new ResponseHelper.ResponseError('Insufficient parameters for OAuth2 callback. Received following query parameters: ' + JSON.stringify(req.query), undefined, 503); - return ResponseHelper.sendErrorResponse(res, errorResponse); - } - - let state; + this.app.get( + `/${this.restEndpoint}/oauth2-credential/callback`, + async (req: express.Request, res: express.Response) => { try { - state = JSON.parse(Buffer.from(stateEncoded as string, 'base64').toString()); - } catch (error) { - const errorResponse = new ResponseHelper.ResponseError('Invalid state format returned', undefined, 503); - return ResponseHelper.sendErrorResponse(res, errorResponse); - } + // realmId it's currently just use for the quickbook OAuth2 flow + const { code, state: stateEncoded } = req.query; - const result = await Db.collections.Credentials!.findOne(state.cid); - if (result === undefined) { - const errorResponse = new ResponseHelper.ResponseError('The credential is not known.', undefined, 404); - return ResponseHelper.sendErrorResponse(res, errorResponse); - } + if (code === undefined || stateEncoded === undefined) { + const errorResponse = new ResponseHelper.ResponseError( + `Insufficient parameters for OAuth2 callback. Received following query parameters: ${JSON.stringify( + req.query, + )}`, + undefined, + 503, + ); + return ResponseHelper.sendErrorResponse(res, errorResponse); + } - let encryptionKey = undefined; - encryptionKey = await UserSettings.getEncryptionKey(); - if (encryptionKey === undefined) { - const errorResponse = new ResponseHelper.ResponseError('No encryption key got found to decrypt the credentials!', undefined, 503); - return ResponseHelper.sendErrorResponse(res, errorResponse); - } + let state; + try { + state = JSON.parse(Buffer.from(stateEncoded as string, 'base64').toString()); + } catch (error) { + const errorResponse = new ResponseHelper.ResponseError( + 'Invalid state format returned', + undefined, + 503, + ); + return ResponseHelper.sendErrorResponse(res, errorResponse); + } - // Decrypt the currently saved credentials - const workflowCredentials: IWorkflowCredentials = { - [result.type as string]: { - [result.name as string]: result as ICredentialsEncrypted, - }, - }; - - const mode: WorkflowExecuteMode = 'internal'; - const credentialsHelper = new CredentialsHelper(encryptionKey); - const decryptedDataOriginal = await credentialsHelper.getDecrypted(result.name, result.type, mode, true); - const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(decryptedDataOriginal, result.type, mode); - - const token = new csrf(); - if (decryptedDataOriginal.csrfSecret === undefined || !token.verify(decryptedDataOriginal.csrfSecret as string, state.token)) { - const errorResponse = new ResponseHelper.ResponseError('The OAuth2 callback state is invalid!', undefined, 404); - return ResponseHelper.sendErrorResponse(res, errorResponse); - } + const result = await Db.collections.Credentials!.findOne(state.cid); + if (result === undefined) { + const errorResponse = new ResponseHelper.ResponseError( + 'The credential is not known.', + undefined, + 404, + ); + return ResponseHelper.sendErrorResponse(res, errorResponse); + } - let options = {}; + let encryptionKey; + encryptionKey = await UserSettings.getEncryptionKey(); + if (encryptionKey === undefined) { + const errorResponse = new ResponseHelper.ResponseError( + 'No encryption key got found to decrypt the credentials!', + undefined, + 503, + ); + return ResponseHelper.sendErrorResponse(res, errorResponse); + } - const oAuth2Parameters = { - clientId: _.get(oauthCredentials, 'clientId') 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, ','), - }; - - if (_.get(oauthCredentials, 'authentication', 'header') as string === 'body') { - options = { - body: { - client_id: _.get(oauthCredentials, 'clientId') as string, - client_secret: _.get(oauthCredentials, 'clientSecret', '') as string, + // Decrypt the currently saved credentials + const workflowCredentials: IWorkflowCredentials = { + [result.type]: { + [result.name]: result as ICredentialsEncrypted, }, }; - delete oAuth2Parameters.clientSecret; + + const mode: WorkflowExecuteMode = 'internal'; + const credentialsHelper = new CredentialsHelper(encryptionKey); + const decryptedDataOriginal = await credentialsHelper.getDecrypted( + result.name, + result.type, + mode, + true, + ); + const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites( + decryptedDataOriginal, + result.type, + mode, + ); + + const token = new csrf(); + if ( + decryptedDataOriginal.csrfSecret === undefined || + !token.verify(decryptedDataOriginal.csrfSecret as string, state.token) + ) { + const errorResponse = new ResponseHelper.ResponseError( + 'The OAuth2 callback state is invalid!', + undefined, + 404, + ); + return ResponseHelper.sendErrorResponse(res, errorResponse); + } + + let options = {}; + + const oAuth2Parameters = { + clientId: _.get(oauthCredentials, 'clientId') 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, ','), + }; + + if ((_.get(oauthCredentials, 'authentication', 'header') as string) === 'body') { + options = { + body: { + client_id: _.get(oauthCredentials, 'clientId') as string, + client_secret: _.get(oauthCredentials, 'clientSecret', '') as string, + }, + }; + delete oAuth2Parameters.clientSecret; + } + + 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( + `${oAuth2Parameters.redirectUri}?${queryParameters}`, + options, + ); + + if (Object.keys(req.query).length > 2) { + _.set(oauthToken.data, 'callbackQueryString', _.omit(req.query, 'state', 'code')); + } + + if (oauthToken === undefined) { + const errorResponse = new ResponseHelper.ResponseError( + 'Unable to get access tokens!', + undefined, + 404, + ); + return ResponseHelper.sendErrorResponse(res, errorResponse); + } + + if (decryptedDataOriginal.oauthTokenData) { + // Only overwrite supplied data as some providers do for example just return the + // refresh_token on the very first request and not on subsequent ones. + Object.assign(decryptedDataOriginal.oauthTokenData, oauthToken.data); + } else { + // No data exists so simply set + decryptedDataOriginal.oauthTokenData = oauthToken.data; + } + + _.unset(decryptedDataOriginal, 'csrfSecret'); + + const credentials = new Credentials(result.name, result.type, result.nodesAccess); + credentials.setData(decryptedDataOriginal, encryptionKey); + const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb; + // Add special database related data + newCredentialsData.updatedAt = this.getCurrentDate(); + // Save the credentials in DB + await Db.collections.Credentials!.update(state.cid, newCredentialsData); + + res.sendFile(pathResolve(__dirname, '../../templates/oauth-callback.html')); + } catch (error) { + // Error response + return ResponseHelper.sendErrorResponse(res, error); } - - 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(`${oAuth2Parameters.redirectUri}?${queryParameters}`, options); - - if (Object.keys(req.query).length > 2) { - _.set(oauthToken.data, 'callbackQueryString', _.omit(req.query, 'state', 'code')); - } - - if (oauthToken === undefined) { - const errorResponse = new ResponseHelper.ResponseError('Unable to get access tokens!', undefined, 404); - return ResponseHelper.sendErrorResponse(res, errorResponse); - } - - if (decryptedDataOriginal.oauthTokenData) { - // Only overwrite supplied data as some providers do for example just return the - // refresh_token on the very first request and not on subsequent ones. - Object.assign(decryptedDataOriginal.oauthTokenData, oauthToken.data); - } else { - // No data exists so simply set - decryptedDataOriginal.oauthTokenData = oauthToken.data; - } - - _.unset(decryptedDataOriginal, 'csrfSecret'); - - const credentials = new Credentials(result.name, result.type, result.nodesAccess); - credentials.setData(decryptedDataOriginal, encryptionKey); - const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb; - // Add special database related data - newCredentialsData.updatedAt = this.getCurrentDate(); - // Save the credentials in DB - await Db.collections.Credentials!.update(state.cid, newCredentialsData); - - res.sendFile(pathResolve(__dirname, '../../templates/oauth-callback.html')); - } catch (error) { - // Error response - return ResponseHelper.sendErrorResponse(res, error); - } - }); - + }, + ); // ---------------------------------------- // Executions // ---------------------------------------- - // Returns all finished executions - this.app.get(`/${this.restEndpoint}/executions`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - let filter: any = {}; // tslint:disable-line:no-any + this.app.get( + `/${this.restEndpoint}/executions`, + ResponseHelper.send( + async (req: express.Request, res: express.Response): Promise => { + let filter: any = {}; - if (req.query.filter) { - filter = JSON.parse(req.query.filter as string); - } - - let limit = 20; - if (req.query.limit) { - limit = parseInt(req.query.limit as string, 10); - } - - const executingWorkflowIds: string[] = []; - - if (config.get('executions.mode') === 'queue') { - const currentJobs = await Queue.getInstance().getJobs(['active', 'waiting']); - executingWorkflowIds.push(...currentJobs.map(job => job.data.executionId) as string[]); - } - // We may have manual executions even with queue so we must account for these. - executingWorkflowIds.push(...this.activeExecutionsInstance.getActiveExecutions().map(execution => execution.id.toString()) as string[]); - - const countFilter = JSON.parse(JSON.stringify(filter)); - if (countFilter.waitTill !== undefined) { - countFilter.waitTill = Not(IsNull()); - } - countFilter.id = Not(In(executingWorkflowIds)); - - const resultsQuery = await Db.collections.Execution! - .createQueryBuilder("execution") - .select([ - 'execution.id', - 'execution.finished', - 'execution.mode', - 'execution.retryOf', - 'execution.retrySuccessId', - 'execution.waitTill', - 'execution.startedAt', - 'execution.stoppedAt', - 'execution.workflowData', - ]) - .orderBy('execution.id', 'DESC') - .take(limit); - - Object.keys(filter).forEach((filterField) => { - if (filterField === 'waitTill') { - resultsQuery.andWhere(`execution.${filterField} is not null`); - } else if(filterField === 'finished' && filter[filterField] === false) { - resultsQuery.andWhere(`execution.${filterField} = :${filterField}`, {[filterField]: filter[filterField]}); - resultsQuery.andWhere(`execution.waitTill is null`); - } else { - resultsQuery.andWhere(`execution.${filterField} = :${filterField}`, {[filterField]: filter[filterField]}); - } - }); - if (req.query.lastId) { - resultsQuery.andWhere(`execution.id < :lastId`, {lastId: req.query.lastId}); - } - if (req.query.firstId) { - resultsQuery.andWhere(`execution.id > :firstId`, {firstId: req.query.firstId}); - } - if (executingWorkflowIds.length > 0) { - resultsQuery.andWhere(`execution.id NOT IN (:...ids)`, {ids: executingWorkflowIds}); - } - - const resultsPromise = resultsQuery.getMany(); - - const countPromise = getExecutionsCount(countFilter); - - const results: IExecutionFlattedDb[] = await resultsPromise; - const countedObjects = await countPromise; - - const returnResults: IExecutionsSummary[] = []; - - for (const result of results) { - returnResults.push({ - id: result.id!.toString(), - finished: result.finished, - mode: result.mode, - retryOf: result.retryOf ? result.retryOf.toString() : undefined, - retrySuccessId: result.retrySuccessId ? result.retrySuccessId.toString() : undefined, - waitTill: result.waitTill as Date | undefined, - startedAt: result.startedAt, - stoppedAt: result.stoppedAt, - workflowId: result.workflowData!.id ? result.workflowData!.id!.toString() : '', - workflowName: result.workflowData!.name, - }); - } - - return { - count: countedObjects.count, - results: returnResults, - estimated: countedObjects.estimate, - }; - })); - - - // Returns a specific execution - this.app.get(`/${this.restEndpoint}/executions/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - const result = await Db.collections.Execution!.findOne(req.params.id); - - if (result === undefined) { - return undefined; - } - - if (req.query.unflattedResponse === 'true') { - const fullExecutionData = ResponseHelper.unflattenExecutionData(result); - return fullExecutionData as IExecutionResponse; - } else { - // Convert to response format in which the id is a string - (result as IExecutionFlatted as IExecutionFlattedResponse).id = result.id.toString(); - return result as IExecutionFlatted as IExecutionFlattedResponse; - } - })); - - - // Retries a failed execution - this.app.post(`/${this.restEndpoint}/executions/:id/retry`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - // Get the data to execute - const fullExecutionDataFlatted = await Db.collections.Execution!.findOne(req.params.id); - - if (fullExecutionDataFlatted === undefined) { - throw new ResponseHelper.ResponseError(`The execution with the id "${req.params.id}" does not exist.`, 404, 404); - } - - const fullExecutionData = ResponseHelper.unflattenExecutionData(fullExecutionDataFlatted); - - if (fullExecutionData.finished === true) { - throw new Error('The execution did succeed and can so not be retried.'); - } - - const executionMode = 'retry'; - - fullExecutionData.workflowData.active = false; - - // Start the workflow - const data: IWorkflowExecutionDataProcess = { - executionMode, - executionData: fullExecutionData.data, - retryOf: req.params.id, - workflowData: fullExecutionData.workflowData, - }; - - const lastNodeExecuted = data!.executionData!.resultData.lastNodeExecuted as string | undefined; - - if (lastNodeExecuted) { - // Remove the old error and the data of the last run of the node that it can be replaced - delete data!.executionData!.resultData.error; - const length = data!.executionData!.resultData.runData[lastNodeExecuted].length; - if (length > 0 && data!.executionData!.resultData.runData[lastNodeExecuted][length - 1].error !== undefined) { - // Remove results only if it is an error. - // If we are retrying due to a crash, the information is simply success info from last node - data!.executionData!.resultData.runData[lastNodeExecuted].pop(); - // Stack will determine what to run next - } - } - - if (req.body.loadWorkflow === true) { - // Loads the currently saved workflow to execute instead of the - // one saved at the time of the execution. - const workflowId = fullExecutionData.workflowData.id; - const workflowData = await Db.collections.Workflow!.findOne(workflowId) as IWorkflowBase; - - if (workflowData === undefined) { - throw new Error(`The workflow with the ID "${workflowId}" could not be found and so the data not be loaded for the retry.`); - } - - data.workflowData = workflowData; - const nodeTypes = NodeTypes(); - const workflowInstance = new Workflow({ id: workflowData.id as string, name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: false, nodeTypes, staticData: undefined, settings: workflowData.settings }); - - // Replace all of the nodes in the execution stack with the ones of the new workflow - for (const stack of data!.executionData!.executionData!.nodeExecutionStack) { - // Find the data of the last executed node in the new workflow - const node = workflowInstance.getNode(stack.node.name); - if (node === null) { - throw new Error(`Could not find the node "${stack.node.name}" in workflow. It probably got deleted or renamed. Without it the workflow can sadly not be retried.`); + if (req.query.filter) { + filter = JSON.parse(req.query.filter as string); } - // Replace the node data in the stack that it really uses the current data - stack.node = node; + let limit = 20; + if (req.query.limit) { + limit = parseInt(req.query.limit as string, 10); + } + + const executingWorkflowIds: string[] = []; + + if (config.get('executions.mode') === 'queue') { + const currentJobs = await Queue.getInstance().getJobs(['active', 'waiting']); + executingWorkflowIds.push( + ...(currentJobs.map((job) => job.data.executionId) as string[]), + ); + } + // We may have manual executions even with queue so we must account for these. + executingWorkflowIds.push( + ...this.activeExecutionsInstance + .getActiveExecutions() + .map((execution) => execution.id.toString()), + ); + + const countFilter = JSON.parse(JSON.stringify(filter)); + if (countFilter.waitTill !== undefined) { + countFilter.waitTill = Not(IsNull()); + } + countFilter.id = Not(In(executingWorkflowIds)); + + const resultsQuery = await Db.collections + .Execution!.createQueryBuilder('execution') + .select([ + 'execution.id', + 'execution.finished', + 'execution.mode', + 'execution.retryOf', + 'execution.retrySuccessId', + 'execution.waitTill', + 'execution.startedAt', + 'execution.stoppedAt', + 'execution.workflowData', + ]) + .orderBy('execution.id', 'DESC') + .take(limit); + + Object.keys(filter).forEach((filterField) => { + if (filterField === 'waitTill') { + resultsQuery.andWhere(`execution.${filterField} is not null`); + } else if (filterField === 'finished' && filter[filterField] === false) { + resultsQuery.andWhere(`execution.${filterField} = :${filterField}`, { + [filterField]: filter[filterField], + }); + resultsQuery.andWhere(`execution.waitTill is null`); + } else { + resultsQuery.andWhere(`execution.${filterField} = :${filterField}`, { + [filterField]: filter[filterField], + }); + } + }); + if (req.query.lastId) { + resultsQuery.andWhere(`execution.id < :lastId`, { lastId: req.query.lastId }); + } + if (req.query.firstId) { + resultsQuery.andWhere(`execution.id > :firstId`, { firstId: req.query.firstId }); + } + if (executingWorkflowIds.length > 0) { + resultsQuery.andWhere(`execution.id NOT IN (:...ids)`, { ids: executingWorkflowIds }); + } + + const resultsPromise = resultsQuery.getMany(); + + const countPromise = getExecutionsCount(countFilter); + + const results: IExecutionFlattedDb[] = await resultsPromise; + const countedObjects = await countPromise; + + const returnResults: IExecutionsSummary[] = []; + + for (const result of results) { + returnResults.push({ + id: result.id.toString(), + finished: result.finished, + mode: result.mode, + retryOf: result.retryOf ? result.retryOf.toString() : undefined, + retrySuccessId: result.retrySuccessId ? result.retrySuccessId.toString() : undefined, + waitTill: result.waitTill as Date | undefined, + startedAt: result.startedAt, + stoppedAt: result.stoppedAt, + workflowId: result.workflowData.id ? result.workflowData.id.toString() : '', + workflowName: result.workflowData.name, + }); + } + + return { + count: countedObjects.count, + results: returnResults, + estimated: countedObjects.estimate, + }; + }, + ), + ); + + // Returns a specific execution + this.app.get( + `/${this.restEndpoint}/executions/:id`, + ResponseHelper.send( + async ( + req: express.Request, + res: express.Response, + ): Promise => { + const result = await Db.collections.Execution!.findOne(req.params.id); + + if (result === undefined) { + return undefined; + } + + if (req.query.unflattedResponse === 'true') { + const fullExecutionData = ResponseHelper.unflattenExecutionData(result); + return fullExecutionData; + } + // Convert to response format in which the id is a string + (result as IExecutionFlatted as IExecutionFlattedResponse).id = result.id.toString(); + return result as IExecutionFlatted as IExecutionFlattedResponse; + }, + ), + ); + + // Retries a failed execution + this.app.post( + `/${this.restEndpoint}/executions/:id/retry`, + ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + // Get the data to execute + const fullExecutionDataFlatted = await Db.collections.Execution!.findOne(req.params.id); + + if (fullExecutionDataFlatted === undefined) { + throw new ResponseHelper.ResponseError( + `The execution with the id "${req.params.id}" does not exist.`, + 404, + 404, + ); } - } - const workflowRunner = new WorkflowRunner(); - const executionId = await workflowRunner.run(data); + const fullExecutionData = ResponseHelper.unflattenExecutionData(fullExecutionDataFlatted); - const executionData = await this.activeExecutionsInstance.getPostExecutePromise(executionId); + if (fullExecutionData.finished) { + throw new Error('The execution did succeed and can so not be retried.'); + } - if (executionData === undefined) { - throw new Error('The retry did not start for an unknown reason.'); - } + const executionMode = 'retry'; - return !!executionData.finished; - })); + fullExecutionData.workflowData.active = false; + // Start the workflow + const data: IWorkflowExecutionDataProcess = { + executionMode, + executionData: fullExecutionData.data, + retryOf: req.params.id, + workflowData: fullExecutionData.workflowData, + }; + + const { lastNodeExecuted } = data.executionData!.resultData; + + if (lastNodeExecuted) { + // Remove the old error and the data of the last run of the node that it can be replaced + delete data.executionData!.resultData.error; + const { length } = data.executionData!.resultData.runData[lastNodeExecuted]; + if ( + length > 0 && + data.executionData!.resultData.runData[lastNodeExecuted][length - 1].error !== undefined + ) { + // Remove results only if it is an error. + // If we are retrying due to a crash, the information is simply success info from last node + data.executionData!.resultData.runData[lastNodeExecuted].pop(); + // Stack will determine what to run next + } + } + + if (req.body.loadWorkflow === true) { + // Loads the currently saved workflow to execute instead of the + // one saved at the time of the execution. + const workflowId = fullExecutionData.workflowData.id; + const workflowData = (await Db.collections.Workflow!.findOne( + workflowId, + )) as IWorkflowBase; + + if (workflowData === undefined) { + throw new Error( + `The workflow with the ID "${workflowId}" could not be found and so the data not be loaded for the retry.`, + ); + } + + data.workflowData = workflowData; + const nodeTypes = NodeTypes(); + const workflowInstance = new Workflow({ + id: workflowData.id as string, + name: workflowData.name, + nodes: workflowData.nodes, + connections: workflowData.connections, + active: false, + nodeTypes, + staticData: undefined, + settings: workflowData.settings, + }); + + // Replace all of the nodes in the execution stack with the ones of the new workflow + for (const stack of data.executionData!.executionData!.nodeExecutionStack) { + // Find the data of the last executed node in the new workflow + const node = workflowInstance.getNode(stack.node.name); + if (node === null) { + throw new Error( + `Could not find the node "${stack.node.name}" in workflow. It probably got deleted or renamed. Without it the workflow can sadly not be retried.`, + ); + } + + // Replace the node data in the stack that it really uses the current data + stack.node = node; + } + } + + const workflowRunner = new WorkflowRunner(); + const executionId = await workflowRunner.run(data); + + const executionData = await this.activeExecutionsInstance.getPostExecutePromise( + executionId, + ); + + if (executionData === undefined) { + throw new Error('The retry did not start for an unknown reason.'); + } + + return !!executionData.finished; + }), + ); // Delete Executions // INFORMATION: We use POST instead of DELETE to not run into any issues // with the query data getting to long - this.app.post(`/${this.restEndpoint}/executions/delete`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - const deleteData = req.body as IExecutionDeleteFilter; + this.app.post( + `/${this.restEndpoint}/executions/delete`, + ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + const deleteData = req.body as IExecutionDeleteFilter; - if (deleteData.deleteBefore !== undefined) { - const filters = { - startedAt: LessThanOrEqual(deleteData.deleteBefore), - }; - if (deleteData.filters !== undefined) { - Object.assign(filters, deleteData.filters); + if (deleteData.deleteBefore !== undefined) { + const filters = { + startedAt: LessThanOrEqual(deleteData.deleteBefore), + }; + if (deleteData.filters !== undefined) { + Object.assign(filters, deleteData.filters); + } + + await Db.collections.Execution!.delete(filters); + } else if (deleteData.ids !== undefined) { + // Deletes all executions with the given ids + await Db.collections.Execution!.delete(deleteData.ids); + } else { + throw new Error('Required body-data "ids" or "deleteBefore" is missing!'); } - - await Db.collections.Execution!.delete(filters); - } else if (deleteData.ids !== undefined) { - // Deletes all executions with the given ids - await Db.collections.Execution!.delete(deleteData.ids); - } else { - throw new Error('Required body-data "ids" or "deleteBefore" is missing!'); - } - })); - + }), + ); // ---------------------------------------- // Executing Workflows // ---------------------------------------- - // Returns all the currently working executions - this.app.get(`/${this.restEndpoint}/executions-current`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - if (config.get('executions.mode') === 'queue') { - const currentJobs = await Queue.getInstance().getJobs(['active', 'waiting']); + this.app.get( + `/${this.restEndpoint}/executions-current`, + ResponseHelper.send( + async (req: express.Request, res: express.Response): Promise => { + if (config.get('executions.mode') === 'queue') { + const currentJobs = await Queue.getInstance().getJobs(['active', 'waiting']); - const currentlyRunningQueueIds = currentJobs.map(job => job.data.executionId); + const currentlyRunningQueueIds = currentJobs.map((job) => job.data.executionId); - const currentlyRunningManualExecutions = this.activeExecutionsInstance.getActiveExecutions(); - const manualExecutionIds = currentlyRunningManualExecutions.map(execution => execution.id); + const currentlyRunningManualExecutions = + this.activeExecutionsInstance.getActiveExecutions(); + const manualExecutionIds = currentlyRunningManualExecutions.map( + (execution) => execution.id, + ); - const currentlyRunningExecutionIds = currentlyRunningQueueIds.concat(manualExecutionIds); + const currentlyRunningExecutionIds = + currentlyRunningQueueIds.concat(manualExecutionIds); - if (currentlyRunningExecutionIds.length === 0) { - return []; - } + if (currentlyRunningExecutionIds.length === 0) { + return []; + } - const resultsQuery = await Db.collections.Execution! - .createQueryBuilder("execution") - .select([ - 'execution.id', - 'execution.workflowId', - 'execution.mode', - 'execution.retryOf', - 'execution.startedAt', - ]) - .orderBy('execution.id', 'DESC') - .andWhere(`execution.id IN (:...ids)`, {ids: currentlyRunningExecutionIds}); + const resultsQuery = await Db.collections + .Execution!.createQueryBuilder('execution') + .select([ + 'execution.id', + 'execution.workflowId', + 'execution.mode', + 'execution.retryOf', + 'execution.startedAt', + ]) + .orderBy('execution.id', 'DESC') + .andWhere(`execution.id IN (:...ids)`, { ids: currentlyRunningExecutionIds }); - if (req.query.filter) { - const filter = JSON.parse(req.query.filter as string); - if (filter.workflowId !== undefined) { - resultsQuery.andWhere('execution.workflowId = :workflowId', {workflowId: filter.workflowId}); + if (req.query.filter) { + const filter = JSON.parse(req.query.filter as string); + if (filter.workflowId !== undefined) { + resultsQuery.andWhere('execution.workflowId = :workflowId', { + workflowId: filter.workflowId, + }); + } + } + + const results = await resultsQuery.getMany(); + + return results.map((result) => { + return { + id: result.id, + workflowId: result.workflowId, + mode: result.mode, + retryOf: result.retryOf !== null ? result.retryOf : undefined, + startedAt: new Date(result.startedAt), + } as IExecutionsSummary; + }); } - } + const executingWorkflows = this.activeExecutionsInstance.getActiveExecutions(); - const results = await resultsQuery.getMany(); + const returnData: IExecutionsSummary[] = []; - return results.map(result => { - return { - id: result.id, - workflowId: result.workflowId, - mode: result.mode, - retryOf: result.retryOf !== null ? result.retryOf : undefined, - startedAt: new Date(result.startedAt), - } as IExecutionsSummary; - }); - } else { - const executingWorkflows = this.activeExecutionsInstance.getActiveExecutions(); - - const returnData: IExecutionsSummary[] = []; - - let filter: any = {}; // tslint:disable-line:no-any - if (req.query.filter) { - filter = JSON.parse(req.query.filter as string); - } - - for (const data of executingWorkflows) { - if (filter.workflowId !== undefined && filter.workflowId !== data.workflowId) { - continue; + let filter: any = {}; + if (req.query.filter) { + filter = JSON.parse(req.query.filter as string); } - returnData.push( - { + + for (const data of executingWorkflows) { + if (filter.workflowId !== undefined && filter.workflowId !== data.workflowId) { + continue; + } + returnData.push({ id: data.id.toString(), workflowId: data.workflowId === undefined ? '' : data.workflowId.toString(), mode: data.mode, retryOf: data.retryOf, startedAt: new Date(data.startedAt), - } - ); - } - returnData.sort((a, b) => parseInt(b.id, 10) - parseInt(a.id, 10)); + }); + } + returnData.sort((a, b) => parseInt(b.id, 10) - parseInt(a.id, 10)); - return returnData; - } - })); + return returnData; + }, + ), + ); // Forces the execution to stop - this.app.post(`/${this.restEndpoint}/executions-current/:id/stop`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - if (config.get('executions.mode') === 'queue') { - // Manual executions should still be stoppable, so - // try notifying the `activeExecutions` to stop it. - const result = await this.activeExecutionsInstance.stopExecution(req.params.id); + this.app.post( + `/${this.restEndpoint}/executions-current/:id/stop`, + ResponseHelper.send( + async (req: express.Request, res: express.Response): Promise => { + if (config.get('executions.mode') === 'queue') { + // Manual executions should still be stoppable, so + // try notifying the `activeExecutions` to stop it. + const result = await this.activeExecutionsInstance.stopExecution(req.params.id); - if (result === undefined) { - // If active execution could not be found check if it is a waiting one - try { - return await this.waitTracker.stopExecution(req.params.id); - } catch (error) { - // Ignore, if it errors as then it is probably a currently running - // execution + if (result === undefined) { + // If active execution could not be found check if it is a waiting one + try { + return await this.waitTracker.stopExecution(req.params.id); + } catch (error) { + // Ignore, if it errors as then it is probably a currently running + // execution + } + } else { + return { + mode: result.mode, + startedAt: new Date(result.startedAt), + stoppedAt: result.stoppedAt ? new Date(result.stoppedAt) : undefined, + finished: result.finished, + } as IExecutionsStopData; + } + + const currentJobs = await Queue.getInstance().getJobs(['active', 'waiting']); + + const job = currentJobs.find( + (job) => job.data.executionId.toString() === req.params.id, + ); + + if (!job) { + throw new Error(`Could not stop "${req.params.id}" as it is no longer in queue.`); + } else { + await Queue.getInstance().stopJob(job); + } + + const executionDb = (await Db.collections.Execution?.findOne( + req.params.id, + )) as IExecutionFlattedDb; + const fullExecutionData = ResponseHelper.unflattenExecutionData(executionDb); + + const returnData: IExecutionsStopData = { + mode: fullExecutionData.mode, + startedAt: new Date(fullExecutionData.startedAt), + stoppedAt: fullExecutionData.stoppedAt + ? new Date(fullExecutionData.stoppedAt) + : undefined, + finished: fullExecutionData.finished, + }; + + return returnData; } - } else { - return { - mode: result.mode, - startedAt: new Date(result.startedAt), - stoppedAt: result.stoppedAt ? new Date(result.stoppedAt) : undefined, - finished: result.finished, - } as IExecutionsStopData; - } + const executionId = req.params.id; - const currentJobs = await Queue.getInstance().getJobs(['active', 'waiting']); + // Stopt he execution and wait till it is done and we got the data + const result = await this.activeExecutionsInstance.stopExecution(executionId); - const job = currentJobs.find(job => job.data.executionId.toString() === req.params.id); - - if (!job) { - throw new Error(`Could not stop "${req.params.id}" as it is no longer in queue.`); - } else { - await Queue.getInstance().stopJob(job); - } - - const executionDb = await Db.collections.Execution?.findOne(req.params.id) as IExecutionFlattedDb; - const fullExecutionData = ResponseHelper.unflattenExecutionData(executionDb) as IExecutionResponse; - - const returnData: IExecutionsStopData = { - mode: fullExecutionData.mode, - startedAt: new Date(fullExecutionData.startedAt), - stoppedAt: fullExecutionData.stoppedAt ? new Date(fullExecutionData.stoppedAt) : undefined, - finished: fullExecutionData.finished, - }; - - return returnData; - - } else { - const executionId = req.params.id; - - // Stopt he execution and wait till it is done and we got the data - const result = await this.activeExecutionsInstance.stopExecution(executionId); - - let returnData: IExecutionsStopData; - if (result === undefined) { - // If active execution could not be found check if it is a waiting one - returnData = await this.waitTracker.stopExecution(executionId); - } else { - returnData = { - mode: result.mode, - startedAt: new Date(result.startedAt), - stoppedAt: result.stoppedAt ? new Date(result.stoppedAt) : undefined, - finished: result.finished, - }; - } - - return returnData; - } - })); + let returnData: IExecutionsStopData; + if (result === undefined) { + // If active execution could not be found check if it is a waiting one + returnData = await this.waitTracker.stopExecution(executionId); + } else { + returnData = { + mode: result.mode, + startedAt: new Date(result.startedAt), + stoppedAt: result.stoppedAt ? new Date(result.stoppedAt) : undefined, + finished: result.finished, + }; + } + return returnData; + }, + ), + ); // Removes a test webhook - this.app.delete(`/${this.restEndpoint}/test-webhook/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - const workflowId = req.params.id; - return this.testWebhooks.cancelTestWebhook(workflowId); - })); - - + this.app.delete( + `/${this.restEndpoint}/test-webhook/:id`, + ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + const workflowId = req.params.id; + return this.testWebhooks.cancelTestWebhook(workflowId); + }), + ); // ---------------------------------------- // Options // ---------------------------------------- // Returns all the available timezones - this.app.get(`/${this.restEndpoint}/options/timezones`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - return timezones; - })); - - - + this.app.get( + `/${this.restEndpoint}/options/timezones`, + ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + return timezones; + }), + ); // ---------------------------------------- // Settings // ---------------------------------------- - // 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 this.frontendSettings; - })); - - + this.app.get( + `/${this.restEndpoint}/settings`, + ResponseHelper.send( + async (req: express.Request, res: express.Response): Promise => { + return this.frontendSettings; + }, + ), + ); // ---------------------------------------- // Webhooks @@ -2023,188 +2494,224 @@ class App { const waitingWebhooks = new WaitingWebhooks(); // HEAD webhook-waiting requests - this.app.head(`/${this.endpointWebhookWaiting}/*`, async (req: express.Request, res: express.Response) => { - // Cut away the "/webhook-waiting/" to get the registred part of the url - const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhookWaiting.length + 2); + this.app.head( + `/${this.endpointWebhookWaiting}/*`, + async (req: express.Request, res: express.Response) => { + // Cut away the "/webhook-waiting/" to get the registred part of the url + const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice( + this.endpointWebhookWaiting.length + 2, + ); - let response; - try { - response = await waitingWebhooks.executeWebhook('HEAD', requestUrl, req, res); - } catch (error) { - ResponseHelper.sendErrorResponse(res, error); - return; - } + let response; + try { + response = await waitingWebhooks.executeWebhook('HEAD', requestUrl, req, res); + } catch (error) { + ResponseHelper.sendErrorResponse(res, error); + return; + } - if (response.noWebhookResponse === true) { - // Nothing else to do as the response got already sent - return; - } + if (response.noWebhookResponse === true) { + // Nothing else to do as the response got already sent + return; + } - ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); - }); + ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); + }, + ); // GET webhook-waiting requests - this.app.get(`/${this.endpointWebhookWaiting}/*`, async (req: express.Request, res: express.Response) => { - // Cut away the "/webhook-waiting/" to get the registred part of the url - const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhookWaiting.length + 2); + this.app.get( + `/${this.endpointWebhookWaiting}/*`, + async (req: express.Request, res: express.Response) => { + // Cut away the "/webhook-waiting/" to get the registred part of the url + const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice( + this.endpointWebhookWaiting.length + 2, + ); - let response; - try { - response = await waitingWebhooks.executeWebhook('GET', requestUrl, req, res); - } catch (error) { - ResponseHelper.sendErrorResponse(res, error); - return; - } + let response; + try { + response = await waitingWebhooks.executeWebhook('GET', requestUrl, req, res); + } catch (error) { + ResponseHelper.sendErrorResponse(res, error); + return; + } - if (response.noWebhookResponse === true) { - // Nothing else to do as the response got already sent - return; - } + if (response.noWebhookResponse === true) { + // Nothing else to do as the response got already sent + return; + } - ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); - }); + ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); + }, + ); // POST webhook-waiting requests - this.app.post(`/${this.endpointWebhookWaiting}/*`, async (req: express.Request, res: express.Response) => { - // Cut away the "/webhook-waiting/" to get the registred part of the url - const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhookWaiting.length + 2); + this.app.post( + `/${this.endpointWebhookWaiting}/*`, + async (req: express.Request, res: express.Response) => { + // Cut away the "/webhook-waiting/" to get the registred part of the url + const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice( + this.endpointWebhookWaiting.length + 2, + ); - let response; - try { - response = await waitingWebhooks.executeWebhook('POST', requestUrl, req, res); - } catch (error) { - ResponseHelper.sendErrorResponse(res, error); - return; - } + let response; + try { + response = await waitingWebhooks.executeWebhook('POST', requestUrl, req, res); + } catch (error) { + ResponseHelper.sendErrorResponse(res, error); + return; + } - if (response.noWebhookResponse === true) { - // Nothing else to do as the response got already sent - return; - } - - ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); - }); + if (response.noWebhookResponse === true) { + // Nothing else to do as the response got already sent + return; + } + ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); + }, + ); // HEAD webhook requests (test for UI) - this.app.head(`/${this.endpointWebhookTest}/*`, async (req: express.Request, res: express.Response) => { - // Cut away the "/webhook-test/" to get the registred part of the url - const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhookTest.length + 2); + this.app.head( + `/${this.endpointWebhookTest}/*`, + async (req: express.Request, res: express.Response) => { + // Cut away the "/webhook-test/" to get the registred part of the url + const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice( + this.endpointWebhookTest.length + 2, + ); - let response; - try { - response = await this.testWebhooks.callTestWebhook('HEAD', requestUrl, req, res); - } catch (error) { - ResponseHelper.sendErrorResponse(res, error); - return; - } + let response; + try { + response = await this.testWebhooks.callTestWebhook('HEAD', requestUrl, req, res); + } catch (error) { + ResponseHelper.sendErrorResponse(res, error); + return; + } - if (response.noWebhookResponse === true) { - // Nothing else to do as the response got already sent - return; - } + if (response.noWebhookResponse === true) { + // Nothing else to do as the response got already sent + return; + } - ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); - }); + ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); + }, + ); // HEAD webhook requests (test for UI) - this.app.options(`/${this.endpointWebhookTest}/*`, async (req: express.Request, res: express.Response) => { - // Cut away the "/webhook-test/" to get the registred part of the url - const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhookTest.length + 2); + this.app.options( + `/${this.endpointWebhookTest}/*`, + async (req: express.Request, res: express.Response) => { + // Cut away the "/webhook-test/" to get the registred part of the url + const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice( + this.endpointWebhookTest.length + 2, + ); - let allowedMethods: string[]; - try { - allowedMethods = await this.testWebhooks.getWebhookMethods(requestUrl); - allowedMethods.push('OPTIONS'); + let allowedMethods: string[]; + try { + allowedMethods = await this.testWebhooks.getWebhookMethods(requestUrl); + allowedMethods.push('OPTIONS'); - // Add custom "Allow" header to satisfy OPTIONS response. - res.append('Allow', allowedMethods); - } catch (error) { - ResponseHelper.sendErrorResponse(res, error); - return; - } + // Add custom "Allow" header to satisfy OPTIONS response. + res.append('Allow', allowedMethods); + } catch (error) { + ResponseHelper.sendErrorResponse(res, error); + return; + } - ResponseHelper.sendSuccessResponse(res, {}, true, 204); - }); + ResponseHelper.sendSuccessResponse(res, {}, true, 204); + }, + ); // GET webhook requests (test for UI) - this.app.get(`/${this.endpointWebhookTest}/*`, async (req: express.Request, res: express.Response) => { - // Cut away the "/webhook-test/" to get the registred part of the url - const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhookTest.length + 2); + this.app.get( + `/${this.endpointWebhookTest}/*`, + async (req: express.Request, res: express.Response) => { + // Cut away the "/webhook-test/" to get the registred part of the url + const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice( + this.endpointWebhookTest.length + 2, + ); - let response; - try { - response = await this.testWebhooks.callTestWebhook('GET', requestUrl, req, res); - } catch (error) { - ResponseHelper.sendErrorResponse(res, error); - return; - } + let response; + try { + response = await this.testWebhooks.callTestWebhook('GET', requestUrl, req, res); + } catch (error) { + ResponseHelper.sendErrorResponse(res, error); + return; + } - if (response.noWebhookResponse === true) { - // Nothing else to do as the response got already sent - return; - } + if (response.noWebhookResponse === true) { + // Nothing else to do as the response got already sent + return; + } - ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); - }); + ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); + }, + ); // POST webhook requests (test for UI) - this.app.post(`/${this.endpointWebhookTest}/*`, async (req: express.Request, res: express.Response) => { - // Cut away the "/webhook-test/" to get the registred part of the url - const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhookTest.length + 2); + this.app.post( + `/${this.endpointWebhookTest}/*`, + async (req: express.Request, res: express.Response) => { + // Cut away the "/webhook-test/" to get the registred part of the url + const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice( + this.endpointWebhookTest.length + 2, + ); - let response; - try { - response = await this.testWebhooks.callTestWebhook('POST', requestUrl, req, res); - } catch (error) { - ResponseHelper.sendErrorResponse(res, error); - return; - } + let response; + try { + response = await this.testWebhooks.callTestWebhook('POST', requestUrl, req, res); + } catch (error) { + ResponseHelper.sendErrorResponse(res, error); + return; + } - if (response.noWebhookResponse === true) { - // Nothing else to do as the response got already sent - return; - } - - ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); - }); + if (response.noWebhookResponse === true) { + // Nothing else to do as the response got already sent + return; + } + ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); + }, + ); if (this.endpointPresetCredentials !== '') { - // POST endpoint to set preset credentials - this.app.post(`/${this.endpointPresetCredentials}`, async (req: express.Request, res: express.Response) => { + this.app.post( + `/${this.endpointPresetCredentials}`, + async (req: express.Request, res: express.Response) => { + if (!this.presetCredentialsLoaded) { + const body = req.body as ICredentialsOverwrite; - if (this.presetCredentialsLoaded === false) { + if (req.headers['content-type'] !== 'application/json') { + ResponseHelper.sendErrorResponse( + res, + new Error( + 'Body must be a valid JSON, make sure the content-type is application/json', + ), + ); + return; + } - const body = req.body as ICredentialsOverwrite; + const loadNodesAndCredentials = LoadNodesAndCredentials(); - if (req.headers['content-type'] !== 'application/json') { - ResponseHelper.sendErrorResponse(res, new Error('Body must be a valid JSON, make sure the content-type is application/json')); - return; + const credentialsOverwrites = CredentialsOverwrites(); + + await credentialsOverwrites.init(body); + + const credentialTypes = CredentialTypes(); + + await credentialTypes.init(loadNodesAndCredentials.credentialTypes); + + this.presetCredentialsLoaded = true; + + ResponseHelper.sendSuccessResponse(res, { success: true }, true, 200); + } else { + ResponseHelper.sendErrorResponse(res, new Error('Preset credentials can be set once')); } - - const loadNodesAndCredentials = LoadNodesAndCredentials(); - - const credentialsOverwrites = CredentialsOverwrites(); - - await credentialsOverwrites.init(body); - - const credentialTypes = CredentialTypes(); - - await credentialTypes.init(loadNodesAndCredentials.credentialTypes); - - this.presetCredentialsLoaded = true; - - ResponseHelper.sendSuccessResponse(res, { success: true }, true, 200); - - } else { - ResponseHelper.sendErrorResponse(res, new Error('Preset credentials can be set once')); - } - }); + }, + ); } - // Read the index file and replace the path placeholder const editorUiPath = require.resolve('n8n-editor-ui'); const filePath = pathJoin(pathDirname(editorUiPath), 'dist', 'index.html'); @@ -2220,20 +2727,22 @@ class App { }); // Serve the website - const startTime = (new Date()).toUTCString(); - this.app.use('/', express.static(pathJoin(pathDirname(editorUiPath), 'dist'), { - index: 'index.html', - setHeaders: (res, path) => { - if (res.req && res.req.url === '/index.html') { - // Set last modified date manually to n8n start time so - // that it hopefully refreshes the page when a new version - // got used - res.setHeader('Last-Modified', startTime); - } - }, - })); + const startTime = new Date().toUTCString(); + this.app.use( + '/', + express.static(pathJoin(pathDirname(editorUiPath), 'dist'), { + index: 'index.html', + setHeaders: (res, path) => { + if (res.req && res.req.url === '/index.html') { + // Set last modified date manually to n8n start time so + // that it hopefully refreshes the page when a new version + // got used + res.setHeader('Last-Modified', startTime); + } + }, + }), + ); } - } export async function start(): Promise { @@ -2266,10 +2775,11 @@ export async function start(): Promise { }); } -async function getExecutionsCount(countFilter: IDataObject): Promise<{ count: number; estimate: boolean; }> { - - const dbType = await GenericHelpers.getConfigValue('database.type') as DatabaseType; - const filteredFields = Object.keys(countFilter).filter(field => field !== 'id'); +async function getExecutionsCount( + countFilter: IDataObject, +): Promise<{ count: number; estimate: boolean }> { + const dbType = (await GenericHelpers.getConfigValue('database.type')) as DatabaseType; + const filteredFields = Object.keys(countFilter).filter((field) => field !== 'id'); // Do regular count for other databases than pgsql and // if we are filtering based on workflowId or finished fields. @@ -2280,8 +2790,11 @@ async function getExecutionsCount(countFilter: IDataObject): Promise<{ count: nu try { // Get an estimate of rows count. - const estimateRowsNumberSql = "SELECT n_live_tup FROM pg_stat_all_tables WHERE relname = 'execution_entity';"; - const rows: Array<{ n_live_tup: string }> = await Db.collections.Execution!.query(estimateRowsNumberSql); + const estimateRowsNumberSql = + "SELECT n_live_tup FROM pg_stat_all_tables WHERE relname = 'execution_entity';"; + const rows: Array<{ n_live_tup: string }> = await Db.collections.Execution!.query( + estimateRowsNumberSql, + ); const estimate = parseInt(rows[0].n_live_tup, 10); // If over 100k, return just an estimate. @@ -2291,7 +2804,7 @@ async function getExecutionsCount(countFilter: IDataObject): Promise<{ count: nu return { count: estimate, estimate: true }; } } catch (err) { - LoggerProxy.warn('Unable to get executions count from postgres: ' + err); + LoggerProxy.warn(`Unable to get executions count from postgres: ${err}`); } const count = await Db.collections.Execution!.count(countFilter); @@ -2300,7 +2813,11 @@ async function getExecutionsCount(countFilter: IDataObject): Promise<{ count: nu async function generateInstanceId() { const encryptionKey = await UserSettings.getEncryptionKey(); - const hash = encryptionKey ? createHash('sha256').update(encryptionKey.slice(Math.round(encryptionKey.length / 2))).digest('hex') : undefined; + const hash = encryptionKey + ? createHash('sha256') + .update(encryptionKey.slice(Math.round(encryptionKey.length / 2))) + .digest('hex') + : undefined; return hash; } diff --git a/packages/cli/src/TagHelpers.ts b/packages/cli/src/TagHelpers.ts index 4a9052a300..dc54fb2469 100644 --- a/packages/cli/src/TagHelpers.ts +++ b/packages/cli/src/TagHelpers.ts @@ -1,18 +1,14 @@ -import { getConnection } from "typeorm"; +/* eslint-disable no-param-reassign */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable import/no-cycle */ +import { getConnection } from 'typeorm'; import { validate } from 'class-validator'; -import { - ResponseHelper, -} from "."; +import { ResponseHelper } from '.'; -import { - TagEntity, -} from "./databases/entities/TagEntity"; - -import { - ITagWithCountDb, -} from "./Interfaces"; +import { TagEntity } from './databases/entities/TagEntity'; +import { ITagWithCountDb } from './Interfaces'; // ---------------------------------- // utils @@ -29,7 +25,7 @@ export function sortByRequestOrder(tagsDb: TagEntity[], tagIds: string[]) { return acc; }, {} as { [key: string]: TagEntity }); - return tagIds.map(tagId => tagMap[tagId]); + return tagIds.map((tagId) => tagMap[tagId]); } // ---------------------------------- @@ -43,6 +39,7 @@ export async function validateTag(newTag: TagEntity) { const errors = await validate(newTag); if (errors.length) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const validationErrorMessage = Object.values(errors[0].constraints!)[0]; throw new ResponseHelper.ResponseError(validationErrorMessage, undefined, 400); } @@ -64,23 +61,30 @@ export function throwDuplicateEntryError(error: Error) { /** * Retrieve all tags and the number of workflows each tag is related to. */ -export function getTagsWithCountDb(tablePrefix: string): Promise { +export async function getTagsWithCountDb(tablePrefix: string): Promise { return getConnection() - .createQueryBuilder() - .select(`${tablePrefix}tag_entity.id`, 'id') - .addSelect(`${tablePrefix}tag_entity.name`, 'name') - .addSelect(`COUNT(${tablePrefix}workflows_tags.workflowId)`, 'usageCount') - .from(`${tablePrefix}tag_entity`, 'tag_entity') - .leftJoin(`${tablePrefix}workflows_tags`, 'workflows_tags', `${tablePrefix}workflows_tags.tagId = tag_entity.id`) - .groupBy(`${tablePrefix}tag_entity.id`) - .getRawMany() - .then(tagsWithCount => { - tagsWithCount.forEach(tag => { - tag.id = tag.id.toString(); - tag.usageCount = Number(tag.usageCount); + .createQueryBuilder() + .select(`${tablePrefix}tag_entity.id`, 'id') + .addSelect(`${tablePrefix}tag_entity.name`, 'name') + .addSelect(`COUNT(${tablePrefix}workflows_tags.workflowId)`, 'usageCount') + .from(`${tablePrefix}tag_entity`, 'tag_entity') + .leftJoin( + `${tablePrefix}workflows_tags`, + 'workflows_tags', + `${tablePrefix}workflows_tags.tagId = tag_entity.id`, + ) + .groupBy(`${tablePrefix}tag_entity.id`) + .getRawMany() + .then((tagsWithCount) => { + tagsWithCount.forEach((tag) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + tag.id = tag.id.toString(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + tag.usageCount = Number(tag.usageCount); + }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return tagsWithCount; }); - return tagsWithCount; - }); } // ---------------------------------- @@ -90,19 +94,19 @@ export function getTagsWithCountDb(tablePrefix: string): Promise ({ workflowId, tagId }))) + .values(tagIds.map((tagId) => ({ workflowId, tagId }))) .execute(); } /** * Remove all tags for a workflow during a tag update operation. */ -export function removeRelations(workflowId: string, tablePrefix: string) { +export async function removeRelations(workflowId: string, tablePrefix: string) { return getConnection() .createQueryBuilder() .delete() diff --git a/packages/cli/src/TestWebhooks.ts b/packages/cli/src/TestWebhooks.ts index 96e6f299a5..93048e3c29 100644 --- a/packages/cli/src/TestWebhooks.ts +++ b/packages/cli/src/TestWebhooks.ts @@ -1,16 +1,9 @@ +/* eslint-disable consistent-return */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable no-param-reassign */ import * as express from 'express'; -import { - IResponseCallbackData, - IWorkflowDb, - Push, - ResponseHelper, - WebhookHelpers, -} from './'; - -import { - ActiveWebhooks, -} from 'n8n-core'; +import { ActiveWebhooks } from 'n8n-core'; import { IWebhookData, @@ -20,28 +13,28 @@ import { WorkflowActivateMode, WorkflowExecuteMode, } from 'n8n-workflow'; +// eslint-disable-next-line import/no-cycle +import { IResponseCallbackData, IWorkflowDb, Push, ResponseHelper, WebhookHelpers } from '.'; const WEBHOOK_TEST_UNREGISTERED_HINT = `Click the 'Execute workflow' button on the canvas, then try again. (In test mode, the webhook only works for one call after you click this button)`; export class TestWebhooks { - private testWebhookData: { [key: string]: { sessionId?: string; - timeout: NodeJS.Timeout, + timeout: NodeJS.Timeout; workflowData: IWorkflowDb; workflow: Workflow; }; } = {}; - private activeWebhooks: ActiveWebhooks | null = null; + private activeWebhooks: ActiveWebhooks | null = null; constructor() { this.activeWebhooks = new ActiveWebhooks(); this.activeWebhooks.testWebhooks = true; } - /** * Executes a test-webhook and returns the data. It also makes sure that the * data gets additionally send to the UI. After the request got handled it @@ -54,7 +47,12 @@ export class TestWebhooks { * @returns {Promise} * @memberof TestWebhooks */ - async callTestWebhook(httpMethod: WebhookHttpMethod, path: string, request: express.Request, response: express.Response): Promise { + async callTestWebhook( + httpMethod: WebhookHttpMethod, + path: string, + request: express.Request, + response: express.Response, + ): Promise { // Reset request parameters request.params = {}; @@ -69,10 +67,16 @@ export class TestWebhooks { if (webhookData === undefined) { const pathElements = path.split('/'); const webhookId = pathElements.shift(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion webhookData = this.activeWebhooks!.get(httpMethod, pathElements.join('/'), webhookId); if (webhookData === undefined) { // The requested webhook is not registered - throw new ResponseHelper.ResponseError(`The requested webhook "${httpMethod} ${path}" is not registered.`, 404, 404, WEBHOOK_TEST_UNREGISTERED_HINT); + throw new ResponseHelper.ResponseError( + `The requested webhook "${httpMethod} ${path}" is not registered.`, + 404, + 404, + WEBHOOK_TEST_UNREGISTERED_HINT, + ); } path = webhookData.path; @@ -85,15 +89,24 @@ export class TestWebhooks { }); } - const webhookKey = this.activeWebhooks!.getWebhookKey(webhookData.httpMethod, webhookData.path, webhookData.webhookId) + `|${webhookData.workflowId}`; + const webhookKey = `${this.activeWebhooks!.getWebhookKey( + webhookData.httpMethod, + webhookData.path, + webhookData.webhookId, + )}|${webhookData.workflowId}`; // 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, WEBHOOK_TEST_UNREGISTERED_HINT); + throw new ResponseHelper.ResponseError( + `The requested webhook "${httpMethod} ${path}" is not registered.`, + 404, + 404, + WEBHOOK_TEST_UNREGISTERED_HINT, + ); } - const workflow = this.testWebhookData[webhookKey].workflow; + const { workflow } = this.testWebhookData[webhookKey]; // Get the node which has the webhook defined to know where to start from and to // get additional data @@ -102,15 +115,28 @@ export class TestWebhooks { throw new ResponseHelper.ResponseError('Could not find node to process webhook.', 404, 404); } + // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { try { const executionMode = 'manual'; - const executionId = await WebhookHelpers.executeWebhook(workflow, webhookData!, this.testWebhookData[webhookKey].workflowData, workflowStartNode, executionMode, this.testWebhookData[webhookKey].sessionId, undefined, undefined, request, response, (error: Error | null, data: IResponseCallbackData) => { - if (error !== null) { - return reject(error); - } - resolve(data); - }); + const executionId = await WebhookHelpers.executeWebhook( + workflow, + webhookData!, + this.testWebhookData[webhookKey].workflowData, + workflowStartNode, + executionMode, + this.testWebhookData[webhookKey].sessionId, + undefined, + undefined, + request, + response, + (error: Error | null, data: IResponseCallbackData) => { + if (error !== null) { + return reject(error); + } + resolve(data); + }, + ); if (executionId === undefined) { // The workflow did not run as the request was probably setup related @@ -122,9 +148,12 @@ export class TestWebhooks { // Inform editor-ui that webhook got received if (this.testWebhookData[webhookKey].sessionId !== undefined) { const pushInstance = Push.getInstance(); - pushInstance.send('testWebhookReceived', { workflowId: webhookData!.workflowId, executionId }, this.testWebhookData[webhookKey].sessionId!); + pushInstance.send( + 'testWebhookReceived', + { workflowId: webhookData!.workflowId, executionId }, + this.testWebhookData[webhookKey].sessionId, + ); } - } catch (error) { // Delete webhook also if an error is thrown } @@ -132,6 +161,7 @@ export class TestWebhooks { // Remove the webhook clearTimeout(this.testWebhookData[webhookKey].timeout); delete this.testWebhookData[webhookKey]; + // eslint-disable-next-line @typescript-eslint/no-floating-promises this.activeWebhooks!.removeWorkflow(workflow); }); } @@ -140,18 +170,22 @@ export class TestWebhooks { * Gets all request methods associated with a single test webhook * @param path webhook path */ - async getWebhookMethods(path : string) : Promise { + async getWebhookMethods(path: string): Promise { const webhookMethods: string[] = this.activeWebhooks!.getWebhookMethods(path); if (webhookMethods === undefined) { // The requested webhook is not registered - throw new ResponseHelper.ResponseError(`The requested webhook "${path}" is not registered.`, 404, 404, WEBHOOK_TEST_UNREGISTERED_HINT); + throw new ResponseHelper.ResponseError( + `The requested webhook "${path}" is not registered.`, + 404, + 404, + WEBHOOK_TEST_UNREGISTERED_HINT, + ); } return webhookMethods; } - /** * Checks if it has to wait for webhook data to execute the workflow. If yes it waits * for it and resolves with the result of the workflow if not it simply resolves @@ -162,9 +196,22 @@ export class TestWebhooks { * @returns {(Promise)} * @memberof TestWebhooks */ - async needsWebhookData(workflowData: IWorkflowDb, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode, sessionId?: string, destinationNode?: string): Promise { - const webhooks = WebhookHelpers.getWorkflowWebhooks(workflow, additionalData, destinationNode, true); - if (!webhooks.find(webhook => webhook.webhookDescription.restartWebhook !== true)) { + async needsWebhookData( + workflowData: IWorkflowDb, + workflow: Workflow, + additionalData: IWorkflowExecuteAdditionalData, + mode: WorkflowExecuteMode, + activation: WorkflowActivateMode, + sessionId?: string, + destinationNode?: string, + ): Promise { + const webhooks = WebhookHelpers.getWorkflowWebhooks( + workflow, + additionalData, + destinationNode, + true, + ); + if (!webhooks.find((webhook) => webhook.webhookDescription.restartWebhook !== true)) { // No webhooks found to start a workflow return false; } @@ -180,8 +227,13 @@ export class TestWebhooks { let key: string; const activatedKey: string[] = []; + // eslint-disable-next-line no-restricted-syntax for (const webhookData of webhooks) { - key = this.activeWebhooks!.getWebhookKey(webhookData.httpMethod, webhookData.path, webhookData.webhookId) + `|${workflowData.id}`; + key = `${this.activeWebhooks!.getWebhookKey( + webhookData.httpMethod, + webhookData.path, + webhookData.webhookId, + )}|${workflowData.id}`; activatedKey.push(key); @@ -193,17 +245,18 @@ export class TestWebhooks { }; try { + // eslint-disable-next-line no-await-in-loop await this.activeWebhooks!.add(workflow, webhookData, mode, activation); } catch (error) { - activatedKey.forEach(deleteKey => delete this.testWebhookData[deleteKey] ); + activatedKey.forEach((deleteKey) => delete this.testWebhookData[deleteKey]); + // eslint-disable-next-line no-await-in-loop await this.activeWebhooks!.removeWorkflow(workflow); throw error; } } return true; - } - + } /** * Removes a test webhook of the workflow with the given id @@ -214,10 +267,12 @@ export class TestWebhooks { */ cancelTestWebhook(workflowId: string): boolean { let foundWebhook = false; + // eslint-disable-next-line no-restricted-syntax for (const webhookKey of Object.keys(this.testWebhookData)) { const webhookData = this.testWebhookData[webhookKey]; if (webhookData.workflowData.id.toString() !== workflowId) { + // eslint-disable-next-line no-continue continue; } @@ -227,19 +282,24 @@ export class TestWebhooks { if (this.testWebhookData[webhookKey].sessionId !== undefined) { try { const pushInstance = Push.getInstance(); - pushInstance.send('testWebhookDeleted', { workflowId }, this.testWebhookData[webhookKey].sessionId!); + pushInstance.send( + 'testWebhookDeleted', + { workflowId }, + this.testWebhookData[webhookKey].sessionId, + ); } catch (error) { // Could not inform editor, probably is not connected anymore. So sipmly go on. } } - const workflow = this.testWebhookData[webhookKey].workflow; + const { workflow } = this.testWebhookData[webhookKey]; // Remove the webhook delete this.testWebhookData[webhookKey]; - if (foundWebhook === false) { + if (!foundWebhook) { // As it removes all webhooks of the workflow execute only once + // eslint-disable-next-line @typescript-eslint/no-floating-promises this.activeWebhooks!.removeWorkflow(workflow); } @@ -249,7 +309,6 @@ export class TestWebhooks { return foundWebhook; } - /** * Removes all the currently active test webhooks */ @@ -260,6 +319,7 @@ export class TestWebhooks { let workflow: Workflow; const workflows: Workflow[] = []; + // eslint-disable-next-line no-restricted-syntax for (const webhookKey of Object.keys(this.testWebhookData)) { workflow = this.testWebhookData[webhookKey].workflow; workflows.push(workflow); diff --git a/packages/cli/src/WaitTracker.ts b/packages/cli/src/WaitTracker.ts index 81ee39e418..791ff68e8e 100644 --- a/packages/cli/src/WaitTracker.ts +++ b/packages/cli/src/WaitTracker.ts @@ -1,3 +1,16 @@ +/* eslint-disable import/no-cycle */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-floating-promises */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { IRun, LoggerProxy as Logger, WorkflowOperationError } from 'n8n-workflow'; + +import { FindManyOptions, LessThanOrEqual, ObjectLiteral } from 'typeorm'; + +import { DateUtils } from 'typeorm/util/DateUtils'; import { ActiveExecutions, DatabaseType, @@ -7,38 +20,23 @@ import { IExecutionsStopData, IWorkflowExecutionDataProcess, ResponseHelper, + // eslint-disable-next-line @typescript-eslint/no-unused-vars WorkflowCredentials, WorkflowRunner, } from '.'; -import { - IRun, - LoggerProxy as Logger, - WorkflowOperationError, -} from 'n8n-workflow'; - -import { - FindManyOptions, - LessThanOrEqual, - ObjectLiteral, -} from 'typeorm'; - -import { DateUtils } from 'typeorm/util/DateUtils'; - - export class WaitTrackerClass { activeExecutionsInstance: ActiveExecutions.ActiveExecutions; private waitingExecutions: { [key: string]: { - executionId: string, - timer: NodeJS.Timeout, + executionId: string; + timer: NodeJS.Timeout; }; } = {}; mainTimer: NodeJS.Timeout; - constructor() { this.activeExecutionsInstance = ActiveExecutions.getInstance(); @@ -50,7 +48,7 @@ export class WaitTrackerClass { this.getwaitingExecutions(); } - + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async getwaitingExecutions() { Logger.debug('Wait tracker querying database for waiting executions'); // Find all the executions which should be triggered in the next 70 seconds @@ -63,11 +61,13 @@ export class WaitTrackerClass { waitTill: 'ASC', }, }; - const dbType = await GenericHelpers.getConfigValue('database.type') as DatabaseType; + const dbType = (await GenericHelpers.getConfigValue('database.type')) as DatabaseType; if (dbType === 'sqlite') { // This is needed because of issue in TypeORM <> SQLite: // https://github.com/typeorm/typeorm/issues/2286 - (findQuery.where! as ObjectLiteral).waitTill = LessThanOrEqual(DateUtils.mixedDateToUtcDatetimeString(new Date(Date.now() + 70000))); + (findQuery.where! as ObjectLiteral).waitTill = LessThanOrEqual( + DateUtils.mixedDateToUtcDatetimeString(new Date(Date.now() + 70000)), + ); } const executions = await Db.collections.Execution!.find(findQuery); @@ -76,10 +76,13 @@ export class WaitTrackerClass { return; } - const executionIds = executions.map(execution => execution.id.toString()).join(', '); - Logger.debug(`Wait tracker found ${executions.length} executions. Setting timer for IDs: ${executionIds}`); + const executionIds = executions.map((execution) => execution.id.toString()).join(', '); + Logger.debug( + `Wait tracker found ${executions.length} executions. Setting timer for IDs: ${executionIds}`, + ); // Add timers for each waiting execution that they get started at the correct time + // eslint-disable-next-line no-restricted-syntax for (const execution of executions) { const executionId = execution.id.toString(); if (this.waitingExecutions[executionId] === undefined) { @@ -94,7 +97,6 @@ export class WaitTrackerClass { } } - async stopExecution(executionId: string): Promise { if (this.waitingExecutions[executionId] !== undefined) { // The waiting execution was already sheduled to execute. @@ -124,7 +126,10 @@ export class WaitTrackerClass { fullExecutionData.stoppedAt = new Date(); fullExecutionData.waitTill = undefined; - await Db.collections.Execution!.update(executionId, ResponseHelper.flattenExecutionData(fullExecutionData)); + await Db.collections.Execution!.update( + executionId, + ResponseHelper.flattenExecutionData(fullExecutionData), + ); return { mode: fullExecutionData.mode, @@ -134,9 +139,8 @@ export class WaitTrackerClass { }; } - startExecution(executionId: string) { - Logger.debug(`Wait tracker resuming execution ${executionId}`, {executionId}); + Logger.debug(`Wait tracker resuming execution ${executionId}`, { executionId }); delete this.waitingExecutions[executionId]; (async () => { @@ -149,7 +153,7 @@ export class WaitTrackerClass { const fullExecutionData = ResponseHelper.unflattenExecutionData(fullExecutionDataFlatted); - if (fullExecutionData.finished === true) { + if (fullExecutionData.finished) { throw new Error('The execution did succeed and can so not be started again.'); } @@ -163,13 +167,14 @@ export class WaitTrackerClass { const workflowRunner = new WorkflowRunner(); await workflowRunner.run(data, false, false, executionId); })().catch((error) => { - Logger.error(`There was a problem starting the waiting execution with id "${executionId}": "${error.message}"`, { executionId }); + Logger.error( + `There was a problem starting the waiting execution with id "${executionId}": "${error.message}"`, + { executionId }, + ); }); - } } - let waitTrackerInstance: WaitTrackerClass | undefined; export function WaitTracker(): WaitTrackerClass { diff --git a/packages/cli/src/WaitingWebhooks.ts b/packages/cli/src/WaitingWebhooks.ts index f0b84d3804..dd54b37a96 100644 --- a/packages/cli/src/WaitingWebhooks.ts +++ b/packages/cli/src/WaitingWebhooks.ts @@ -1,3 +1,19 @@ +/* eslint-disable import/no-cycle */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable no-param-reassign */ +import { + INode, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + IRunExecutionData, + NodeHelpers, + WebhookHttpMethod, + Workflow, + LoggerProxy as Logger, +} from 'n8n-workflow'; + +import * as express from 'express'; + import { Db, IExecutionResponse, @@ -6,26 +22,18 @@ import { NodeTypes, ResponseHelper, WebhookHelpers, + // eslint-disable-next-line @typescript-eslint/no-unused-vars WorkflowCredentials, WorkflowExecuteAdditionalData, } from '.'; -import { - INode, - IRunExecutionData, - NodeHelpers, - WebhookHttpMethod, - Workflow, -} from 'n8n-workflow'; - -import * as express from 'express'; -import { - LoggerProxy as Logger, -} from 'n8n-workflow'; - export class WaitingWebhooks { - - async executeWebhook(httpMethod: WebhookHttpMethod, fullPath: string, req: express.Request, res: express.Response): Promise { + async executeWebhook( + httpMethod: WebhookHttpMethod, + fullPath: string, + req: express.Request, + res: express.Response, + ): Promise { Logger.debug(`Received waiting-webhoook "${httpMethod}" for path "${fullPath}"`); // Reset request parameters @@ -44,47 +52,77 @@ export class WaitingWebhooks { const execution = await Db.collections.Execution?.findOne(executionId); if (execution === undefined) { - throw new ResponseHelper.ResponseError(`The execution "${executionId} does not exist.`, 404, 404); + throw new ResponseHelper.ResponseError( + `The execution "${executionId} does not exist.`, + 404, + 404, + ); } const fullExecutionData = ResponseHelper.unflattenExecutionData(execution); - if (fullExecutionData.finished === true || fullExecutionData.data.resultData.error) { - throw new ResponseHelper.ResponseError(`The execution "${executionId} has finished already.`, 409, 409); + if (fullExecutionData.finished || fullExecutionData.data.resultData.error) { + throw new ResponseHelper.ResponseError( + `The execution "${executionId} has finished already.`, + 409, + 409, + ); } return this.startExecution(httpMethod, path, fullExecutionData, req, res); } - - async startExecution(httpMethod: WebhookHttpMethod, path: string, fullExecutionData: IExecutionResponse, req: express.Request, res: express.Response): Promise { + async startExecution( + httpMethod: WebhookHttpMethod, + path: string, + fullExecutionData: IExecutionResponse, + req: express.Request, + res: express.Response, + ): Promise { const executionId = fullExecutionData.id; - if (fullExecutionData.finished === true) { + if (fullExecutionData.finished) { throw new Error('The execution did succeed and can so not be started again.'); } - const lastNodeExecuted = fullExecutionData!.data.resultData.lastNodeExecuted as string; + const lastNodeExecuted = fullExecutionData.data.resultData.lastNodeExecuted as string; // Set the node as disabled so that the data does not get executed again as it would result // in starting the wait all over again - fullExecutionData!.data.executionData!.nodeExecutionStack[0].node.disabled = true; + fullExecutionData.data.executionData!.nodeExecutionStack[0].node.disabled = true; // Remove waitTill information else the execution would stop - fullExecutionData!.data.waitTill = undefined; + fullExecutionData.data.waitTill = undefined; // Remove the data of the node execution again else it will display the node as executed twice - fullExecutionData!.data.resultData.runData[lastNodeExecuted].pop(); + fullExecutionData.data.resultData.runData[lastNodeExecuted].pop(); - const workflowData = fullExecutionData.workflowData; + const { workflowData } = fullExecutionData; const nodeTypes = NodeTypes(); - 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 = 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 additionalData = await WorkflowExecuteAdditionalData.getBase(); - const webhookData = NodeHelpers.getNodeWebhooks(workflow, workflow.getNode(lastNodeExecuted) as INode, additionalData).filter((webhook) => { - return (webhook.httpMethod === httpMethod && webhook.path === path && webhook.webhookDescription.restartWebhook === true); + const webhookData = NodeHelpers.getNodeWebhooks( + workflow, + workflow.getNode(lastNodeExecuted) as INode, + additionalData, + ).filter((webhook) => { + return ( + webhook.httpMethod === httpMethod && + webhook.path === path && + webhook.webhookDescription.restartWebhook === true + ); })[0]; if (webhookData === undefined) { @@ -100,18 +138,30 @@ export class WaitingWebhooks { throw new ResponseHelper.ResponseError('Could not find node to process webhook.', 404, 404); } - const runExecutionData = fullExecutionData.data as IRunExecutionData; + const runExecutionData = fullExecutionData.data; return new Promise((resolve, reject) => { const executionMode = 'webhook'; - WebhookHelpers.executeWebhook(workflow, webhookData, workflowData as IWorkflowDb, workflowStartNode, executionMode, undefined, runExecutionData, fullExecutionData.id, req, res, (error: Error | null, data: object) => { - if (error !== null) { - return reject(error); - } - resolve(data); - }); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + WebhookHelpers.executeWebhook( + workflow, + webhookData, + workflowData as IWorkflowDb, + workflowStartNode, + executionMode, + undefined, + runExecutionData, + fullExecutionData.id, + req, + res, + // eslint-disable-next-line consistent-return + (error: Error | null, data: object) => { + if (error !== null) { + return reject(error); + } + resolve(data); + }, + ); }); - } - } diff --git a/packages/cli/src/WebhookHelpers.ts b/packages/cli/src/WebhookHelpers.ts index 6005f7740d..79f875bdea 100644 --- a/packages/cli/src/WebhookHelpers.ts +++ b/packages/cli/src/WebhookHelpers.ts @@ -1,24 +1,21 @@ +/* eslint-disable no-param-reassign */ +/* eslint-disable @typescript-eslint/prefer-optional-chain */ +/* eslint-disable @typescript-eslint/no-shadow */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable id-denylist */ +/* eslint-disable prefer-spread */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable no-restricted-syntax */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable prefer-destructuring */ import * as express from 'express'; +// eslint-disable-next-line import/no-extraneous-dependencies import { get } from 'lodash'; -import { - ActiveExecutions, - GenericHelpers, - IExecutionDb, - IResponseCallbackData, - IWorkflowDb, - IWorkflowExecutionDataProcess, - ResponseHelper, - WorkflowCredentials, - WorkflowExecuteAdditionalData, - WorkflowHelpers, - WorkflowRunner, -} from './'; - -import { - BINARY_ENCODING, - NodeExecuteFunctions, -} from 'n8n-core'; +import { BINARY_ENCODING, NodeExecuteFunctions } from 'n8n-core'; import { IBinaryKeyData, @@ -35,7 +32,21 @@ import { Workflow, WorkflowExecuteMode, } from 'n8n-workflow'; - +// eslint-disable-next-line import/no-cycle +import { + ActiveExecutions, + GenericHelpers, + IExecutionDb, + IResponseCallbackData, + IWorkflowDb, + IWorkflowExecutionDataProcess, + ResponseHelper, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + WorkflowCredentials, + WorkflowExecuteAdditionalData, + WorkflowHelpers, + WorkflowRunner, +} from '.'; const activeExecutions = ActiveExecutions.getInstance(); @@ -47,7 +58,12 @@ const activeExecutions = ActiveExecutions.getInstance(); * @param {Workflow} workflow * @returns {IWebhookData[]} */ -export function getWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, destinationNode?: string, ignoreRestartWehbooks = false): IWebhookData[] { +export function getWorkflowWebhooks( + workflow: Workflow, + additionalData: IWorkflowExecuteAdditionalData, + destinationNode?: string, + ignoreRestartWehbooks = false, +): IWebhookData[] { // Check all the nodes in the workflow if they have webhooks const returnData: IWebhookData[] = []; @@ -63,9 +79,13 @@ export function getWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflo if (parentNodes !== undefined && !parentNodes.includes(node.name)) { // If parentNodes are given check only them if they have webhooks // and no other ones + // eslint-disable-next-line no-continue continue; } - returnData.push.apply(returnData, NodeHelpers.getNodeWebhooks(workflow, node, additionalData, ignoreRestartWehbooks)); + returnData.push.apply( + returnData, + NodeHelpers.getNodeWebhooks(workflow, node, additionalData, ignoreRestartWehbooks), + ); } return returnData; @@ -91,22 +111,33 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] { return returnData; } - - /** - * Executes a webhook - * - * @export - * @param {IWebhookData} webhookData - * @param {IWorkflowDb} workflowData - * @param {INode} workflowStartNode - * @param {WorkflowExecuteMode} executionMode - * @param {(string | undefined)} sessionId - * @param {express.Request} req - * @param {express.Response} res - * @param {((error: Error | null, data: IResponseCallbackData) => void)} responseCallback - * @returns {(Promise)} - */ -export async function executeWebhook(workflow: Workflow, webhookData: IWebhookData, workflowData: IWorkflowDb, workflowStartNode: INode, executionMode: WorkflowExecuteMode, sessionId: string | undefined, runExecutionData: IRunExecutionData | undefined, executionId: string | undefined, req: express.Request, res: express.Response, responseCallback: (error: Error | null, data: IResponseCallbackData) => void): Promise { +/** + * Executes a webhook + * + * @export + * @param {IWebhookData} webhookData + * @param {IWorkflowDb} workflowData + * @param {INode} workflowStartNode + * @param {WorkflowExecuteMode} executionMode + * @param {(string | undefined)} sessionId + * @param {express.Request} req + * @param {express.Response} res + * @param {((error: Error | null, data: IResponseCallbackData) => void)} responseCallback + * @returns {(Promise)} + */ +export async function executeWebhook( + workflow: Workflow, + webhookData: IWebhookData, + workflowData: IWorkflowDb, + workflowStartNode: INode, + executionMode: WorkflowExecuteMode, + sessionId: string | undefined, + runExecutionData: IRunExecutionData | undefined, + executionId: string | undefined, + req: express.Request, + res: express.Response, + responseCallback: (error: Error | null, data: IResponseCallbackData) => void, +): Promise { // Get the nodeType to know which responseMode is set const nodeType = workflow.nodeTypes.getByName(workflowStartNode.type); if (nodeType === undefined) { @@ -120,8 +151,20 @@ export async function executeWebhook(workflow: Workflow, webhookData: IWebhookDa }; // Get the responseMode - const responseMode = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseMode'], executionMode, additionalKeys, 'onReceived'); - const responseCode = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseCode'], executionMode, additionalKeys, 200) as number; + const responseMode = workflow.expression.getSimpleParameterValue( + workflowStartNode, + webhookData.webhookDescription.responseMode, + executionMode, + additionalKeys, + 'onReceived', + ); + const responseCode = workflow.expression.getSimpleParameterValue( + workflowStartNode, + webhookData.webhookDescription.responseCode, + executionMode, + additionalKeys, + 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 @@ -147,7 +190,13 @@ export async function executeWebhook(workflow: Workflow, webhookData: IWebhookDa let webhookResultData: IWebhookResponseData; try { - webhookResultData = await workflow.runWebhook(webhookData, workflowStartNode, additionalData, NodeExecuteFunctions, executionMode); + webhookResultData = await workflow.runWebhook( + webhookData, + workflowStartNode, + additionalData, + NodeExecuteFunctions, + executionMode, + ); } catch (err) { // Send error response to webhook caller const errorMessage = 'Workflow Webhook Error: Workflow could not be started!'; @@ -171,7 +220,7 @@ export async function executeWebhook(workflow: Workflow, webhookData: IWebhookDa noWebhookResponse: true, // Add empty data that it at least tries to "execute" the webhook // which then so gets the chance to throw the error. - workflowData: [[{json: {}}]], + workflowData: [[{ json: {} }]], }; } @@ -182,22 +231,30 @@ export async function executeWebhook(workflow: Workflow, webhookData: IWebhookDa $executionId: executionId, }; - if (webhookData.webhookDescription['responseHeaders'] !== undefined) { - const responseHeaders = workflow.expression.getComplexParameterValue(workflowStartNode, webhookData.webhookDescription['responseHeaders'], executionMode, additionalKeys, undefined) as { - entries?: Array<{ - name: string; - value: string; - }> | undefined; + if (webhookData.webhookDescription.responseHeaders !== undefined) { + const responseHeaders = workflow.expression.getComplexParameterValue( + workflowStartNode, + webhookData.webhookDescription.responseHeaders, + executionMode, + additionalKeys, + undefined, + ) as { + entries?: + | Array<{ + name: string; + value: string; + }> + | undefined; }; - if (responseHeaders !== undefined && responseHeaders['entries'] !== undefined) { - for (const item of responseHeaders['entries']) { - res.setHeader(item['name'], item['value']); + if (responseHeaders !== undefined && responseHeaders.entries !== undefined) { + for (const item of responseHeaders.entries) { + res.setHeader(item.name, item.value); } } } - if (webhookResultData.noWebhookResponse === true && didSendResponse === false) { + if (webhookResultData.noWebhookResponse === true && !didSendResponse) { // The response got already send responseCallback(null, { noWebhookResponse: true, @@ -209,7 +266,7 @@ export async function executeWebhook(workflow: Workflow, webhookData: IWebhookDa // Workflow should not run if (webhookResultData.webhookResponse !== undefined) { // Data to respond with is given - if (didSendResponse === false) { + if (!didSendResponse) { responseCallback(null, { data: webhookResultData.webhookResponse, responseCode, @@ -218,7 +275,8 @@ export async function executeWebhook(workflow: Workflow, webhookData: IWebhookDa } } else { // Send default response - if (didSendResponse === false) { + // eslint-disable-next-line no-lonely-if + if (!didSendResponse) { responseCallback(null, { data: { message: 'Webhook call got received.', @@ -233,7 +291,7 @@ export async function executeWebhook(workflow: Workflow, webhookData: IWebhookDa // Now that we know that the workflow should run we can return the default response // directly if responseMode it set to "onReceived" and a respone should be sent - if (responseMode === 'onReceived' && didSendResponse === false) { + if (responseMode === 'onReceived' && !didSendResponse) { // Return response directly and do not wait for the workflow to finish if (webhookResultData.webhookResponse !== undefined) { // Data to respond with is given @@ -255,32 +313,32 @@ export async function executeWebhook(workflow: Workflow, webhookData: IWebhookDa // Initialize the data of the webhook node const nodeExecutionStack: IExecuteData[] = []; - nodeExecutionStack.push( - { - node: workflowStartNode, - data: { - main: webhookResultData.workflowData, - }, - } - ); + nodeExecutionStack.push({ + node: workflowStartNode, + data: { + main: webhookResultData.workflowData, + }, + }); - runExecutionData = runExecutionData || { - startData: { - }, - resultData: { - runData: {}, - }, - executionData: { - contextData: {}, - nodeExecutionStack, - waitingExecution: {}, - }, - } as IRunExecutionData; + runExecutionData = + runExecutionData || + ({ + startData: {}, + resultData: { + runData: {}, + }, + executionData: { + contextData: {}, + nodeExecutionStack, + waitingExecution: {}, + }, + } as IRunExecutionData); if (executionId !== undefined) { // Set the data the webhook node did return on the waiting node if executionId // already exists as it means that we are restarting an existing execution. - runExecutionData.executionData!.nodeExecutionStack[0].data.main = webhookResultData.workflowData; + runExecutionData.executionData!.nodeExecutionStack[0].data.main = + webhookResultData.workflowData; } if (Object.keys(runExecutionDataMerge).length !== 0) { @@ -299,163 +357,203 @@ export async function executeWebhook(workflow: Workflow, webhookData: IWebhookDa const workflowRunner = new WorkflowRunner(); executionId = await workflowRunner.run(runData, true, !didSendResponse, executionId); - Logger.verbose(`Started execution of workflow "${workflow.name}" from webhook with execution ID ${executionId}`, { executionId }); + Logger.verbose( + `Started execution of workflow "${workflow.name}" from webhook with execution ID ${executionId}`, + { executionId }, + ); // Get a promise which resolves when the workflow did execute and send then response - const executePromise = activeExecutions.getPostExecutePromise(executionId) as Promise; - executePromise.then((data) => { - if (data === undefined) { - if (didSendResponse === false) { - responseCallback(null, { - data: { - message: 'Workflow did execute sucessfully but no data got returned.', - }, - responseCode, - }); - didSendResponse = true; - } - return undefined; - } - - const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data); - if(data.data.resultData.error || returnData?.error !== undefined) { - if (didSendResponse === false) { - responseCallback(null, { - data: { - message: 'Workflow did error.', - }, - responseCode: 500, - }); - } - didSendResponse = true; - return data; - } - - if (returnData === undefined) { - if (didSendResponse === false) { - responseCallback(null, { - data: { - message: 'Workflow did execute sucessfully but the last node did not return any data.', - }, - responseCode, - }); - } - didSendResponse = true; - return data; - } - - const additionalKeys: IWorkflowDataProxyAdditionalKeys = { - $executionId: executionId, - }; - - const responseData = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseData'], executionMode, additionalKeys, 'firstEntryJson'); - - if (didSendResponse === false) { - let data: IDataObject | IDataObject[]; - - if (responseData === 'firstEntryJson') { - // Return the JSON data of the first entry - - if (returnData.data!.main[0]![0] === undefined) { - responseCallback(new Error('No item to return got found.'), {}); + const executePromise = activeExecutions.getPostExecutePromise(executionId) as Promise< + IExecutionDb | undefined + >; + executePromise + .then((data) => { + if (data === undefined) { + if (!didSendResponse) { + responseCallback(null, { + data: { + message: 'Workflow did execute sucessfully but no data got returned.', + }, + responseCode, + }); didSendResponse = true; } + return undefined; + } - data = returnData.data!.main[0]![0].json; - - const responsePropertyName = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responsePropertyName'], executionMode, additionalKeys, undefined); - - if (responsePropertyName !== undefined) { - data = get(data, responsePropertyName as string) as IDataObject; + const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data); + if (data.data.resultData.error || returnData?.error !== undefined) { + if (!didSendResponse) { + responseCallback(null, { + data: { + message: 'Workflow did error.', + }, + responseCode: 500, + }); } + didSendResponse = true; + return data; + } - const responseContentType = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseContentType'], executionMode, additionalKeys, undefined); + if (returnData === undefined) { + if (!didSendResponse) { + responseCallback(null, { + data: { + message: + 'Workflow did execute sucessfully but the last node did not return any data.', + }, + responseCode, + }); + } + didSendResponse = true; + return data; + } - if (responseContentType !== undefined) { - // Send the webhook response manually to be able to set the content-type - res.setHeader('Content-Type', responseContentType as string); + const additionalKeys: IWorkflowDataProxyAdditionalKeys = { + $executionId: executionId, + }; - // Returning an object, boolean, number, ... causes problems so make sure to stringify if needed - if (data !== null && data !== undefined && ['Buffer', 'String'].includes(data.constructor.name)) { - res.end(data); - } else { - res.end(JSON.stringify(data)); + const responseData = workflow.expression.getSimpleParameterValue( + workflowStartNode, + webhookData.webhookDescription.responseData, + executionMode, + additionalKeys, + 'firstEntryJson', + ); + + if (!didSendResponse) { + let data: IDataObject | IDataObject[]; + + if (responseData === 'firstEntryJson') { + // Return the JSON data of the first entry + + if (returnData.data!.main[0]![0] === undefined) { + responseCallback(new Error('No item to return got found.'), {}); + didSendResponse = true; } + data = returnData.data!.main[0]![0].json; + + const responsePropertyName = workflow.expression.getSimpleParameterValue( + workflowStartNode, + webhookData.webhookDescription.responsePropertyName, + executionMode, + additionalKeys, + undefined, + ); + + if (responsePropertyName !== undefined) { + data = get(data, responsePropertyName as string) as IDataObject; + } + + const responseContentType = workflow.expression.getSimpleParameterValue( + workflowStartNode, + webhookData.webhookDescription.responseContentType, + executionMode, + additionalKeys, + undefined, + ); + + if (responseContentType !== undefined) { + // Send the webhook response manually to be able to set the content-type + res.setHeader('Content-Type', responseContentType as string); + + // Returning an object, boolean, number, ... causes problems so make sure to stringify if needed + if ( + data !== null && + data !== undefined && + ['Buffer', 'String'].includes(data.constructor.name) + ) { + res.end(data); + } else { + res.end(JSON.stringify(data)); + } + + responseCallback(null, { + noWebhookResponse: true, + }); + didSendResponse = true; + } + } else if (responseData === 'firstEntryBinary') { + // Return the binary data of the first entry + data = returnData.data!.main[0]![0]; + + if (data === undefined) { + responseCallback(new Error('No item to return got found.'), {}); + didSendResponse = true; + } + + if (data.binary === undefined) { + responseCallback(new Error('No binary data to return got found.'), {}); + didSendResponse = true; + } + + const responseBinaryPropertyName = workflow.expression.getSimpleParameterValue( + workflowStartNode, + webhookData.webhookDescription.responseBinaryPropertyName, + executionMode, + additionalKeys, + 'data', + ); + + if (responseBinaryPropertyName === undefined && !didSendResponse) { + responseCallback(new Error('No "responseBinaryPropertyName" is set.'), {}); + didSendResponse = true; + } + + const binaryData = (data.binary as IBinaryKeyData)[ + responseBinaryPropertyName as string + ]; + if (binaryData === undefined && !didSendResponse) { + responseCallback( + new Error( + `The binary property "${responseBinaryPropertyName}" which should be returned does not exist.`, + ), + {}, + ); + didSendResponse = true; + } + + if (!didSendResponse) { + // Send the webhook response manually + res.setHeader('Content-Type', binaryData.mimeType); + res.end(Buffer.from(binaryData.data, BINARY_ENCODING)); + + responseCallback(null, { + noWebhookResponse: true, + }); + } + } else { + // Return the JSON data of all the entries + data = []; + for (const entry of returnData.data!.main[0]!) { + data.push(entry.json); + } + } + + if (!didSendResponse) { responseCallback(null, { - noWebhookResponse: true, - }); - didSendResponse = true; - } - - } else if (responseData === 'firstEntryBinary') { - // Return the binary data of the first entry - data = returnData.data!.main[0]![0]; - - if (data === undefined) { - responseCallback(new Error('No item to return got found.'), {}); - didSendResponse = true; - } - - if (data.binary === undefined) { - responseCallback(new Error('No binary data to return got found.'), {}); - didSendResponse = true; - } - - const responseBinaryPropertyName = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseBinaryPropertyName'], executionMode, additionalKeys, 'data'); - - if (responseBinaryPropertyName === undefined && didSendResponse === false) { - responseCallback(new Error('No "responseBinaryPropertyName" is set.'), {}); - didSendResponse = true; - } - - const binaryData = (data.binary as IBinaryKeyData)[responseBinaryPropertyName as string]; - if (binaryData === undefined && didSendResponse === false) { - responseCallback(new Error(`The binary property "${responseBinaryPropertyName}" which should be returned does not exist.`), {}); - didSendResponse = true; - } - - if (didSendResponse === false) { - // Send the webhook response manually - res.setHeader('Content-Type', binaryData.mimeType); - res.end(Buffer.from(binaryData.data, BINARY_ENCODING)); - - responseCallback(null, { - noWebhookResponse: true, + data, + responseCode, }); } + } + didSendResponse = true; - } else { - // Return the JSON data of all the entries - data = []; - for (const entry of returnData.data!.main[0]!) { - data.push(entry.json); - } + return data; + }) + .catch((e) => { + if (!didSendResponse) { + responseCallback(new Error('There was a problem executing the workflow.'), {}); } - if (didSendResponse === false) { - responseCallback(null, { - data, - responseCode, - }); - } - } - didSendResponse = true; - - return data; - }) - .catch((e) => { - if (didSendResponse === false) { - responseCallback(new Error('There was a problem executing the workflow.'), {}); - } - - throw new ResponseHelper.ResponseError(e.message, 500, 500); - }); + throw new ResponseHelper.ResponseError(e.message, 500, 500); + }); + // eslint-disable-next-line consistent-return return executionId; - } catch (e) { - if (didSendResponse === false) { + if (!didSendResponse) { responseCallback(new Error('There was a problem executing the workflow.'), {}); } @@ -463,7 +561,6 @@ export async function executeWebhook(workflow: Workflow, webhookData: IWebhookDa } } - /** * Returns the base URL of the webhooks * diff --git a/packages/cli/src/WebhookServer.ts b/packages/cli/src/WebhookServer.ts index 83c28ab2d0..95ad977a0e 100644 --- a/packages/cli/src/WebhookServer.ts +++ b/packages/cli/src/WebhookServer.ts @@ -1,14 +1,22 @@ +/* eslint-disable no-console */ +/* eslint-disable consistent-return */ +/* eslint-disable @typescript-eslint/restrict-plus-operands */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import * as express from 'express'; -import { - readFileSync, -} from 'fs'; -import { - getConnectionManager, -} from 'typeorm'; +import { readFileSync } from 'fs'; +import { getConnectionManager } from 'typeorm'; import * as bodyParser from 'body-parser'; -require('body-parser-xml')(bodyParser); +// eslint-disable-next-line import/no-extraneous-dependencies, @typescript-eslint/no-unused-vars import * as _ from 'lodash'; +import * as compression from 'compression'; +// eslint-disable-next-line import/no-extraneous-dependencies +import * as parseUrl from 'parseurl'; +// eslint-disable-next-line import/no-cycle import { ActiveExecutions, ActiveWorkflowRunner, @@ -19,120 +27,157 @@ import { IExternalHooksClass, IPackageVersions, ResponseHelper, -} from './'; +} from '.'; -import * as compression from 'compression'; import * as config from '../config'; -import * as parseUrl from 'parseurl'; +// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-call +require('body-parser-xml')(bodyParser); + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export function registerProductionWebhooks() { - // ---------------------------------------- // Regular Webhooks // ---------------------------------------- // HEAD webhook requests - this.app.head(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => { - // Cut away the "/webhook/" to get the registred part of the url - const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhook.length + 2); + this.app.head( + `/${this.endpointWebhook}/*`, + async (req: express.Request, res: express.Response) => { + // Cut away the "/webhook/" to get the registred part of the url + const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice( + this.endpointWebhook.length + 2, + ); - let response; - try { - response = await this.activeWorkflowRunner.executeWebhook('HEAD', requestUrl, req, res); - } catch (error) { - ResponseHelper.sendErrorResponse(res, error); - return; - } + let response; + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + response = await this.activeWorkflowRunner.executeWebhook('HEAD', requestUrl, req, res); + } catch (error) { + ResponseHelper.sendErrorResponse(res, error); + return; + } - if (response.noWebhookResponse === true) { - // Nothing else to do as the response got already sent - return; - } + if (response.noWebhookResponse === true) { + // Nothing else to do as the response got already sent + return; + } - ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); - }); + ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); + }, + ); // OPTIONS webhook requests - this.app.options(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => { - // Cut away the "/webhook/" to get the registred part of the url - const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhook.length + 2); + this.app.options( + `/${this.endpointWebhook}/*`, + async (req: express.Request, res: express.Response) => { + // Cut away the "/webhook/" to get the registred part of the url + const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice( + this.endpointWebhook.length + 2, + ); - let allowedMethods: string[]; - try { - allowedMethods = await this.activeWorkflowRunner.getWebhookMethods(requestUrl); - allowedMethods.push('OPTIONS'); + let allowedMethods: string[]; + try { + allowedMethods = await this.activeWorkflowRunner.getWebhookMethods(requestUrl); + allowedMethods.push('OPTIONS'); - // Add custom "Allow" header to satisfy OPTIONS response. - res.append('Allow', allowedMethods); - } catch (error) { - ResponseHelper.sendErrorResponse(res, error); - return; - } + // Add custom "Allow" header to satisfy OPTIONS response. + res.append('Allow', allowedMethods); + } catch (error) { + ResponseHelper.sendErrorResponse(res, error); + return; + } - ResponseHelper.sendSuccessResponse(res, {}, true, 204); - }); + ResponseHelper.sendSuccessResponse(res, {}, true, 204); + }, + ); // GET webhook requests - this.app.get(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => { - // Cut away the "/webhook/" to get the registred part of the url - const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhook.length + 2); + this.app.get( + `/${this.endpointWebhook}/*`, + async (req: express.Request, res: express.Response) => { + // Cut away the "/webhook/" to get the registred part of the url + const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice( + this.endpointWebhook.length + 2, + ); - let response; - try { - response = await this.activeWorkflowRunner.executeWebhook('GET', requestUrl, req, res); - } catch (error) { - ResponseHelper.sendErrorResponse(res, error); - return; - } + let response; + try { + response = await this.activeWorkflowRunner.executeWebhook('GET', requestUrl, req, res); + } catch (error) { + ResponseHelper.sendErrorResponse(res, error); + return; + } - if (response.noWebhookResponse === true) { - // Nothing else to do as the response got already sent - return; - } + if (response.noWebhookResponse === true) { + // Nothing else to do as the response got already sent + return; + } - ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); - }); + ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); + }, + ); // POST webhook requests - this.app.post(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => { - // Cut away the "/webhook/" to get the registred part of the url - const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhook.length + 2); + this.app.post( + `/${this.endpointWebhook}/*`, + async (req: express.Request, res: express.Response) => { + // Cut away the "/webhook/" to get the registred part of the url + const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice( + this.endpointWebhook.length + 2, + ); - let response; - try { - response = await this.activeWorkflowRunner.executeWebhook('POST', requestUrl, req, res); - } catch (error) { - ResponseHelper.sendErrorResponse(res, error); - return; - } + let response; + try { + response = await this.activeWorkflowRunner.executeWebhook('POST', requestUrl, req, res); + } catch (error) { + ResponseHelper.sendErrorResponse(res, error); + return; + } - if (response.noWebhookResponse === true) { - // Nothing else to do as the response got already sent - return; - } + if (response.noWebhookResponse === true) { + // Nothing else to do as the response got already sent + return; + } - ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); - }); + ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); + }, + ); } class App { - app: express.Application; + activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner; + endpointWebhook: string; + endpointPresetCredentials: string; + externalHooks: IExternalHooksClass; + saveDataErrorExecution: string; + saveDataSuccessExecution: string; + saveManualExecutions: boolean; + executionTimeout: number; + maxExecutionTimeout: number; + timezone: string; + activeExecutionsInstance: ActiveExecutions.ActiveExecutions; + versions: IPackageVersions | undefined; + restEndpoint: string; + protocol: string; + sslKey: string; + sslCert: string; presetCredentialsLoaded: boolean; @@ -163,7 +208,6 @@ class App { this.endpointPresetCredentials = config.get('credentials.overwrite.endpoint') as string; } - /** * Returns the current epoch time * @@ -174,9 +218,7 @@ class App { return new Date(); } - async config(): Promise { - this.versions = await GenericHelpers.getVersions(); // Compress the response data @@ -191,49 +233,63 @@ class App { }); // Support application/json type post data - this.app.use(bodyParser.json({ - limit: '16mb', verify: (req, res, buf) => { - // @ts-ignore - req.rawBody = buf; - }, - })); + this.app.use( + bodyParser.json({ + limit: '16mb', + verify: (req, res, buf) => { + // @ts-ignore + req.rawBody = buf; + }, + }), + ); // Support application/xml type post data - // @ts-ignore - this.app.use(bodyParser.xml({ - limit: '16mb', xmlParseOptions: { - 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( + // @ts-ignore + bodyParser.xml({ + limit: '16mb', + xmlParseOptions: { + 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; - }, - })); + this.app.use( + bodyParser.text({ + limit: '16mb', + verify: (req, res, buf) => { + // @ts-ignore + req.rawBody = buf; + }, + }), + ); - //support application/x-www-form-urlencoded post data - this.app.use(bodyParser.urlencoded({ extended: false, - verify: (req, res, buf) => { - // @ts-ignore - req.rawBody = buf; - }, - })); + // support application/x-www-form-urlencoded post data + this.app.use( + bodyParser.urlencoded({ + extended: false, + verify: (req, res, buf) => { + // @ts-ignore + req.rawBody = buf; + }, + }), + ); - if (process.env['NODE_ENV'] !== 'production') { + if (process.env.NODE_ENV !== 'production') { this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { // Allow access also from frontend when developing res.header('Access-Control-Allow-Origin', 'http://localhost:8080'); res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); - res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, sessionid'); + res.header( + 'Access-Control-Allow-Headers', + 'Origin, X-Requested-With, Content-Type, Accept, sessionid', + ); next(); }); } - this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { if (Db.collections.Workflow === null) { const error = new ResponseHelper.ResponseError('Database is not ready!', undefined, 503); @@ -243,25 +299,22 @@ class App { next(); }); - - // ---------------------------------------- // Healthcheck // ---------------------------------------- - // Does very basic health check this.app.get('/healthz', async (req: express.Request, res: express.Response) => { - const connection = getConnectionManager().get(); try { - if (connection.isConnected === false) { + if (!connection.isConnected) { // Connection is not active throw new Error('No active database connection!'); } // DB ping await connection.query('SELECT 1'); + // eslint-disable-next-line id-denylist } catch (err) { const error = new ResponseHelper.ResponseError('No Database connection!', undefined, 503); return ResponseHelper.sendErrorResponse(res, error); @@ -276,9 +329,7 @@ class App { }); registerProductionWebhooks.apply(this); - } - } export async function start(): Promise { @@ -292,12 +343,14 @@ export async function start(): Promise { let server; if (app.protocol === 'https' && app.sslKey && app.sslCert) { + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires const https = require('https'); const privateKey = readFileSync(app.sslKey, 'utf8'); const cert = readFileSync(app.sslCert, 'utf8'); const credentials = { key: privateKey, cert }; server = https.createServer(credentials, app.app); } else { + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires const http = require('http'); server = http.createServer(app.app); } diff --git a/packages/cli/src/WorkflowCredentials.ts b/packages/cli/src/WorkflowCredentials.ts index bd02acabf7..622f1d9b0e 100644 --- a/packages/cli/src/WorkflowCredentials.ts +++ b/packages/cli/src/WorkflowCredentials.ts @@ -1,22 +1,25 @@ -import { - Db, -} from './'; -import { - INode, - IWorkflowCredentials -} from 'n8n-workflow'; - +/* eslint-disable no-prototype-builtins */ +import { INode, IWorkflowCredentials } from 'n8n-workflow'; +// eslint-disable-next-line import/no-cycle +import { Db } from '.'; +// eslint-disable-next-line @typescript-eslint/naming-convention export async function WorkflowCredentials(nodes: INode[]): Promise { // Go through all nodes to find which credentials are needed to execute the workflow const returnCredentials: IWorkflowCredentials = {}; - let node, type, name, foundCredentials; + let node; + let type; + let name; + let foundCredentials; + // eslint-disable-next-line no-restricted-syntax for (node of nodes) { if (node.disabled === true || !node.credentials) { + // eslint-disable-next-line no-continue continue; } + // eslint-disable-next-line no-restricted-syntax for (type of Object.keys(node.credentials)) { if (!returnCredentials.hasOwnProperty(type)) { returnCredentials[type] = {}; @@ -24,14 +27,15 @@ export async function WorkflowCredentials(nodes: INode[]): Promise node.type === ERROR_TRIGGER_TYPE)) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + WorkflowHelpers.executeErrorWorkflow( + workflowData.settings.errorWorkflow as string, + workflowErrorData, + ); + } else if ( + mode !== 'error' && + workflowData.id !== undefined && + workflowData.nodes.some((node) => node.type === ERROR_TRIGGER_TYPE) + ) { Logger.verbose(`Start internal error workflow`, { executionId, workflowId: workflowData.id }); // If the workflow contains + // eslint-disable-next-line @typescript-eslint/no-floating-promises WorkflowHelpers.executeErrorWorkflow(workflowData.id.toString(), workflowErrorData); } } @@ -114,23 +154,34 @@ function pruneExecutionData(this: WorkflowHooks): void { date.setHours(date.getHours() - maxAge); // date reformatting needed - see https://github.com/typeorm/typeorm/issues/2286 + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const utcDate = DateUtils.mixedDateToUtcDatetimeString(date); // throttle just on success to allow for self healing on failure - Db.collections.Execution!.delete({ stoppedAt: LessThanOrEqual(utcDate) }) - .then(data => + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + Db.collections + .Execution!.delete({ stoppedAt: LessThanOrEqual(utcDate) }) + .then((data) => setTimeout(() => { throttling = false; - }, timeout * 1000) - ).catch(error => { + }, timeout * 1000), + ) + .catch((error) => { throttling = false; - Logger.error(`Failed pruning execution data from database for execution ID ${this.executionId} (hookFunctionsSave)`, { ...error, executionId: this.executionId, sessionId: this.sessionId, workflowId: this.workflowData.id }); + Logger.error( + `Failed pruning execution data from database for execution ID ${this.executionId} (hookFunctionsSave)`, + { + ...error, + executionId: this.executionId, + sessionId: this.sessionId, + workflowId: this.workflowData.id, + }, + ); }); } } - /** * Returns hook functions to push data to Editor-UI * @@ -145,13 +196,21 @@ function hookFunctionsPush(): IWorkflowExecuteHooks { if (this.sessionId === undefined) { return; } - Logger.debug(`Executing hook on node "${nodeName}" (hookFunctionsPush)`, { executionId: this.executionId, sessionId: this.sessionId, workflowId: this.workflowData.id }); + Logger.debug(`Executing hook on node "${nodeName}" (hookFunctionsPush)`, { + executionId: this.executionId, + sessionId: this.sessionId, + workflowId: this.workflowData.id, + }); const pushInstance = Push.getInstance(); - pushInstance.send('nodeExecuteBefore', { - executionId: this.executionId, - nodeName, - }, this.sessionId); + pushInstance.send( + 'nodeExecuteBefore', + { + executionId: this.executionId, + nodeName, + }, + this.sessionId, + ); }, ], nodeExecuteAfter: [ @@ -160,37 +219,62 @@ function hookFunctionsPush(): IWorkflowExecuteHooks { if (this.sessionId === undefined) { return; } - Logger.debug(`Executing hook on node "${nodeName}" (hookFunctionsPush)`, { executionId: this.executionId, sessionId: this.sessionId, workflowId: this.workflowData.id }); + Logger.debug(`Executing hook on node "${nodeName}" (hookFunctionsPush)`, { + executionId: this.executionId, + sessionId: this.sessionId, + workflowId: this.workflowData.id, + }); const pushInstance = Push.getInstance(); - pushInstance.send('nodeExecuteAfter', { - executionId: this.executionId, - nodeName, - data, - }, this.sessionId); + pushInstance.send( + 'nodeExecuteAfter', + { + executionId: this.executionId, + nodeName, + data, + }, + this.sessionId, + ); }, ], workflowExecuteBefore: [ async function (this: WorkflowHooks): Promise { - Logger.debug(`Executing hook (hookFunctionsPush)`, { executionId: this.executionId, sessionId: this.sessionId, workflowId: this.workflowData.id }); + Logger.debug(`Executing hook (hookFunctionsPush)`, { + executionId: this.executionId, + sessionId: this.sessionId, + workflowId: this.workflowData.id, + }); // Push data to session which started the workflow if (this.sessionId === undefined) { return; } const pushInstance = Push.getInstance(); - pushInstance.send('executionStarted', { - executionId: this.executionId, - mode: this.mode, - startedAt: new Date(), - retryOf: this.retryOf, - workflowId: this.workflowData.id, sessionId: this.sessionId as string, - workflowName: this.workflowData.name, - }, this.sessionId); + pushInstance.send( + 'executionStarted', + { + executionId: this.executionId, + mode: this.mode, + startedAt: new Date(), + retryOf: this.retryOf, + workflowId: this.workflowData.id, + sessionId: this.sessionId, + workflowName: this.workflowData.name, + }, + this.sessionId, + ); }, ], workflowExecuteAfter: [ - async function (this: WorkflowHooks, fullRunData: IRun, newStaticData: IDataObject): Promise { - Logger.debug(`Executing hook (hookFunctionsPush)`, { executionId: this.executionId, sessionId: this.sessionId, workflowId: this.workflowData.id }); + async function ( + this: WorkflowHooks, + fullRunData: IRun, + newStaticData: IDataObject, + ): Promise { + Logger.debug(`Executing hook (hookFunctionsPush)`, { + executionId: this.executionId, + sessionId: this.sessionId, + workflowId: this.workflowData.id, + }); // Push data to session which started the workflow if (this.sessionId === undefined) { return; @@ -211,7 +295,10 @@ function hookFunctionsPush(): IWorkflowExecuteHooks { }; // Push data to editor-ui once workflow finished - Logger.debug(`Save execution progress to database for execution ID ${this.executionId} `, { executionId: this.executionId, workflowId: this.workflowData.id }); + Logger.debug(`Save execution progress to database for execution ID ${this.executionId} `, { + executionId: this.executionId, + workflowId: this.workflowData.id, + }); // TODO: Look at this again const sendData: IPushDataExecutionFinished = { executionId: this.executionId, @@ -226,7 +313,6 @@ function hookFunctionsPush(): IWorkflowExecuteHooks { }; } - export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowExecuteHooks { const externalHooks = ExternalHooks(); @@ -237,20 +323,32 @@ export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowEx }, ], nodeExecuteAfter: [ - async function (nodeName: string, data: ITaskData, executionData: IRunExecutionData): Promise { + async function ( + nodeName: string, + data: ITaskData, + executionData: IRunExecutionData, + ): Promise { if (this.workflowData.settings !== undefined) { if (this.workflowData.settings.saveExecutionProgress === false) { return; - } else if (this.workflowData.settings.saveExecutionProgress !== true && !config.get('executions.saveExecutionProgress') as boolean) { + } + if ( + this.workflowData.settings.saveExecutionProgress !== true && + !config.get('executions.saveExecutionProgress') + ) { return; } - } else if (!config.get('executions.saveExecutionProgress') as boolean) { + } else if (!config.get('executions.saveExecutionProgress')) { return; } try { - Logger.debug(`Save execution progress to database for execution ID ${this.executionId} `, { executionId: this.executionId, nodeName }); + Logger.debug( + `Save execution progress to database for execution ID ${this.executionId} `, + { executionId: this.executionId, nodeName }, + ); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const execution = await Db.collections.Execution!.findOne(this.executionId); if (execution === undefined) { @@ -258,7 +356,8 @@ export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowEx // This check is here mostly to make typescript happy. return; } - const fullExecutionData: IExecutionResponse = ResponseHelper.unflattenExecutionData(execution); + const fullExecutionData: IExecutionResponse = + ResponseHelper.unflattenExecutionData(execution); if (fullExecutionData.finished) { // We already received ´workflowExecuteAfter´ webhook, so this is just an async call @@ -296,22 +395,32 @@ export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowEx const flattenedExecutionData = ResponseHelper.flattenExecutionData(fullExecutionData); - await Db.collections.Execution!.update(this.executionId, flattenedExecutionData as IExecutionFlattedDb); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await Db.collections.Execution!.update( + this.executionId, + flattenedExecutionData as IExecutionFlattedDb, + ); } catch (err) { // TODO: Improve in the future! // Errors here might happen because of database access // For busy machines, we may get "Database is locked" errors. // We do this to prevent crashes and executions ending in `unknown` state. - Logger.error(`Failed saving execution progress to database for execution ID ${this.executionId} (hookFunctionsPreExecute, nodeExecuteAfter)`, { ...err, executionId: this.executionId, sessionId: this.sessionId, workflowId: this.workflowData.id }); + Logger.error( + `Failed saving execution progress to database for execution ID ${this.executionId} (hookFunctionsPreExecute, nodeExecuteAfter)`, + { + ...err, + executionId: this.executionId, + sessionId: this.sessionId, + workflowId: this.workflowData.id, + }, + ); } - }, ], }; } - /** * Returns hook functions to save workflow execution and call error workflow * @@ -323,8 +432,15 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks { nodeExecuteAfter: [], workflowExecuteBefore: [], workflowExecuteAfter: [ - async function (this: WorkflowHooks, fullRunData: IRun, newStaticData: IDataObject): Promise { - Logger.debug(`Executing hook (hookFunctionsSave)`, { executionId: this.executionId, workflowId: this.workflowData.id }); + async function ( + this: WorkflowHooks, + fullRunData: IRun, + newStaticData: IDataObject, + ): Promise { + Logger.debug(`Executing hook (hookFunctionsSave)`, { + executionId: this.executionId, + workflowId: this.workflowData.id, + }); // Prune old execution data if (config.get('executions.pruneData')) { @@ -334,23 +450,37 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks { const isManualMode = [this.mode, parentProcessMode].includes('manual'); try { - if (!isManualMode && WorkflowHelpers.isWorkflowIdValid(this.workflowData.id as string) === true && newStaticData) { + if ( + !isManualMode && + WorkflowHelpers.isWorkflowIdValid(this.workflowData.id as string) && + newStaticData + ) { // Workflow is saved so update in database try { - await WorkflowHelpers.saveStaticDataById(this.workflowData.id as string, newStaticData); + await WorkflowHelpers.saveStaticDataById( + this.workflowData.id as string, + newStaticData, + ); } catch (e) { - Logger.error(`There was a problem saving the workflow with id "${this.workflowData.id}" to save changed staticData: "${e.message}" (hookFunctionsSave)`, { executionId: this.executionId, workflowId: this.workflowData.id }); + Logger.error( + `There was a problem saving the workflow with id "${this.workflowData.id}" to save changed staticData: "${e.message}" (hookFunctionsSave)`, + { executionId: this.executionId, workflowId: this.workflowData.id }, + ); } } let saveManualExecutions = config.get('executions.saveDataManualExecutions') as boolean; - if (this.workflowData.settings !== undefined && this.workflowData.settings.saveManualExecutions !== undefined) { + if ( + this.workflowData.settings !== undefined && + this.workflowData.settings.saveManualExecutions !== undefined + ) { // Apply to workflow override saveManualExecutions = this.workflowData.settings.saveManualExecutions as boolean; } - if (isManualMode && saveManualExecutions === false && !fullRunData.waitTill) { + if (isManualMode && !saveManualExecutions && !fullRunData.waitTill) { // Data is always saved, so we remove from database + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion await Db.collections.Execution!.delete(this.executionId); return; } @@ -359,17 +489,28 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks { let saveDataErrorExecution = config.get('executions.saveDataOnError') as string; let saveDataSuccessExecution = config.get('executions.saveDataOnSuccess') as string; if (this.workflowData.settings !== undefined) { - saveDataErrorExecution = (this.workflowData.settings.saveDataErrorExecution as string) || saveDataErrorExecution; - saveDataSuccessExecution = (this.workflowData.settings.saveDataSuccessExecution as string) || saveDataSuccessExecution; + saveDataErrorExecution = + (this.workflowData.settings.saveDataErrorExecution as string) || + saveDataErrorExecution; + saveDataSuccessExecution = + (this.workflowData.settings.saveDataSuccessExecution as string) || + saveDataSuccessExecution; } const workflowDidSucceed = !fullRunData.data.resultData.error; - if (workflowDidSucceed === true && saveDataSuccessExecution === 'none' || - workflowDidSucceed === false && saveDataErrorExecution === 'none' + if ( + (workflowDidSucceed && saveDataSuccessExecution === 'none') || + (!workflowDidSucceed && saveDataErrorExecution === 'none') ) { if (!fullRunData.waitTill) { if (!isManualMode) { - executeErrorWorkflow(this.workflowData, fullRunData, this.mode, undefined, this.retryOf); + executeErrorWorkflow( + this.workflowData, + fullRunData, + this.mode, + undefined, + this.retryOf, + ); } // Data is always saved, so we remove from database await Db.collections.Execution!.delete(this.executionId); @@ -391,7 +532,10 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks { fullExecutionData.retryOf = this.retryOf.toString(); } - if (this.workflowData.id !== undefined && WorkflowHelpers.isWorkflowIdValid(this.workflowData.id.toString()) === true) { + if ( + this.workflowData.id !== undefined && + WorkflowHelpers.isWorkflowIdValid(this.workflowData.id.toString()) + ) { fullExecutionData.workflowId = this.workflowData.id.toString(); } @@ -406,16 +550,27 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks { const executionData = ResponseHelper.flattenExecutionData(fullExecutionData); // Save the Execution in DB - await Db.collections.Execution!.update(this.executionId, executionData as IExecutionFlattedDb); + await Db.collections.Execution!.update( + this.executionId, + executionData as IExecutionFlattedDb, + ); if (fullRunData.finished === true && this.retryOf !== undefined) { // If the retry was successful save the reference it on the original execution // await Db.collections.Execution!.save(executionData as IExecutionFlattedDb); - await Db.collections.Execution!.update(this.retryOf, { retrySuccessId: this.executionId }); + await Db.collections.Execution!.update(this.retryOf, { + retrySuccessId: this.executionId, + }); } if (!isManualMode) { - executeErrorWorkflow(this.workflowData, fullRunData, this.mode, this.executionId, this.retryOf); + executeErrorWorkflow( + this.workflowData, + fullRunData, + this.mode, + this.executionId, + this.retryOf, + ); } } catch (error) { Logger.error(`Failed saving execution data to DB on execution ID ${this.executionId}`, { @@ -425,7 +580,13 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks { }); if (!isManualMode) { - executeErrorWorkflow(this.workflowData, fullRunData, this.mode, undefined, this.retryOf); + executeErrorWorkflow( + this.workflowData, + fullRunData, + this.mode, + undefined, + this.retryOf, + ); } } }, @@ -433,7 +594,6 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks { }; } - /** * Returns hook functions to save workflow execution and call error workflow * for running with queues. Manual executions should never run on queues as @@ -447,20 +607,36 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks { nodeExecuteAfter: [], workflowExecuteBefore: [], workflowExecuteAfter: [ - async function (this: WorkflowHooks, fullRunData: IRun, newStaticData: IDataObject): Promise { + async function ( + this: WorkflowHooks, + fullRunData: IRun, + newStaticData: IDataObject, + ): Promise { try { - if (WorkflowHelpers.isWorkflowIdValid(this.workflowData.id as string) === true && newStaticData) { + if (WorkflowHelpers.isWorkflowIdValid(this.workflowData.id as string) && newStaticData) { // Workflow is saved so update in database try { - await WorkflowHelpers.saveStaticDataById(this.workflowData.id as string, newStaticData); + await WorkflowHelpers.saveStaticDataById( + this.workflowData.id as string, + newStaticData, + ); } catch (e) { - Logger.error(`There was a problem saving the workflow with id "${this.workflowData.id}" to save changed staticData: "${e.message}" (workflowExecuteAfter)`, { sessionId: this.sessionId, workflowId: this.workflowData.id }); + Logger.error( + `There was a problem saving the workflow with id "${this.workflowData.id}" to save changed staticData: "${e.message}" (workflowExecuteAfter)`, + { sessionId: this.sessionId, workflowId: this.workflowData.id }, + ); } } const workflowDidSucceed = !fullRunData.data.resultData.error; - if (workflowDidSucceed === false) { - executeErrorWorkflow(this.workflowData, fullRunData, this.mode, undefined, this.retryOf); + if (!workflowDidSucceed) { + executeErrorWorkflow( + this.workflowData, + fullRunData, + this.mode, + undefined, + this.retryOf, + ); } const fullExecutionData: IExecutionDb = { @@ -477,18 +653,26 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks { fullExecutionData.retryOf = this.retryOf.toString(); } - if (this.workflowData.id !== undefined && WorkflowHelpers.isWorkflowIdValid(this.workflowData.id.toString()) === true) { + if ( + this.workflowData.id !== undefined && + WorkflowHelpers.isWorkflowIdValid(this.workflowData.id.toString()) + ) { fullExecutionData.workflowId = this.workflowData.id.toString(); } const executionData = ResponseHelper.flattenExecutionData(fullExecutionData); // Save the Execution in DB - await Db.collections.Execution!.update(this.executionId, executionData as IExecutionFlattedDb); + await Db.collections.Execution!.update( + this.executionId, + executionData as IExecutionFlattedDb, + ); if (fullRunData.finished === true && this.retryOf !== undefined) { // If the retry was successful save the reference it on the original execution - await Db.collections.Execution!.update(this.retryOf, { retrySuccessId: this.executionId }); + await Db.collections.Execution!.update(this.retryOf, { + retrySuccessId: this.executionId, + }); } } catch (error) { executeErrorWorkflow(this.workflowData, fullRunData, this.mode, undefined, this.retryOf); @@ -498,13 +682,17 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks { }; } -export async function getRunData(workflowData: IWorkflowBase, inputData?: INodeExecutionData[]): Promise { +export async function getRunData( + workflowData: IWorkflowBase, + inputData?: INodeExecutionData[], +): Promise { const mode = 'integrated'; // Find Start-Node const requiredNodeTypes = ['n8n-nodes-base.start']; let startNode: INode | undefined; - for (const node of workflowData!.nodes) { + // eslint-disable-next-line no-restricted-syntax + for (const node of workflowData.nodes) { if (requiredNodeTypes.includes(node.type)) { startNode = node; break; @@ -525,18 +713,15 @@ export async function getRunData(workflowData: IWorkflowBase, inputData?: INodeE // Initialize the incoming data const nodeExecutionStack: IExecuteData[] = []; - nodeExecutionStack.push( - { - node: startNode, - data: { - main: [inputData], - }, - } - ); + nodeExecutionStack.push({ + node: startNode, + data: { + main: [inputData], + }, + }); const runExecutionData: IRunExecutionData = { - startData: { - }, + startData: {}, resultData: { runData: {}, }, @@ -557,13 +742,14 @@ export async function getRunData(workflowData: IWorkflowBase, inputData?: INodeE return runData; } - export async function getWorkflowData(workflowInfo: IExecuteWorkflowInfo): Promise { if (workflowInfo.id === undefined && workflowInfo.code === undefined) { - throw new Error(`No information about the workflow to execute found. Please provide either the "id" or "code"!`); + throw new Error( + `No information about the workflow to execute found. Please provide either the "id" or "code"!`, + ); } - if (Db.collections!.Workflow === null) { + if (Db.collections.Workflow === null) { // The first time executeWorkflow gets called the Database has // to get initialized first await Db.init(); @@ -571,7 +757,7 @@ export async function getWorkflowData(workflowInfo: IExecuteWorkflowInfo): Promi let workflowData: IWorkflowBase | undefined; if (workflowInfo.id !== undefined) { - workflowData = await Db.collections!.Workflow!.findOne(workflowInfo.id); + workflowData = await Db.collections.Workflow!.findOne(workflowInfo.id); if (workflowData === undefined) { throw new Error(`The workflow with the id "${workflowInfo.id}" does not exist.`); } @@ -582,7 +768,6 @@ export async function getWorkflowData(workflowInfo: IExecuteWorkflowInfo): Promi return workflowData!; } - /** * Executes the workflow with the given ID * @@ -592,25 +777,45 @@ export async function getWorkflowData(workflowInfo: IExecuteWorkflowInfo): Promi * @param {INodeExecutionData[]} [inputData] * @returns {(Promise>)} */ -export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additionalData: IWorkflowExecuteAdditionalData, inputData?: INodeExecutionData[], parentExecutionId?: string, loadedWorkflowData?: IWorkflowBase, loadedRunData?: IWorkflowExecutionDataProcess): Promise | IWorkflowExecuteProcess> { +export async function executeWorkflow( + workflowInfo: IExecuteWorkflowInfo, + additionalData: IWorkflowExecuteAdditionalData, + inputData?: INodeExecutionData[], + parentExecutionId?: string, + loadedWorkflowData?: IWorkflowBase, + loadedRunData?: IWorkflowExecutionDataProcess, +): Promise | IWorkflowExecuteProcess> { const externalHooks = ExternalHooks(); await externalHooks.init(); const nodeTypes = NodeTypes(); - const workflowData = loadedWorkflowData !== undefined ? loadedWorkflowData : await getWorkflowData(workflowInfo); + const workflowData = + loadedWorkflowData !== undefined ? loadedWorkflowData : await getWorkflowData(workflowInfo); const workflowName = workflowData ? workflowData.name : undefined; - const workflow = new Workflow({ id: workflowInfo.id, name: workflowName, nodes: workflowData!.nodes, connections: workflowData!.connections, active: workflowData!.active, nodeTypes, staticData: workflowData!.staticData }); + const workflow = new Workflow({ + id: workflowInfo.id, + name: workflowName, + nodes: workflowData.nodes, + connections: workflowData.connections, + active: workflowData.active, + nodeTypes, + staticData: workflowData.staticData, + }); - const runData = loadedRunData !== undefined ? loadedRunData : await getRunData(workflowData, inputData); + const runData = + loadedRunData !== undefined ? loadedRunData : await getRunData(workflowData, inputData); let executionId; if (parentExecutionId !== undefined) { executionId = parentExecutionId; } else { - executionId = parentExecutionId !== undefined ? parentExecutionId : await ActiveExecutions.getInstance().add(runData); + executionId = + parentExecutionId !== undefined + ? parentExecutionId + : await ActiveExecutions.getInstance().add(runData); } let data; @@ -618,18 +823,29 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi // Create new additionalData to have different workflow loaded and to call // different webooks const additionalDataIntegrated = await getBase(); - additionalDataIntegrated.hooks = getWorkflowHooksIntegrated(runData.executionMode, executionId, workflowData!, { parentProcessMode: additionalData.hooks!.mode }); + additionalDataIntegrated.hooks = getWorkflowHooksIntegrated( + runData.executionMode, + executionId, + workflowData, + { parentProcessMode: additionalData.hooks!.mode }, + ); // Make sure we pass on the original executeWorkflow function we received // This one already contains changes to talk to parent process // and get executionID from `activeExecutions` running on main process additionalDataIntegrated.executeWorkflow = additionalData.executeWorkflow; let subworkflowTimeout = additionalData.executionTimeoutTimestamp; - if (workflowData.settings?.executionTimeout !== undefined && workflowData.settings.executionTimeout > 0) { + if ( + workflowData.settings?.executionTimeout !== undefined && + workflowData.settings.executionTimeout > 0 + ) { // We might have received a max timeout timestamp from the parent workflow // If we did, then we get the minimum time between the two timeouts // If no timeout was given from the parent, then we use our timeout. - subworkflowTimeout = Math.min(additionalData.executionTimeoutTimestamp || Number.MAX_SAFE_INTEGER, Date.now() + (workflowData.settings.executionTimeout as number * 1000)); + subworkflowTimeout = Math.min( + additionalData.executionTimeoutTimestamp || Number.MAX_SAFE_INTEGER, + Date.now() + (workflowData.settings.executionTimeout as number) * 1000, + ); } additionalDataIntegrated.executionTimeoutTimestamp = subworkflowTimeout; @@ -637,7 +853,11 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi const runExecutionData = runData.executionData as IRunExecutionData; // Execute the workflow - const workflowExecute = new WorkflowExecute(additionalDataIntegrated, runData.executionMode, runExecutionData); + const workflowExecute = new WorkflowExecute( + additionalDataIntegrated, + runData.executionMode, + runExecutionData, + ); if (parentExecutionId !== undefined) { // Must be changed to become typed return { @@ -678,7 +898,7 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi await Db.collections.Execution!.update(executionId, executionData as IExecutionFlattedDb); throw { ...error, - stack: error!.stack, + stack: error.stack, }; } @@ -690,19 +910,19 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi await ActiveExecutions.getInstance().remove(executionId, data); const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data); return returnData!.data!.main; - } else { - await ActiveExecutions.getInstance().remove(executionId, data); - // Workflow did fail - const { error } = data.data.resultData; - throw { - ...error, - stack: error!.stack, - }; } + await ActiveExecutions.getInstance().remove(executionId, data); + // Workflow did fail + const { error } = data.data.resultData; + // eslint-disable-next-line @typescript-eslint/no-throw-literal + throw { + ...error, + stack: error!.stack, + }; } - -export function sendMessageToUI(source: string, message: any) { // tslint:disable-line:no-any +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function sendMessageToUI(source: string, message: any) { if (this.sessionId === undefined) { return; } @@ -710,16 +930,19 @@ export function sendMessageToUI(source: string, message: any) { // tslint:disabl // Push data to session which started workflow try { const pushInstance = Push.getInstance(); - pushInstance.send('sendConsoleMessage', { - source: `Node: "${source}"`, - message, - }, this.sessionId); + pushInstance.send( + 'sendConsoleMessage', + { + source: `Node: "${source}"`, + message, + }, + this.sessionId, + ); } catch (error) { Logger.warn(`There was a problem sending messsage to UI: ${error.message}`); } } - /** * Returns the base additional data without webhooks * @@ -728,13 +951,16 @@ export function sendMessageToUI(source: string, message: any) { // tslint:disabl * @param {INodeParameters} currentNodeParameters * @returns {Promise} */ -export async function getBase(currentNodeParameters?: INodeParameters, executionTimeoutTimestamp?: number): Promise { +export async function getBase( + currentNodeParameters?: INodeParameters, + executionTimeoutTimestamp?: number, +): Promise { const urlBaseWebhook = WebhookHelpers.getWebhookBaseUrl(); const timezone = config.get('generic.timezone') as string; - const webhookBaseUrl = urlBaseWebhook + config.get('endpoints.webhook') as string; - const webhookWaitingBaseUrl = urlBaseWebhook + config.get('endpoints.webhookWaiting') as string; - const webhookTestBaseUrl = urlBaseWebhook + config.get('endpoints.webhookTest') as string; + const webhookBaseUrl = urlBaseWebhook + config.get('endpoints.webhook'); + const webhookWaitingBaseUrl = urlBaseWebhook + config.get('endpoints.webhookWaiting'); + const webhookTestBaseUrl = urlBaseWebhook + config.get('endpoints.webhookTest'); const encryptionKey = await UserSettings.getEncryptionKey(); if (encryptionKey === undefined) { @@ -745,7 +971,7 @@ export async function getBase(currentNodeParameters?: INodeParameters, execution credentialsHelper: new CredentialsHelper(encryptionKey), encryptionKey, executeWorkflow, - restApiUrl: urlBaseWebhook + config.get('endpoints.rest') as string, + restApiUrl: urlBaseWebhook + config.get('endpoints.rest'), timezone, webhookBaseUrl, webhookWaitingBaseUrl, @@ -755,12 +981,16 @@ export async function getBase(currentNodeParameters?: INodeParameters, execution }; } - /** * Returns WorkflowHooks instance for running integrated workflows * (Workflows which get started inside of another workflow) */ -export function getWorkflowHooksIntegrated(mode: WorkflowExecuteMode, executionId: string, workflowData: IWorkflowBase, optionalParameters?: IWorkflowHooksOptionalParameters): WorkflowHooks { +export function getWorkflowHooksIntegrated( + mode: WorkflowExecuteMode, + executionId: string, + workflowData: IWorkflowBase, + optionalParameters?: IWorkflowHooksOptionalParameters, +): WorkflowHooks { optionalParameters = optionalParameters || {}; const hookFunctions = hookFunctionsSave(optionalParameters.parentProcessMode); const preExecuteFunctions = hookFunctionsPreExecute(optionalParameters.parentProcessMode); @@ -777,7 +1007,12 @@ export function getWorkflowHooksIntegrated(mode: WorkflowExecuteMode, executionI * Returns WorkflowHooks instance for running integrated workflows * (Workflows which get started inside of another workflow) */ -export function getWorkflowHooksWorkerExecuter(mode: WorkflowExecuteMode, executionId: string, workflowData: IWorkflowBase, optionalParameters?: IWorkflowHooksOptionalParameters): WorkflowHooks { +export function getWorkflowHooksWorkerExecuter( + mode: WorkflowExecuteMode, + executionId: string, + workflowData: IWorkflowBase, + optionalParameters?: IWorkflowHooksOptionalParameters, +): WorkflowHooks { optionalParameters = optionalParameters || {}; const hookFunctions = hookFunctionsSaveWorker(); const preExecuteFunctions = hookFunctionsPreExecute(optionalParameters.parentProcessMode); @@ -793,7 +1028,12 @@ export function getWorkflowHooksWorkerExecuter(mode: WorkflowExecuteMode, execut /** * Returns WorkflowHooks instance for main process if workflow runs via worker */ -export function getWorkflowHooksWorkerMain(mode: WorkflowExecuteMode, executionId: string, workflowData: IWorkflowBase, optionalParameters?: IWorkflowHooksOptionalParameters): WorkflowHooks { +export function getWorkflowHooksWorkerMain( + mode: WorkflowExecuteMode, + executionId: string, + workflowData: IWorkflowBase, + optionalParameters?: IWorkflowHooksOptionalParameters, +): WorkflowHooks { optionalParameters = optionalParameters || {}; const hookFunctions = hookFunctionsPush(); const preExecuteFunctions = hookFunctionsPreExecute(optionalParameters.parentProcessMode); @@ -812,7 +1052,6 @@ export function getWorkflowHooksWorkerMain(mode: WorkflowExecuteMode, executionI return new WorkflowHooks(hookFunctions, mode, executionId, workflowData, optionalParameters); } - /** * Returns WorkflowHooks instance for running the main workflow * @@ -821,7 +1060,11 @@ export function getWorkflowHooksWorkerMain(mode: WorkflowExecuteMode, executionI * @param {string} executionId * @returns {WorkflowHooks} */ -export function getWorkflowHooksMain(data: IWorkflowExecutionDataProcess, executionId: string, isMainProcess = false): WorkflowHooks { +export function getWorkflowHooksMain( + data: IWorkflowExecutionDataProcess, + executionId: string, + isMainProcess = false, +): WorkflowHooks { const hookFunctions = hookFunctionsSave(); const pushFunctions = hookFunctionsPush(); for (const key of Object.keys(pushFunctions)) { @@ -841,5 +1084,8 @@ export function getWorkflowHooksMain(data: IWorkflowExecutionDataProcess, execut } } - return new WorkflowHooks(hookFunctions, data.executionMode, executionId, data.workflowData, { sessionId: data.sessionId, retryOf: data.retryOf as string }); + return new WorkflowHooks(hookFunctions, data.executionMode, executionId, data.workflowData, { + sessionId: data.sessionId, + retryOf: data.retryOf as string, + }); } diff --git a/packages/cli/src/WorkflowHelpers.ts b/packages/cli/src/WorkflowHelpers.ts index 5107e261c8..138965994e 100644 --- a/packages/cli/src/WorkflowHelpers.ts +++ b/packages/cli/src/WorkflowHelpers.ts @@ -1,3 +1,24 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable no-underscore-dangle */ +/* eslint-disable no-continue */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable no-restricted-syntax */ +/* eslint-disable no-param-reassign */ +import { + IDataObject, + IExecuteData, + INode, + IRun, + IRunExecutionData, + ITaskData, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + IWorkflowCredentials, + LoggerProxy as Logger, + Workflow, +} from 'n8n-workflow'; +import { validate } from 'class-validator'; +// eslint-disable-next-line import/no-cycle import { CredentialTypes, Db, @@ -7,28 +28,17 @@ import { IWorkflowExecutionDataProcess, NodeTypes, ResponseHelper, + // eslint-disable-next-line @typescript-eslint/no-unused-vars WorkflowCredentials, WorkflowRunner, -} from './'; - -import { - IDataObject, - IExecuteData, - INode, - IRun, - IRunExecutionData, - ITaskData, - IWorkflowCredentials, - LoggerProxy as Logger, - Workflow,} from 'n8n-workflow'; +} from '.'; import * as config from '../config'; +// eslint-disable-next-line import/no-cycle import { WorkflowEntity } from './databases/entities/WorkflowEntity'; -import { validate } from 'class-validator'; const ERROR_TRIGGER_TYPE = config.get('nodes.errorTriggerType') as string; - /** * Returns the data of the last executed node * @@ -37,8 +47,8 @@ const ERROR_TRIGGER_TYPE = config.get('nodes.errorTriggerType') as string; * @returns {(ITaskData | undefined)} */ export function getDataLastExecutedNodeData(inputData: IRun): ITaskData | undefined { - const runData = inputData.data.resultData.runData; - const lastNodeExecuted = inputData.data.resultData.lastNodeExecuted; + const { runData } = inputData.data.resultData; + const { lastNodeExecuted } = inputData.data.resultData; if (lastNodeExecuted === undefined) { return undefined; @@ -51,8 +61,6 @@ export function getDataLastExecutedNodeData(inputData: IRun): ITaskData | undefi return runData[lastNodeExecuted][runData[lastNodeExecuted].length - 1]; } - - /** * Returns if the given id is a valid workflow id * @@ -60,20 +68,18 @@ export function getDataLastExecutedNodeData(inputData: IRun): ITaskData | undefi * @returns {boolean} * @memberof App */ -export function isWorkflowIdValid (id: string | null | undefined | number): boolean { +export function isWorkflowIdValid(id: string | null | undefined | number): boolean { if (typeof id === 'string') { id = parseInt(id, 10); } + // eslint-disable-next-line no-restricted-globals if (isNaN(id as number)) { return false; - } return true; } - - /** * Executes the error workflow * @@ -82,21 +88,37 @@ export function isWorkflowIdValid (id: string | null | undefined | number): bool * @param {IWorkflowErrorData} workflowErrorData The error data * @returns {Promise} */ -export async function executeErrorWorkflow(workflowId: string, workflowErrorData: IWorkflowErrorData): Promise { +export async function executeErrorWorkflow( + workflowId: string, + workflowErrorData: IWorkflowErrorData, +): Promise { // Wrap everything in try/catch to make sure that no errors bubble up and all get caught here try { const workflowData = await Db.collections.Workflow!.findOne({ id: Number(workflowId) }); if (workflowData === undefined) { // The error workflow could not be found - Logger.error(`Calling Error Workflow for "${workflowErrorData.workflow.id}". Could not find error workflow "${workflowId}"`, { workflowId }); + Logger.error( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Calling Error Workflow for "${workflowErrorData.workflow.id}". Could not find error workflow "${workflowId}"`, + { workflowId }, + ); return; } const executionMode = 'error'; const nodeTypes = NodeTypes(); - const workflowInstance = new Workflow({ id: workflowId, name: workflowData.name, nodeTypes, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, staticData: workflowData.staticData, settings: workflowData.settings}); + const workflowInstance = new Workflow({ + id: workflowId, + name: workflowData.name, + nodeTypes, + nodes: workflowData.nodes, + connections: workflowData.connections, + active: workflowData.active, + staticData: workflowData.staticData, + settings: workflowData.settings, + }); let node: INode; let workflowStartNode: INode | undefined; @@ -108,7 +130,9 @@ export async function executeErrorWorkflow(workflowId: string, workflowErrorData } if (workflowStartNode === undefined) { - Logger.error(`Calling Error Workflow for "${workflowErrorData.workflow.id}". Could not find "${ERROR_TRIGGER_TYPE}" in workflow "${workflowId}"`); + Logger.error( + `Calling Error Workflow for "${workflowErrorData.workflow.id}". Could not find "${ERROR_TRIGGER_TYPE}" in workflow "${workflowId}"`, + ); return; } @@ -116,24 +140,21 @@ export async function executeErrorWorkflow(workflowId: string, workflowErrorData // Initialize the data of the webhook node const nodeExecutionStack: IExecuteData[] = []; - nodeExecutionStack.push( - { - node: workflowStartNode, - data: { - main: [ - [ - { - json: workflowErrorData, - }, - ], + nodeExecutionStack.push({ + node: workflowStartNode, + data: { + main: [ + [ + { + json: workflowErrorData, + }, ], - }, - } - ); + ], + }, + }); const runExecutionData: IRunExecutionData = { - startData: { - }, + startData: {}, resultData: { runData: {}, }, @@ -153,12 +174,13 @@ export async function executeErrorWorkflow(workflowId: string, workflowErrorData const workflowRunner = new WorkflowRunner(); await workflowRunner.run(runData); } catch (error) { - Logger.error(`Calling Error Workflow for "${workflowErrorData.workflow.id}": "${error.message}"`, { workflowId: workflowErrorData.workflow.id }); + Logger.error( + `Calling Error Workflow for "${workflowErrorData.workflow.id}": "${error.message}"`, + { workflowId: workflowErrorData.workflow.id }, + ); } } - - /** * Returns all the defined NodeTypes * @@ -185,8 +207,6 @@ export function getAllNodeTypeData(): ITransferNodeTypes { return returnData; } - - /** * Returns the data of the node types that are needed * to execute the given nodes @@ -199,6 +219,7 @@ export function getNodeTypeData(nodes: INode[]): ITransferNodeTypes { const nodeTypes = NodeTypes(); // Check which node-types have to be loaded + // eslint-disable-next-line @typescript-eslint/no-use-before-define const neededNodeTypes = getNeededNodeTypes(nodes); // Get all the data of the needed node types that they @@ -218,8 +239,6 @@ export function getNodeTypeData(nodes: INode[]): ITransferNodeTypes { return returnData; } - - /** * Returns the credentials data of the given type and its parent types * it extends @@ -251,8 +270,6 @@ export function getCredentialsDataWithParents(type: string): ICredentialsTypeDat return credentialTypeData; } - - /** * Returns all the credentialTypes which are needed to resolve * the given workflow credentials @@ -262,14 +279,13 @@ export function getCredentialsDataWithParents(type: string): ICredentialsTypeDat * @returns {ICredentialsTypeData} */ export function getCredentialsDataByNodes(nodes: INode[]): ICredentialsTypeData { - const credentialTypeData: ICredentialsTypeData = {}; for (const node of nodes) { const credentialsUsedByThisNode = node.credentials; if (credentialsUsedByThisNode) { // const credentialTypesUsedByThisNode = Object.keys(credentialsUsedByThisNode!); - for (const credentialType of Object.keys(credentialsUsedByThisNode!)) { + for (const credentialType of Object.keys(credentialsUsedByThisNode)) { if (credentialTypeData[credentialType] !== undefined) { continue; } @@ -277,14 +293,11 @@ export function getCredentialsDataByNodes(nodes: INode[]): ICredentialsTypeData Object.assign(credentialTypeData, getCredentialsDataWithParents(credentialType)); } } - } return credentialTypeData; } - - /** * Returns the names of the NodeTypes which are are needed * to execute the gives nodes @@ -305,8 +318,6 @@ export function getNeededNodeTypes(nodes: INode[]): string[] { return neededNodeTypes; } - - /** * Saves the static data if it changed * @@ -314,23 +325,25 @@ export function getNeededNodeTypes(nodes: INode[]): string[] { * @param {Workflow} workflow * @returns {Promise } */ -export async function saveStaticData(workflow: Workflow): Promise { +export async function saveStaticData(workflow: Workflow): Promise { if (workflow.staticData.__dataChanged === true) { // Static data of workflow changed and so has to be saved - if (isWorkflowIdValid(workflow.id) === true) { + if (isWorkflowIdValid(workflow.id)) { // Workflow is saved so update in database try { + // eslint-disable-next-line @typescript-eslint/no-use-before-define await saveStaticDataById(workflow.id!, workflow.staticData); workflow.staticData.__dataChanged = false; } catch (e) { - Logger.error(`There was a problem saving the workflow with id "${workflow.id}" to save changed staticData: "${e.message}"`, { workflowId: workflow.id }); + Logger.error( + `There was a problem saving the workflow with id "${workflow.id}" to save changed staticData: "${e.message}"`, + { workflowId: workflow.id }, + ); } } } } - - /** * Saves the given static data on workflow * @@ -339,15 +352,15 @@ export async function saveStaticData(workflow: Workflow): Promise { * @param {IDataObject} newStaticData The static data to save * @returns {Promise} */ -export async function saveStaticDataById(workflowId: string | number, newStaticData: IDataObject): Promise { - await Db.collections.Workflow! - .update(workflowId, { - staticData: newStaticData, - }); +export async function saveStaticDataById( + workflowId: string | number, + newStaticData: IDataObject, +): Promise { + await Db.collections.Workflow!.update(workflowId, { + staticData: newStaticData, + }); } - - /** * Returns the static data of workflow * @@ -355,20 +368,23 @@ export async function saveStaticDataById(workflowId: string | number, newStaticD * @param {(string | number)} workflowId The id of the workflow to get static data of * @returns */ +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export async function getStaticDataById(workflowId: string | number) { - const workflowData = await Db.collections.Workflow! - .findOne(workflowId, { select: ['staticData']}); + const workflowData = await Db.collections.Workflow!.findOne(workflowId, { + select: ['staticData'], + }); if (workflowData === undefined) { return {}; } + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing return workflowData.staticData || {}; } - // TODO: Deduplicate `validateWorkflow` and `throwDuplicateEntryError` with TagHelpers? +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export async function validateWorkflow(newWorkflow: WorkflowEntity) { const errors = await validate(newWorkflow); @@ -378,10 +394,15 @@ export async function validateWorkflow(newWorkflow: WorkflowEntity) { } } +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export function throwDuplicateEntryError(error: Error) { const errorMessage = error.message.toLowerCase(); if (errorMessage.includes('unique') || errorMessage.includes('duplicate')) { - throw new ResponseHelper.ResponseError('There is already a workflow with this name', undefined, 400); + throw new ResponseHelper.ResponseError( + 'There is already a workflow with this name', + undefined, + 400, + ); } throw new ResponseHelper.ResponseError(errorMessage, undefined, 400); @@ -391,6 +412,5 @@ export type WorkflowNameRequest = Express.Request & { query: { name?: string; offset?: string; - } + }; }; - diff --git a/packages/cli/src/WorkflowRunner.ts b/packages/cli/src/WorkflowRunner.ts index 869b5c199d..1060b18932 100644 --- a/packages/cli/src/WorkflowRunner.ts +++ b/packages/cli/src/WorkflowRunner.ts @@ -1,3 +1,37 @@ +/* eslint-disable no-restricted-syntax */ +/* eslint-disable no-console */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-shadow */ +/* eslint-disable @typescript-eslint/no-floating-promises */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/prefer-optional-chain */ +/* eslint-disable no-param-reassign */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable import/no-cycle */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { IProcessMessage, WorkflowExecute } from 'n8n-core'; + +import { + ExecutionError, + IRun, + IWorkflowBase, + LoggerProxy as Logger, + Workflow, + WorkflowExecuteMode, + WorkflowHooks, + WorkflowOperationError, +} from 'n8n-workflow'; + +// eslint-disable-next-line import/no-extraneous-dependencies +import * as PCancelable from 'p-cancelable'; +import { join as pathJoin } from 'path'; +import { fork } from 'child_process'; + +import * as Bull from 'bull'; +import * as config from '../config'; +// eslint-disable-next-line import/no-cycle import { ActiveExecutions, CredentialsOverwrites, @@ -20,38 +54,17 @@ import { ResponseHelper, WorkflowExecuteAdditionalData, WorkflowHelpers, -} from './'; - -import { - IProcessMessage, - WorkflowExecute, -} from 'n8n-core'; - -import { - ExecutionError, - IRun, - IWorkflowBase, - LoggerProxy as Logger, - Workflow, - WorkflowExecuteMode, - WorkflowHooks, - WorkflowOperationError, -} from 'n8n-workflow'; - -import * as config from '../config'; -import * as PCancelable from 'p-cancelable'; -import { join as pathJoin } from 'path'; -import { fork } from 'child_process'; - -import * as Bull from 'bull'; +} from '.'; import * as Queue from './Queue'; export class WorkflowRunner { activeExecutions: ActiveExecutions.ActiveExecutions; - credentialsOverwrites: ICredentialsOverwrite; - push: Push.Push; - jobQueue: Bull.Queue; + credentialsOverwrites: ICredentialsOverwrite; + + push: Push.Push; + + jobQueue: Bull.Queue; constructor() { this.push = Push.getInstance(); @@ -65,7 +78,6 @@ export class WorkflowRunner { } } - /** * The process did send a hook message so execute the appropiate hook * @@ -74,10 +86,10 @@ export class WorkflowRunner { * @memberof WorkflowRunner */ processHookMessage(workflowHooks: WorkflowHooks, hookData: IProcessMessageDataHook) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises workflowHooks.executeHookFunctions(hookData.hook, hookData.parameters); } - /** * The process did error * @@ -87,7 +99,13 @@ export class WorkflowRunner { * @param {string} executionId * @memberof WorkflowRunner */ - async processError(error: ExecutionError, startedAt: Date, executionMode: WorkflowExecuteMode, executionId: string, hooks?: WorkflowHooks) { + async processError( + error: ExecutionError, + startedAt: Date, + executionMode: WorkflowExecuteMode, + executionId: string, + hooks?: WorkflowHooks, + ) { const fullRunData: IRun = { data: { resultData: { @@ -123,7 +141,12 @@ export class WorkflowRunner { * @returns {Promise} * @memberof WorkflowRunner */ - async run(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean, realtime?: boolean, executionId?: string): Promise { + async run( + data: IWorkflowExecutionDataProcess, + loadStaticData?: boolean, + realtime?: boolean, + executionId?: string, + ): Promise { const executionsProcess = config.get('executions.process') as string; const executionsMode = config.get('executions.mode') as string; @@ -139,11 +162,12 @@ export class WorkflowRunner { const externalHooks = ExternalHooks(); if (externalHooks.exists('workflow.postExecute')) { - this.activeExecutions.getPostExecutePromise(executionId) + this.activeExecutions + .getPostExecutePromise(executionId) .then(async (executionData) => { await externalHooks.run('workflow.postExecute', [executionData, data.workflowData]); }) - .catch(error => { + .catch((error) => { console.error('There was a problem running hook "workflow.postExecute"', error); }); } @@ -151,7 +175,6 @@ export class WorkflowRunner { return executionId; } - /** * Run the workflow in current process * @@ -161,9 +184,15 @@ export class WorkflowRunner { * @returns {Promise} * @memberof WorkflowRunner */ - async runMainProcess(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean, restartExecutionId?: string): Promise { + async runMainProcess( + data: IWorkflowExecutionDataProcess, + loadStaticData?: boolean, + restartExecutionId?: string, + ): Promise { if (loadStaticData === true && data.workflowData.id) { - data.workflowData.staticData = await WorkflowHelpers.getStaticDataById(data.workflowData.id as string); + data.workflowData.staticData = await WorkflowHelpers.getStaticDataById( + data.workflowData.id as string, + ); } const nodeTypes = NodeTypes(); @@ -174,67 +203,120 @@ export class WorkflowRunner { let executionTimeout: NodeJS.Timeout; let workflowTimeout = config.get('executions.timeout') as number; // initialize with default if (data.workflowData.settings && data.workflowData.settings.executionTimeout) { - workflowTimeout = data.workflowData.settings!.executionTimeout as number; // preference on workflow setting + workflowTimeout = data.workflowData.settings.executionTimeout as number; // preference on workflow setting } if (workflowTimeout > 0) { workflowTimeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number); } - const workflow = new Workflow({ id: data.workflowData.id as string | undefined, name: data.workflowData.name, nodes: data.workflowData!.nodes, connections: data.workflowData!.connections, active: data.workflowData!.active, nodeTypes, staticData: data.workflowData!.staticData }); - const additionalData = await WorkflowExecuteAdditionalData.getBase(undefined, workflowTimeout <= 0 ? undefined : Date.now() + workflowTimeout * 1000); + const workflow = new Workflow({ + id: data.workflowData.id as string | undefined, + name: data.workflowData.name, + nodes: data.workflowData.nodes, + connections: data.workflowData.connections, + active: data.workflowData.active, + nodeTypes, + staticData: data.workflowData.staticData, + }); + const additionalData = await WorkflowExecuteAdditionalData.getBase( + undefined, + workflowTimeout <= 0 ? undefined : Date.now() + workflowTimeout * 1000, + ); // Register the active execution - const executionId = await this.activeExecutions.add(data, undefined, restartExecutionId) as string; + const executionId = await this.activeExecutions.add(data, undefined, restartExecutionId); additionalData.executionId = executionId; - Logger.verbose(`Execution for workflow ${data.workflowData.name} was assigned id ${executionId}`, {executionId}); + Logger.verbose( + `Execution for workflow ${data.workflowData.name} was assigned id ${executionId}`, + { executionId }, + ); let workflowExecution: PCancelable; try { - Logger.verbose(`Execution for workflow ${data.workflowData.name} was assigned id ${executionId}`, { executionId }); - additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId, true); - additionalData.sendMessageToUI = WorkflowExecuteAdditionalData.sendMessageToUI.bind({sessionId: data.sessionId}); + Logger.verbose( + `Execution for workflow ${data.workflowData.name} was assigned id ${executionId}`, + { executionId }, + ); + additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain( + data, + executionId, + true, + ); + additionalData.sendMessageToUI = WorkflowExecuteAdditionalData.sendMessageToUI.bind({ + sessionId: data.sessionId, + }); if (data.executionData !== undefined) { - Logger.debug(`Execution ID ${executionId} had Execution data. Running with payload.`, {executionId}); - const workflowExecute = new WorkflowExecute(additionalData, data.executionMode, data.executionData); + Logger.debug(`Execution ID ${executionId} had Execution data. Running with payload.`, { + executionId, + }); + const workflowExecute = new WorkflowExecute( + additionalData, + data.executionMode, + data.executionData, + ); workflowExecution = workflowExecute.processRunExecutionData(workflow); - } else if (data.runData === undefined || data.startNodes === undefined || data.startNodes.length === 0 || data.destinationNode === undefined) { - Logger.debug(`Execution ID ${executionId} will run executing all nodes.`, {executionId}); + } else if ( + data.runData === undefined || + data.startNodes === undefined || + data.startNodes.length === 0 || + data.destinationNode === undefined + ) { + Logger.debug(`Execution ID ${executionId} will run executing all nodes.`, { executionId }); // Execute all nodes // Can execute without webhook so go on const workflowExecute = new WorkflowExecute(additionalData, data.executionMode); workflowExecution = workflowExecute.run(workflow, undefined, data.destinationNode); } else { - Logger.debug(`Execution ID ${executionId} is a partial execution.`, {executionId}); + Logger.debug(`Execution ID ${executionId} is a partial execution.`, { executionId }); // Execute only the nodes between start and destination nodes const workflowExecute = new WorkflowExecute(additionalData, data.executionMode); - workflowExecution = workflowExecute.runPartialWorkflow(workflow, data.runData, data.startNodes, data.destinationNode); + workflowExecution = workflowExecute.runPartialWorkflow( + workflow, + data.runData, + data.startNodes, + data.destinationNode, + ); } this.activeExecutions.attachWorkflowExecution(executionId, workflowExecution); if (workflowTimeout > 0) { - const timeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number) * 1000; // as seconds + const timeout = + Math.min(workflowTimeout, config.get('executions.maxTimeout') as number) * 1000; // as seconds executionTimeout = setTimeout(() => { this.activeExecutions.stopExecution(executionId, 'timeout'); }, timeout); } - workflowExecution.then((fullRunData) => { - clearTimeout(executionTimeout); - if (workflowExecution.isCanceled) { - fullRunData.finished = false; - } - this.activeExecutions.remove(executionId, fullRunData); - }).catch((error) => { - this.processError(error, new Date(), data.executionMode, executionId, additionalData.hooks); - }); - + workflowExecution + .then((fullRunData) => { + clearTimeout(executionTimeout); + if (workflowExecution.isCanceled) { + fullRunData.finished = false; + } + this.activeExecutions.remove(executionId, fullRunData); + }) + .catch((error) => { + this.processError( + error, + new Date(), + data.executionMode, + executionId, + additionalData.hooks, + ); + }); } catch (error) { - await this.processError(error, new Date(), data.executionMode, executionId, additionalData.hooks); + await this.processError( + error, + new Date(), + data.executionMode, + executionId, + additionalData.hooks, + ); throw error; } @@ -242,8 +324,12 @@ export class WorkflowRunner { return executionId; } - async runBull(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean, realtime?: boolean, restartExecutionId?: string): Promise { - + async runBull( + data: IWorkflowExecutionDataProcess, + loadStaticData?: boolean, + realtime?: boolean, + restartExecutionId?: string, + ): Promise { // TODO: If "loadStaticData" is set to true it has to load data new on worker // Register the active execution @@ -271,9 +357,14 @@ export class WorkflowRunner { try { job = await this.jobQueue.add(jobData, jobOptions); - console.log('Started with ID: ' + job.id.toString()); + console.log(`Started with ID: ${job.id.toString()}`); - hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerMain(data.executionMode, executionId, data.workflowData, { retryOf: data.retryOf ? data.retryOf.toString() : undefined }); + hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerMain( + data.executionMode, + executionId, + data.workflowData, + { retryOf: data.retryOf ? data.retryOf.toString() : undefined }, + ); // Normally also workflow should be supplied here but as it only used for sending // data to editor-UI is not needed. @@ -281,130 +372,154 @@ export class WorkflowRunner { } catch (error) { // We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the // "workflowExecuteAfter" which we require. - const hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(data.executionMode, executionId, data.workflowData, { retryOf: data.retryOf ? data.retryOf.toString() : undefined }); + const hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter( + data.executionMode, + executionId, + data.workflowData, + { retryOf: data.retryOf ? data.retryOf.toString() : undefined }, + ); await this.processError(error, new Date(), data.executionMode, executionId, hooks); throw error; } - const workflowExecution: PCancelable = new PCancelable(async (resolve, reject, onCancel) => { - onCancel.shouldReject = false; - onCancel(async () => { - await Queue.getInstance().stopJob(job); + const workflowExecution: PCancelable = new PCancelable( + async (resolve, reject, onCancel) => { + onCancel.shouldReject = false; + onCancel(async () => { + await Queue.getInstance().stopJob(job); - // We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the - // "workflowExecuteAfter" which we require. - const hooksWorker = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(data.executionMode, executionId, data.workflowData, { retryOf: data.retryOf ? data.retryOf.toString() : undefined }); + // We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the + // "workflowExecuteAfter" which we require. + const hooksWorker = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter( + data.executionMode, + executionId, + data.workflowData, + { retryOf: data.retryOf ? data.retryOf.toString() : undefined }, + ); - const error = new WorkflowOperationError('Workflow-Execution has been canceled!'); - await this.processError(error, new Date(), data.executionMode, executionId, hooksWorker); + const error = new WorkflowOperationError('Workflow-Execution has been canceled!'); + await this.processError(error, new Date(), data.executionMode, executionId, hooksWorker); - reject(error); - }); - - const jobData: Promise = job.finished(); - - const queueRecoveryInterval = config.get('queue.bull.queueRecoveryInterval') as number; - - const racingPromises: Array> = [jobData]; - - let clearWatchdogInterval; - if (queueRecoveryInterval > 0) { - /************************************************* - * Long explanation about what this solves: * - * This only happens in a very specific scenario * - * when Redis crashes and recovers shortly * - * but during this time, some execution(s) * - * finished. The end result is that the main * - * process will wait indefinitively and never * - * get a response. This adds an active polling to* - * the queue that allows us to identify that the * - * execution finished and get information from * - * the database. * - *************************************************/ - let watchDogInterval: NodeJS.Timeout | undefined; - - const watchDog: Promise = new Promise((res) => { - watchDogInterval = setInterval(async () => { - const currentJob = await this.jobQueue.getJob(job.id); - // When null means job is finished (not found in queue) - if (currentJob === null) { - // Mimic worker's success message - res({success: true}); - } - }, queueRecoveryInterval * 1000); + reject(error); }); - racingPromises.push(watchDog); + const jobData: Promise = job.finished(); - clearWatchdogInterval = () => { - if (watchDogInterval) { - clearInterval(watchDogInterval); - watchDogInterval = undefined; + const queueRecoveryInterval = config.get('queue.bull.queueRecoveryInterval') as number; + + const racingPromises: Array> = [jobData]; + + let clearWatchdogInterval; + if (queueRecoveryInterval > 0) { + /** *********************************************** + * Long explanation about what this solves: * + * This only happens in a very specific scenario * + * when Redis crashes and recovers shortly * + * but during this time, some execution(s) * + * finished. The end result is that the main * + * process will wait indefinitively and never * + * get a response. This adds an active polling to* + * the queue that allows us to identify that the * + * execution finished and get information from * + * the database. * + ************************************************ */ + let watchDogInterval: NodeJS.Timeout | undefined; + + const watchDog: Promise = new Promise((res) => { + watchDogInterval = setInterval(async () => { + const currentJob = await this.jobQueue.getJob(job.id); + // When null means job is finished (not found in queue) + if (currentJob === null) { + // Mimic worker's success message + res({ success: true }); + } + }, queueRecoveryInterval * 1000); + }); + + racingPromises.push(watchDog); + + clearWatchdogInterval = () => { + if (watchDogInterval) { + clearInterval(watchDogInterval); + watchDogInterval = undefined; + } + }; + } + + try { + await Promise.race(racingPromises); + if (clearWatchdogInterval !== undefined) { + clearWatchdogInterval(); } - }; - } + } catch (error) { + // We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the + // "workflowExecuteAfter" which we require. + const hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter( + data.executionMode, + executionId, + data.workflowData, + { retryOf: data.retryOf ? data.retryOf.toString() : undefined }, + ); + Logger.error(`Problem with execution ${executionId}: ${error.message}. Aborting.`); + if (clearWatchdogInterval !== undefined) { + clearWatchdogInterval(); + } + await this.processError(error, new Date(), data.executionMode, executionId, hooks); - try { - await Promise.race(racingPromises); - if (clearWatchdogInterval !== undefined) { - clearWatchdogInterval(); - } - } catch (error) { - // We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the - // "workflowExecuteAfter" which we require. - const hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(data.executionMode, executionId, data.workflowData, { retryOf: data.retryOf ? data.retryOf.toString() : undefined }); - Logger.error(`Problem with execution ${executionId}: ${error.message}. Aborting.`); - if (clearWatchdogInterval !== undefined) { - clearWatchdogInterval(); - } - await this.processError(error, new Date(), data.executionMode, executionId, hooks); - - reject(error); - } - - const executionDb = await Db.collections.Execution!.findOne(executionId) as IExecutionFlattedDb; - const fullExecutionData = ResponseHelper.unflattenExecutionData(executionDb) as IExecutionResponse; - const runData = { - data: fullExecutionData.data, - finished: fullExecutionData.finished, - mode: fullExecutionData.mode, - startedAt: fullExecutionData.startedAt, - stoppedAt: fullExecutionData.stoppedAt, - } as IRun; - - this.activeExecutions.remove(executionId, runData); - // Normally also static data should be supplied here but as it only used for sending - // data to editor-UI is not needed. - hooks.executeHookFunctions('workflowExecuteAfter', [runData]); - try { - // Check if this execution data has to be removed from database - // based on workflow settings. - let saveDataErrorExecution = config.get('executions.saveDataOnError') as string; - let saveDataSuccessExecution = config.get('executions.saveDataOnSuccess') as string; - if (data.workflowData.settings !== undefined) { - saveDataErrorExecution = (data.workflowData.settings.saveDataErrorExecution as string) || saveDataErrorExecution; - saveDataSuccessExecution = (data.workflowData.settings.saveDataSuccessExecution as string) || saveDataSuccessExecution; + reject(error); } - const workflowDidSucceed = !runData.data.resultData.error; - if (workflowDidSucceed === true && saveDataSuccessExecution === 'none' || - workflowDidSucceed === false && saveDataErrorExecution === 'none' - ) { - await Db.collections.Execution!.delete(executionId); - } - } catch (err) { - // We don't want errors here to crash n8n. Just log and proceed. - console.log('Error removing saved execution from database. More details: ', err); - } + const executionDb = (await Db.collections.Execution!.findOne( + executionId, + )) as IExecutionFlattedDb; + const fullExecutionData = ResponseHelper.unflattenExecutionData(executionDb); + const runData = { + data: fullExecutionData.data, + finished: fullExecutionData.finished, + mode: fullExecutionData.mode, + startedAt: fullExecutionData.startedAt, + stoppedAt: fullExecutionData.stoppedAt, + } as IRun; - resolve(runData); - }); + this.activeExecutions.remove(executionId, runData); + // Normally also static data should be supplied here but as it only used for sending + // data to editor-UI is not needed. + hooks.executeHookFunctions('workflowExecuteAfter', [runData]); + try { + // Check if this execution data has to be removed from database + // based on workflow settings. + let saveDataErrorExecution = config.get('executions.saveDataOnError') as string; + let saveDataSuccessExecution = config.get('executions.saveDataOnSuccess') as string; + if (data.workflowData.settings !== undefined) { + saveDataErrorExecution = + (data.workflowData.settings.saveDataErrorExecution as string) || + saveDataErrorExecution; + saveDataSuccessExecution = + (data.workflowData.settings.saveDataSuccessExecution as string) || + saveDataSuccessExecution; + } + + const workflowDidSucceed = !runData.data.resultData.error; + if ( + (workflowDidSucceed && saveDataSuccessExecution === 'none') || + (!workflowDidSucceed && saveDataErrorExecution === 'none') + ) { + await Db.collections.Execution!.delete(executionId); + } + // eslint-disable-next-line id-denylist + } catch (err) { + // We don't want errors here to crash n8n. Just log and proceed. + console.log('Error removing saved execution from database. More details: ', err); + } + + resolve(runData); + }, + ); this.activeExecutions.attachWorkflowExecution(executionId, workflowExecution); return executionId; } - /** * Run the workflow * @@ -414,12 +529,18 @@ export class WorkflowRunner { * @returns {Promise} * @memberof WorkflowRunner */ - async runSubprocess(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean, restartExecutionId?: string): Promise { + async runSubprocess( + data: IWorkflowExecutionDataProcess, + loadStaticData?: boolean, + restartExecutionId?: string, + ): Promise { let startedAt = new Date(); const subprocess = fork(pathJoin(__dirname, 'WorkflowRunnerProcess.js')); if (loadStaticData === true && data.workflowData.id) { - data.workflowData.staticData = await WorkflowHelpers.getStaticDataById(data.workflowData.id as string); + data.workflowData.staticData = await WorkflowHelpers.getStaticDataById( + data.workflowData.id as string, + ); } // Register the active execution @@ -437,8 +558,9 @@ export class WorkflowRunner { } let nodeTypeData: ITransferNodeTypes; let credentialTypeData: ICredentialsTypeData; + // eslint-disable-next-line prefer-destructuring let credentialsOverwrites = this.credentialsOverwrites; - if (loadAllNodeTypes === true) { + if (loadAllNodeTypes) { // Supply all nodeTypes and credentialTypes nodeTypeData = WorkflowHelpers.getAllNodeTypeData(); const credentialTypes = CredentialTypes(); @@ -458,8 +580,10 @@ export class WorkflowRunner { (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).credentialsTypeData = credentialTypeData; + (data as unknown as IWorkflowExecutionDataProcessWithExecution).credentialsOverwrite = + this.credentialsOverwrites; + (data as unknown as IWorkflowExecutionDataProcessWithExecution).credentialsTypeData = + credentialTypeData; const workflowHooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId); @@ -475,7 +599,7 @@ export class WorkflowRunner { let executionTimeout: NodeJS.Timeout; let workflowTimeout = config.get('executions.timeout') as number; // initialize with default if (data.workflowData.settings && data.workflowData.settings.executionTimeout) { - workflowTimeout = data.workflowData.settings!.executionTimeout as number; // preference on workflow setting + workflowTimeout = data.workflowData.settings.executionTimeout as number; // preference on workflow setting } const processTimeoutFunction = (timeout: number) => { @@ -484,11 +608,16 @@ export class WorkflowRunner { }; if (workflowTimeout > 0) { - workflowTimeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number) * 1000; // as seconds + workflowTimeout = + Math.min(workflowTimeout, config.get('executions.maxTimeout') as number) * 1000; // as seconds // Start timeout already now but give process at least 5 seconds to start. // Without it could would it be possible that the workflow executions times out before it even got started if // the timeout time is very short as the process start time can be quite long. - executionTimeout = setTimeout(processTimeoutFunction, Math.max(5000, workflowTimeout), workflowTimeout); + executionTimeout = setTimeout( + processTimeoutFunction, + Math.max(5000, workflowTimeout), + workflowTimeout, + ); } // Create a list of child spawned executions @@ -498,7 +627,10 @@ export class WorkflowRunner { // Listen to data from the subprocess subprocess.on('message', async (message: IProcessMessage) => { - Logger.debug(`Received child process message of type ${message.type} for execution ID ${executionId}.`, {executionId}); + Logger.debug( + `Received child process message of type ${message.type} for execution ID ${executionId}.`, + { executionId }, + ); if (message.type === 'start') { // Now that the execution actually started set the timeout again so that does not time out to early. startedAt = new Date(); @@ -506,18 +638,25 @@ export class WorkflowRunner { clearTimeout(executionTimeout); executionTimeout = setTimeout(processTimeoutFunction, workflowTimeout, workflowTimeout); } - } else if (message.type === 'end') { clearTimeout(executionTimeout); - this.activeExecutions.remove(executionId!, message.data.runData); - + this.activeExecutions.remove(executionId, message.data.runData); } else if (message.type === 'sendMessageToUI') { - WorkflowExecuteAdditionalData.sendMessageToUI.bind({ sessionId: data.sessionId })(message.data.source, message.data.message); - + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + WorkflowExecuteAdditionalData.sendMessageToUI.bind({ sessionId: data.sessionId })( + message.data.source, + message.data.message, + ); } else if (message.type === 'processError') { clearTimeout(executionTimeout); const executionError = message.data.executionError as ExecutionError; - await this.processError(executionError, startedAt, data.executionMode, executionId, workflowHooks); + await this.processError( + executionError, + startedAt, + data.executionMode, + executionId, + workflowHooks, + ); } else if (message.type === 'processHook') { this.processHookMessage(workflowHooks, message.data as IProcessMessageDataHook); } else if (message.type === 'timeout') { @@ -529,43 +668,61 @@ export class WorkflowRunner { } else if (message.type === 'startExecution') { const executionId = await this.activeExecutions.add(message.data.runData); childExecutionIds.push(executionId); - subprocess.send({ type: 'executionId', data: {executionId} } as IProcessMessage); + subprocess.send({ type: 'executionId', data: { executionId } } as IProcessMessage); } else if (message.type === 'finishExecution') { const executionIdIndex = childExecutionIds.indexOf(message.data.executionId); if (executionIdIndex !== -1) { childExecutionIds.splice(executionIdIndex, 1); } + // eslint-disable-next-line @typescript-eslint/await-thenable await this.activeExecutions.remove(message.data.executionId, message.data.result); } }); // Also get informed when the processes does exit especially when it did crash or timed out subprocess.on('exit', async (code, signal) => { - if (signal === 'SIGTERM'){ - Logger.debug(`Subprocess for execution ID ${executionId} timed out.`, {executionId}); + if (signal === 'SIGTERM') { + Logger.debug(`Subprocess for execution ID ${executionId} timed out.`, { executionId }); // Execution timed out and its process has been terminated const timeoutError = new WorkflowOperationError('Workflow execution timed out!'); - await this.processError(timeoutError, startedAt, data.executionMode, executionId, workflowHooks); + await this.processError( + timeoutError, + startedAt, + data.executionMode, + executionId, + workflowHooks, + ); } else if (code !== 0) { - Logger.debug(`Subprocess for execution ID ${executionId} finished with error code ${code}.`, {executionId}); + Logger.debug( + `Subprocess for execution ID ${executionId} finished with error code ${code}.`, + { executionId }, + ); // Process did exit with error code, so something went wrong. - const executionError = new WorkflowOperationError('Workflow execution process did crash for an unknown reason!'); + const executionError = new WorkflowOperationError( + 'Workflow execution process did crash for an unknown reason!', + ); - await this.processError(executionError, startedAt, data.executionMode, executionId, workflowHooks); + await this.processError( + executionError, + startedAt, + data.executionMode, + executionId, + workflowHooks, + ); } - for(const executionId of childExecutionIds) { + for (const executionId of childExecutionIds) { // When the child process exits, if we still have // pending child executions, we mark them as finished // They will display as unknown to the user // Instead of pending forever as executing when it // actually isn't anymore. + // eslint-disable-next-line @typescript-eslint/await-thenable, no-await-in-loop await this.activeExecutions.remove(executionId); } - clearTimeout(executionTimeout); }); diff --git a/packages/cli/src/WorkflowRunnerProcess.ts b/packages/cli/src/WorkflowRunnerProcess.ts index 6b3262322b..731854572e 100644 --- a/packages/cli/src/WorkflowRunnerProcess.ts +++ b/packages/cli/src/WorkflowRunnerProcess.ts @@ -1,20 +1,11 @@ - -import { - CredentialsOverwrites, - CredentialTypes, - Db, - ExternalHooks, - IWorkflowExecuteProcess, - IWorkflowExecutionDataProcessWithExecution, - NodeTypes, - WorkflowExecuteAdditionalData, - WorkflowHelpers, -} from './'; - -import { - IProcessMessage, - WorkflowExecute, -} from 'n8n-core'; +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable consistent-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-shadow */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/no-use-before-define */ +/* eslint-disable @typescript-eslint/unbound-method */ +import { IProcessMessage, WorkflowExecute } from 'n8n-core'; import { ExecutionError, @@ -34,24 +25,41 @@ import { WorkflowHooks, WorkflowOperationError, } from 'n8n-workflow'; - import { - getLogger, -} from '../src/Logger'; + CredentialsOverwrites, + CredentialTypes, + Db, + ExternalHooks, + IWorkflowExecuteProcess, + IWorkflowExecutionDataProcessWithExecution, + NodeTypes, + WorkflowExecuteAdditionalData, + WorkflowHelpers, +} from '.'; + +import { getLogger } from './Logger'; import * as config from '../config'; export class WorkflowRunnerProcess { data: IWorkflowExecutionDataProcessWithExecution | undefined; + logger: ILogger; + startedAt = new Date(); + workflow: Workflow | undefined; + workflowExecute: WorkflowExecute | undefined; + + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type executionIdCallback: (executionId: string) => void | undefined; + childExecutions: { - [key: string]: IWorkflowExecuteProcess, + [key: string]: IWorkflowExecuteProcess; } = {}; + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types static async stopProcess() { setTimeout(() => { // Attempt a graceful shutdown, giving executions 30 seconds to finish @@ -59,17 +67,20 @@ export class WorkflowRunnerProcess { }, 30000); } - async runWorkflow(inputData: IWorkflowExecutionDataProcessWithExecution): Promise { process.on('SIGTERM', WorkflowRunnerProcess.stopProcess); process.on('SIGINT', WorkflowRunnerProcess.stopProcess); - const logger = this.logger = getLogger(); + // eslint-disable-next-line no-multi-assign + const logger = (this.logger = getLogger()); LoggerProxy.init(logger); this.data = inputData; - logger.verbose('Initializing n8n sub-process', { pid: process.pid, workflowId: this.data.workflowData.id }); + logger.verbose('Initializing n8n sub-process', { + pid: process.pid, + workflowId: this.data.workflowData.id, + }); let className: string; let tempNode: INodeType; @@ -78,13 +89,16 @@ export class WorkflowRunnerProcess { this.startedAt = new Date(); const nodeTypesData: INodeTypeData = {}; + // eslint-disable-next-line no-restricted-syntax for (const nodeTypeName of Object.keys(this.data.nodeTypeData)) { className = this.data.nodeTypeData[nodeTypeName].className; filePath = this.data.nodeTypeData[nodeTypeName].sourcePath; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, import/no-dynamic-require, global-require, @typescript-eslint/no-var-requires const tempModule = require(filePath); try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access tempNode = new tempModule[className]() as INodeType; } catch (error) { throw new Error(`Error loading node "${nodeTypeName}" from: "${filePath}"`); @@ -115,7 +129,8 @@ export class WorkflowRunnerProcess { // We check if any node uses credentials. If it does, then // init database. let shouldInitializaDb = false; - inputData.workflowData.nodes.map(node => { + // eslint-disable-next-line array-callback-return + inputData.workflowData.nodes.map((node) => { if (Object.keys(node.credentials === undefined ? {} : node.credentials).length > 0) { shouldInitializaDb = true; } @@ -126,45 +141,77 @@ export class WorkflowRunnerProcess { if (shouldInitializaDb) { // initialize db as we need to load credentials await Db.init(); - } else if (inputData.workflowData.settings !== undefined && inputData.workflowData.settings.saveExecutionProgress === true) { + } else if ( + inputData.workflowData.settings !== undefined && + inputData.workflowData.settings.saveExecutionProgress === true + ) { // Workflow settings specifying it should save await Db.init(); - } else if (inputData.workflowData.settings !== undefined && inputData.workflowData.settings.saveExecutionProgress !== false && config.get('executions.saveExecutionProgress') as boolean) { + } else if ( + inputData.workflowData.settings !== undefined && + inputData.workflowData.settings.saveExecutionProgress !== false && + (config.get('executions.saveExecutionProgress') as boolean) + ) { // Workflow settings not saying anything about saving but default settings says so await Db.init(); - } else if (inputData.workflowData.settings === undefined && config.get('executions.saveExecutionProgress') as boolean) { + } else if ( + inputData.workflowData.settings === undefined && + (config.get('executions.saveExecutionProgress') as boolean) + ) { // Workflow settings not saying anything about saving but default settings says so await Db.init(); } // Start timeout for the execution let workflowTimeout = config.get('executions.timeout') as number; // initialize with default + // eslint-disable-next-line @typescript-eslint/prefer-optional-chain if (this.data.workflowData.settings && this.data.workflowData.settings.executionTimeout) { - workflowTimeout = this.data.workflowData.settings!.executionTimeout as number; // preference on workflow setting + workflowTimeout = this.data.workflowData.settings.executionTimeout as number; // preference on workflow setting } if (workflowTimeout > 0) { workflowTimeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number); } - 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(undefined, workflowTimeout <= 0 ? undefined : Date.now() + workflowTimeout * 1000); + 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( + undefined, + workflowTimeout <= 0 ? undefined : Date.now() + workflowTimeout * 1000, + ); additionalData.hooks = this.getProcessForwardHooks(); additionalData.executionId = inputData.executionId; - additionalData.sendMessageToUI = async (source: string, message: any) => { // tslint:disable-line:no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any + additionalData.sendMessageToUI = async (source: string, message: any) => { if (workflowRunner.data!.executionMode !== 'manual') { return; } try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment await sendToParentProcess('sendMessageToUI', { source, message }); } catch (error) { - this.logger.error(`There was a problem sending UI data to parent process: "${error.message}"`); + this.logger.error( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access + `There was a problem sending UI data to parent process: "${error.message}"`, + ); } }; const executeWorkflowFunction = additionalData.executeWorkflow; - additionalData.executeWorkflow = async (workflowInfo: IExecuteWorkflowInfo, additionalData: IWorkflowExecuteAdditionalData, inputData?: INodeExecutionData[] | undefined): Promise | IRun> => { + additionalData.executeWorkflow = async ( + workflowInfo: IExecuteWorkflowInfo, + additionalData: IWorkflowExecuteAdditionalData, + inputData?: INodeExecutionData[] | undefined, + ): Promise | IRun> => { const workflowData = await WorkflowExecuteAdditionalData.getWorkflowData(workflowInfo); const runData = await WorkflowExecuteAdditionalData.getRunData(workflowData, inputData); await sendToParentProcess('startExecution', { runData }); @@ -175,11 +222,18 @@ export class WorkflowRunnerProcess { }); let result: IRun; try { - const executeWorkflowFunctionOutput = await executeWorkflowFunction(workflowInfo, additionalData, inputData, executionId, workflowData, runData) as {workflowExecute: WorkflowExecute, workflow: Workflow} as IWorkflowExecuteProcess; - const workflowExecute = executeWorkflowFunctionOutput.workflowExecute; + const executeWorkflowFunctionOutput = (await executeWorkflowFunction( + workflowInfo, + additionalData, + inputData, + executionId, + workflowData, + runData, + )) as { workflowExecute: WorkflowExecute; workflow: Workflow } as IWorkflowExecuteProcess; + const { workflowExecute } = executeWorkflowFunctionOutput; this.childExecutions[executionId] = executeWorkflowFunctionOutput; - const workflow = executeWorkflowFunctionOutput.workflow; - result = await workflowExecute.processRunExecutionData(workflow) as IRun; + const { workflow } = executeWorkflowFunctionOutput; + result = await workflowExecute.processRunExecutionData(workflow); await externalHooks.run('workflow.postExecute', [result, workflowData]); await sendToParentProcess('finishExecution', { executionId, result }); delete this.childExecutions[executionId]; @@ -197,22 +251,35 @@ export class WorkflowRunnerProcess { }; if (this.data.executionData !== undefined) { - this.workflowExecute = new WorkflowExecute(additionalData, this.data.executionMode, this.data.executionData); + this.workflowExecute = new WorkflowExecute( + additionalData, + this.data.executionMode, + this.data.executionData, + ); return this.workflowExecute.processRunExecutionData(this.workflow); - } else if (this.data.runData === undefined || this.data.startNodes === undefined || this.data.startNodes.length === 0 || this.data.destinationNode === undefined) { + } + if ( + this.data.runData === undefined || + this.data.startNodes === undefined || + this.data.startNodes.length === 0 || + this.data.destinationNode === undefined + ) { // Execute all nodes // Can execute without webhook so go on this.workflowExecute = new WorkflowExecute(additionalData, this.data.executionMode); return this.workflowExecute.run(this.workflow, undefined, this.data.destinationNode); - } else { - // Execute only the nodes between start and destination nodes - this.workflowExecute = new WorkflowExecute(additionalData, this.data.executionMode); - return this.workflowExecute.runPartialWorkflow(this.workflow, this.data.runData, this.data.startNodes, this.data.destinationNode); } + // Execute only the nodes between start and destination nodes + this.workflowExecute = new WorkflowExecute(additionalData, this.data.executionMode); + return this.workflowExecute.runPartialWorkflow( + this.workflow, + this.data.runData, + this.data.startNodes, + this.data.destinationNode, + ); } - /** * Sends hook data to the parent process that it executes them * @@ -220,18 +287,18 @@ export class WorkflowRunnerProcess { * @param {any[]} parameters * @memberof WorkflowRunnerProcess */ - async sendHookToParentProcess(hook: string, parameters: any[]) { // tslint:disable-line:no-any + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any + async sendHookToParentProcess(hook: string, parameters: any[]) { try { await sendToParentProcess('processHook', { hook, parameters, }); } catch (error) { - this.logger.error(`There was a problem sending hook: "${hook}"`, { parameters, error}); + this.logger.error(`There was a problem sending hook: "${hook}"`, { parameters, error }); } } - /** * Create a wrapper for hooks which simply forwards the data to * the parent process where they then can be executed with access @@ -264,6 +331,7 @@ export class WorkflowRunnerProcess { }; const preExecuteFunctions = WorkflowExecuteAdditionalData.hookFunctionsPreExecute(); + // eslint-disable-next-line no-restricted-syntax for (const key of Object.keys(preExecuteFunctions)) { if (hookFunctions[key] === undefined) { hookFunctions[key] = []; @@ -271,13 +339,16 @@ export class WorkflowRunnerProcess { hookFunctions[key]!.push.apply(hookFunctions[key], preExecuteFunctions[key]); } - return new WorkflowHooks(hookFunctions, this.data!.executionMode, this.data!.executionId, this.data!.workflowData, { sessionId: this.data!.sessionId, retryOf: this.data!.retryOf as string }); + return new WorkflowHooks( + hookFunctions, + this.data!.executionMode, + this.data!.executionId, + this.data!.workflowData, + { sessionId: this.data!.sessionId, retryOf: this.data!.retryOf as string }, + ); } - } - - /** * Sends data to parent process * @@ -285,25 +356,27 @@ export class WorkflowRunnerProcess { * @param {*} data The data * @returns {Promise} */ -async function sendToParentProcess(type: string, data: any): Promise { // tslint:disable-line:no-any +// eslint-disable-next-line @typescript-eslint/no-explicit-any +async function sendToParentProcess(type: string, data: any): Promise { return new Promise((resolve, reject) => { - process.send!({ - type, - data, - }, (error: Error) => { - if (error) { - return reject(error); - } + process.send!( + { + type, + data, + }, + (error: Error) => { + if (error) { + return reject(error); + } - resolve(); - }); + resolve(); + }, + ); }); } - const workflowRunner = new WorkflowRunnerProcess(); - // Listen to messages from parent process which send the data of // the worflow to process process.on('message', async (message: IProcessMessage) => { @@ -324,25 +397,42 @@ process.on('message', async (message: IProcessMessage) => { let runData: IRun; if (workflowRunner.workflowExecute !== undefined) { - const executionIds = Object.keys(workflowRunner.childExecutions); + // eslint-disable-next-line no-restricted-syntax for (const executionId of executionIds) { const childWorkflowExecute = workflowRunner.childExecutions[executionId]; - runData = childWorkflowExecute.workflowExecute.getFullRunData(workflowRunner.childExecutions[executionId].startedAt); - const timeOutError = message.type === 'timeout' ? new WorkflowOperationError('Workflow execution timed out!') : new WorkflowOperationError('Workflow-Execution has been canceled!'); + runData = childWorkflowExecute.workflowExecute.getFullRunData( + workflowRunner.childExecutions[executionId].startedAt, + ); + const timeOutError = + message.type === 'timeout' + ? new WorkflowOperationError('Workflow execution timed out!') + : new WorkflowOperationError('Workflow-Execution has been canceled!'); // If there is any data send it to parent process, if execution timedout add the error - await childWorkflowExecute.workflowExecute.processSuccessExecution(workflowRunner.childExecutions[executionId].startedAt, childWorkflowExecute.workflow, timeOutError); + // eslint-disable-next-line no-await-in-loop + await childWorkflowExecute.workflowExecute.processSuccessExecution( + workflowRunner.childExecutions[executionId].startedAt, + childWorkflowExecute.workflow, + timeOutError, + ); } // Workflow started already executing runData = workflowRunner.workflowExecute.getFullRunData(workflowRunner.startedAt); - const timeOutError = message.type === 'timeout' ? new WorkflowOperationError('Workflow execution timed out!') : new WorkflowOperationError('Workflow-Execution has been canceled!'); + const timeOutError = + message.type === 'timeout' + ? new WorkflowOperationError('Workflow execution timed out!') + : new WorkflowOperationError('Workflow-Execution has been canceled!'); // If there is any data send it to parent process, if execution timedout add the error - await workflowRunner.workflowExecute.processSuccessExecution(workflowRunner.startedAt, workflowRunner.workflow!, timeOutError); + await workflowRunner.workflowExecute.processSuccessExecution( + workflowRunner.startedAt, + workflowRunner.workflow!, + timeOutError, + ); } else { // Workflow did not get started yet runData = { @@ -352,11 +442,14 @@ process.on('message', async (message: IProcessMessage) => { }, }, finished: false, - mode: workflowRunner.data ? workflowRunner.data!.executionMode : 'own' as WorkflowExecuteMode, + mode: workflowRunner.data + ? workflowRunner.data.executionMode + : ('own' as WorkflowExecuteMode), startedAt: workflowRunner.startedAt, stoppedAt: new Date(), }; + // eslint-disable-next-line @typescript-eslint/no-floating-promises workflowRunner.sendHookToParentProcess('workflowExecuteAfter', [runData]); } @@ -367,16 +460,16 @@ process.on('message', async (message: IProcessMessage) => { // Stop process process.exit(); } else if (message.type === 'executionId') { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access workflowRunner.executionIdCallback(message.data.executionId); } } catch (error) { - // Catch all uncaught errors and forward them to parent process const executionError = { ...error, - name: error!.name || 'Error', - message: error!.message, - stack: error!.stack, + name: error.name || 'Error', + message: error.message, + stack: error.stack, } as ExecutionError; await sendToParentProcess('processError', { diff --git a/packages/cli/src/databases/entities/CredentialsEntity.ts b/packages/cli/src/databases/entities/CredentialsEntity.ts index 5fd094e85b..5cf65fb0c3 100644 --- a/packages/cli/src/databases/entities/CredentialsEntity.ts +++ b/packages/cli/src/databases/entities/CredentialsEntity.ts @@ -1,15 +1,6 @@ -import { - ICredentialNodeAccess, -} from 'n8n-workflow'; - -import { - getTimestampSyntax, - resolveDataType -} from '../utils'; - -import { - ICredentialsDb, -} from '../..'; +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable import/no-cycle */ +import { ICredentialNodeAccess } from 'n8n-workflow'; import { BeforeUpdate, @@ -20,10 +11,12 @@ import { PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm'; +import { getTimestampSyntax, resolveDataType } from '../utils'; + +import { ICredentialsDb } from '../..'; @Entity() export class CredentialsEntity implements ICredentialsDb { - @PrimaryGeneratedColumn() id: number; @@ -47,7 +40,11 @@ export class CredentialsEntity implements ICredentialsDb { @CreateDateColumn({ precision: 3, default: () => getTimestampSyntax() }) createdAt: Date; - @UpdateDateColumn({ precision: 3, default: () => getTimestampSyntax(), onUpdate: getTimestampSyntax() }) + @UpdateDateColumn({ + precision: 3, + default: () => getTimestampSyntax(), + onUpdate: getTimestampSyntax(), + }) updatedAt: Date; @BeforeUpdate() diff --git a/packages/cli/src/databases/entities/ExecutionEntity.ts b/packages/cli/src/databases/entities/ExecutionEntity.ts index ba6b60807f..564ec9a4e3 100644 --- a/packages/cli/src/databases/entities/ExecutionEntity.ts +++ b/packages/cli/src/databases/entities/ExecutionEntity.ts @@ -1,27 +1,13 @@ -import { - WorkflowExecuteMode, -} from 'n8n-workflow'; +/* eslint-disable import/no-cycle */ +import { WorkflowExecuteMode } from 'n8n-workflow'; -import { - IExecutionFlattedDb, - IWorkflowDb, -} from '../../'; +import { Column, ColumnOptions, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; +import { IExecutionFlattedDb, IWorkflowDb } from '../..'; -import { - resolveDataType -} from '../utils'; - -import { - Column, - ColumnOptions, - Entity, - Index, - PrimaryGeneratedColumn, -} from 'typeorm'; +import { resolveDataType } from '../utils'; @Entity() export class ExecutionEntity implements IExecutionFlattedDb { - @PrimaryGeneratedColumn() id: number; diff --git a/packages/cli/src/databases/entities/TagEntity.ts b/packages/cli/src/databases/entities/TagEntity.ts index 45b438c9e3..445a104af9 100644 --- a/packages/cli/src/databases/entities/TagEntity.ts +++ b/packages/cli/src/databases/entities/TagEntity.ts @@ -1,4 +1,15 @@ -import { BeforeUpdate, Column, CreateDateColumn, Entity, Index, ManyToMany, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable import/no-cycle */ +import { + BeforeUpdate, + Column, + CreateDateColumn, + Entity, + Index, + ManyToMany, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; import { IsDate, IsOptional, IsString, Length } from 'class-validator'; import { ITagDb } from '../../Interfaces'; @@ -7,7 +18,6 @@ import { getTimestampSyntax } from '../utils'; @Entity() export class TagEntity implements ITagDb { - @PrimaryGeneratedColumn() id: number; @@ -22,12 +32,16 @@ export class TagEntity implements ITagDb { @IsDate() createdAt: Date; - @UpdateDateColumn({ precision: 3, default: () => getTimestampSyntax(), onUpdate: getTimestampSyntax() }) + @UpdateDateColumn({ + precision: 3, + default: () => getTimestampSyntax(), + onUpdate: getTimestampSyntax(), + }) @IsOptional() // ignored by validation because set at DB level @IsDate() updatedAt: Date; - @ManyToMany(() => WorkflowEntity, workflow => workflow.tags) + @ManyToMany(() => WorkflowEntity, (workflow) => workflow.tags) workflows: WorkflowEntity[]; @BeforeUpdate() diff --git a/packages/cli/src/databases/entities/WebhookEntity.ts b/packages/cli/src/databases/entities/WebhookEntity.ts index 8045880127..60afd83f9c 100644 --- a/packages/cli/src/databases/entities/WebhookEntity.ts +++ b/packages/cli/src/databases/entities/WebhookEntity.ts @@ -1,18 +1,11 @@ -import { - Column, - Entity, - Index, - PrimaryColumn, -} from 'typeorm'; +import { Column, Entity, Index, PrimaryColumn } from 'typeorm'; -import { - IWebhookDb, - } from '../../Interfaces'; +// eslint-disable-next-line import/no-cycle +import { IWebhookDb } from '../../Interfaces'; @Entity() @Index(['webhookId', 'method', 'pathLength']) export class WebhookEntity implements IWebhookDb { - @Column() workflowId: number; diff --git a/packages/cli/src/databases/entities/WorkflowEntity.ts b/packages/cli/src/databases/entities/WorkflowEntity.ts index ac7d294ef5..88eb44d7ff 100644 --- a/packages/cli/src/databases/entities/WorkflowEntity.ts +++ b/packages/cli/src/databases/entities/WorkflowEntity.ts @@ -1,13 +1,8 @@ -import { - Length, -} from 'class-validator'; +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable import/no-cycle */ +import { Length } from 'class-validator'; -import { - IConnections, - IDataObject, - INode, - IWorkflowSettings, -} from 'n8n-workflow'; +import { IConnections, IDataObject, INode, IWorkflowSettings } from 'n8n-workflow'; import { BeforeUpdate, @@ -22,22 +17,14 @@ import { UpdateDateColumn, } from 'typeorm'; -import { - IWorkflowDb, -} from '../../'; +import { IWorkflowDb } from '../..'; -import { - getTimestampSyntax, - resolveDataType -} from '../utils'; +import { getTimestampSyntax, resolveDataType } from '../utils'; -import { - TagEntity, -} from './TagEntity'; +import { TagEntity } from './TagEntity'; @Entity() export class WorkflowEntity implements IWorkflowDb { - @PrimaryGeneratedColumn() id: number; @@ -58,7 +45,11 @@ export class WorkflowEntity implements IWorkflowDb { @CreateDateColumn({ precision: 3, default: () => getTimestampSyntax() }) createdAt: Date; - @UpdateDateColumn({ precision: 3, default: () => getTimestampSyntax(), onUpdate: getTimestampSyntax() }) + @UpdateDateColumn({ + precision: 3, + default: () => getTimestampSyntax(), + onUpdate: getTimestampSyntax(), + }) updatedAt: Date; @Column({ @@ -73,16 +64,16 @@ export class WorkflowEntity implements IWorkflowDb { }) staticData?: IDataObject; - @ManyToMany(() => TagEntity, tag => tag.workflows) + @ManyToMany(() => TagEntity, (tag) => tag.workflows) @JoinTable({ - name: "workflows_tags", // table name for the junction table of this relation + name: 'workflows_tags', // table name for the junction table of this relation joinColumn: { - name: "workflowId", - referencedColumnName: "id", + name: 'workflowId', + referencedColumnName: 'id', }, inverseJoinColumn: { - name: "tagId", - referencedColumnName: "id", + name: 'tagId', + referencedColumnName: 'id', }, }) tags: TagEntity[]; diff --git a/packages/cli/src/databases/entities/index.ts b/packages/cli/src/databases/entities/index.ts index 925ec36d7b..4e980573e2 100644 --- a/packages/cli/src/databases/entities/index.ts +++ b/packages/cli/src/databases/entities/index.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable import/no-cycle */ import { CredentialsEntity } from './CredentialsEntity'; import { ExecutionEntity } from './ExecutionEntity'; import { WorkflowEntity } from './WorkflowEntity'; diff --git a/packages/cli/src/databases/utils.ts b/packages/cli/src/databases/utils.ts index 1816deea09..3a0e9b08c3 100644 --- a/packages/cli/src/databases/utils.ts +++ b/packages/cli/src/databases/utils.ts @@ -1,7 +1,6 @@ -import { - DatabaseType, -} from '../index'; -import { getConfigValueSync } from '../../src/GenericHelpers'; +/* eslint-disable import/no-cycle */ +import { DatabaseType } from '../index'; +import { getConfigValueSync } from '../GenericHelpers'; /** * Resolves the data type for the used database type @@ -10,6 +9,7 @@ import { getConfigValueSync } from '../../src/GenericHelpers'; * @param {string} dataType * @returns {string} */ +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export function resolveDataType(dataType: string) { const dbType = getConfigValueSync('database.type') as DatabaseType; @@ -27,16 +27,16 @@ export function resolveDataType(dataType: string) { return typeMap[dbType][dataType] ?? dataType; } +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export function getTimestampSyntax() { const dbType = getConfigValueSync('database.type') as DatabaseType; const map: { [key in DatabaseType]: string } = { sqlite: "STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')", - postgresdb: "CURRENT_TIMESTAMP(3)", - mysqldb: "CURRENT_TIMESTAMP(3)", - mariadb: "CURRENT_TIMESTAMP(3)", + postgresdb: 'CURRENT_TIMESTAMP(3)', + mysqldb: 'CURRENT_TIMESTAMP(3)', + mariadb: 'CURRENT_TIMESTAMP(3)', }; return map[dbType]; } - diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 296348cf65..abca07fb29 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,3 +1,5 @@ +/* eslint-disable import/first */ +/* eslint-disable import/no-cycle */ export * from './CredentialsHelper'; export * from './CredentialTypes'; export * from './CredentialsOverwrites'; @@ -22,6 +24,7 @@ import * as WebhookHelpers from './WebhookHelpers'; import * as WebhookServer from './WebhookServer'; import * as WorkflowExecuteAdditionalData from './WorkflowExecuteAdditionalData'; import * as WorkflowHelpers from './WorkflowHelpers'; + export { ActiveExecutions, ActiveWorkflowRunner, diff --git a/packages/cli/test/placeholder.test.ts b/packages/cli/test/placeholder.test.ts index 23ba06a64f..d3ea1f5dff 100644 --- a/packages/cli/test/placeholder.test.ts +++ b/packages/cli/test/placeholder.test.ts @@ -1,7 +1,5 @@ describe('Placeholder', () => { - test('example', () => { expect(1 + 1).toEqual(2); }); - }); diff --git a/packages/core/package.json b/packages/core/package.json index c9a239e5ef..67ad1fc3f5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -17,8 +17,9 @@ "scripts": { "build": "tsc", "dev": "npm run watch", - "tslint": "tslint -p tsconfig.json -c tslint.json", - "tslintfix": "tslint --fix -p tsconfig.json -c tslint.json", + "format": "cd ../.. && node_modules/prettier/bin-prettier.js packages/core/**/**.ts --write", + "lint": "cd ../.. && node_modules/eslint/bin/eslint.js packages/core", + "lintfix": "cd ../.. && node_modules/eslint/bin/eslint.js packages/core --fix", "watch": "tsc --watch", "test": "jest" }, @@ -38,7 +39,7 @@ "source-map-support": "^0.5.9", "ts-jest": "^26.3.0", "tslint": "^6.1.2", - "typescript": "~3.9.7" + "typescript": "~4.3.5" }, "dependencies": { "client-oauth2": "^4.2.5", diff --git a/packages/core/src/ActiveWebhooks.ts b/packages/core/src/ActiveWebhooks.ts index 95b7f9dc3c..fa0ee578c9 100644 --- a/packages/core/src/ActiveWebhooks.ts +++ b/packages/core/src/ActiveWebhooks.ts @@ -6,10 +6,8 @@ import { WorkflowExecuteMode, } from 'n8n-workflow'; -import { - NodeExecuteFunctions, -} from './'; - +// eslint-disable-next-line import/no-cycle +import { NodeExecuteFunctions } from '.'; export class ActiveWebhooks { private workflowWebhooks: { @@ -22,7 +20,6 @@ export class ActiveWebhooks { testWebhooks = false; - /** * Adds a new webhook * @@ -31,19 +28,31 @@ export class ActiveWebhooks { * @returns {Promise} * @memberof ActiveWebhooks */ - async add(workflow: Workflow, webhookData: IWebhookData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): Promise { + async add( + workflow: Workflow, + webhookData: IWebhookData, + mode: WorkflowExecuteMode, + activation: WorkflowActivateMode, + ): Promise { if (workflow.id === undefined) { throw new Error('Webhooks can only be added for saved workflows as an id is needed!'); } if (webhookData.path.endsWith('/')) { + // eslint-disable-next-line no-param-reassign webhookData.path = webhookData.path.slice(0, -1); } - const webhookKey = this.getWebhookKey(webhookData.httpMethod, webhookData.path, webhookData.webhookId); + const webhookKey = this.getWebhookKey( + webhookData.httpMethod, + webhookData.path, + webhookData.webhookId, + ); - //check that there is not a webhook already registed with that path/method + // check that there is not a webhook already registed with that path/method if (this.webhookUrls[webhookKey] && !webhookData.webhookId) { - throw new Error(`Test-Webhook can not be activated because another one with the same method "${webhookData.httpMethod}" and path "${webhookData.path}" is already active!`); + throw new Error( + `Test-Webhook can not be activated because another one with the same method "${webhookData.httpMethod}" and path "${webhookData.path}" is already active!`, + ); } if (this.workflowWebhooks[webhookData.workflowId] === undefined) { @@ -58,18 +67,33 @@ export class ActiveWebhooks { this.webhookUrls[webhookKey].push(webhookData); try { - const webhookExists = await workflow.runWebhookMethod('checkExists', webhookData, NodeExecuteFunctions, mode, activation, this.testWebhooks); + const webhookExists = await workflow.runWebhookMethod( + 'checkExists', + webhookData, + NodeExecuteFunctions, + mode, + activation, + this.testWebhooks, + ); if (webhookExists !== true) { // If webhook does not exist yet create it - await workflow.runWebhookMethod('create', webhookData, NodeExecuteFunctions, mode, activation, this.testWebhooks); - + await workflow.runWebhookMethod( + 'create', + webhookData, + NodeExecuteFunctions, + mode, + activation, + this.testWebhooks, + ); } } catch (error) { // If there was a problem unregister the webhook again if (this.webhookUrls[webhookKey].length <= 1) { delete this.webhookUrls[webhookKey]; } else { - this.webhookUrls[webhookKey] = this.webhookUrls[webhookKey].filter(webhook => webhook.path !== webhookData.path); + this.webhookUrls[webhookKey] = this.webhookUrls[webhookKey].filter( + (webhook) => webhook.path !== webhookData.path, + ); } throw error; @@ -77,7 +101,6 @@ export class ActiveWebhooks { this.workflowWebhooks[webhookData.workflowId].push(webhookData); } - /** * Returns webhookData if a webhook with matches is currently registered * @@ -98,9 +121,9 @@ export class ActiveWebhooks { const pathElementsSet = new Set(path.split('/')); // check if static elements match in path // if more results have been returned choose the one with the most static-route matches - this.webhookUrls[webhookKey].forEach(dynamicWebhook => { - const staticElements = dynamicWebhook.path.split('/').filter(ele => !ele.startsWith(':')); - const allStaticExist = staticElements.every(staticEle => pathElementsSet.has(staticEle)); + this.webhookUrls[webhookKey].forEach((dynamicWebhook) => { + const staticElements = dynamicWebhook.path.split('/').filter((ele) => !ele.startsWith(':')); + const allStaticExist = staticElements.every((staticEle) => pathElementsSet.has(staticEle)); if (allStaticExist && staticElements.length > maxMatches) { maxMatches = staticElements.length; @@ -120,13 +143,14 @@ export class ActiveWebhooks { * @param path */ getWebhookMethods(path: string): string[] { - const methods : string[] = []; + const methods: string[] = []; Object.keys(this.webhookUrls) - .filter(key => key.includes(path)) - .map(key => { - methods.push(key.split('|')[0]); - }); + .filter((key) => key.includes(path)) + // eslint-disable-next-line array-callback-return + .map((key) => { + methods.push(key.split('|')[0]); + }); return methods; } @@ -141,7 +165,6 @@ export class ActiveWebhooks { return Object.keys(this.workflowWebhooks); } - /** * Returns key to uniquely identify a webhook * @@ -155,6 +178,7 @@ export class ActiveWebhooks { if (webhookId) { if (path.startsWith(webhookId)) { const cutFromIndex = path.indexOf('/') + 1; + // eslint-disable-next-line no-param-reassign path = path.slice(cutFromIndex); } return `${httpMethod}|${webhookId}|${path.split('/').length}`; @@ -162,7 +186,6 @@ export class ActiveWebhooks { return `${httpMethod}|${path}`; } - /** * Removes all webhooks of a workflow * @@ -171,6 +194,7 @@ export class ActiveWebhooks { * @memberof ActiveWebhooks */ async removeWorkflow(workflow: Workflow): Promise { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const workflowId = workflow.id!.toString(); if (this.workflowWebhooks[workflowId] === undefined) { @@ -183,10 +207,21 @@ export class ActiveWebhooks { const mode = 'internal'; // Go through all the registered webhooks of the workflow and remove them + // eslint-disable-next-line no-restricted-syntax for (const webhookData of webhooks) { - await workflow.runWebhookMethod('delete', webhookData, NodeExecuteFunctions, mode, 'update', this.testWebhooks); + // eslint-disable-next-line no-await-in-loop + await workflow.runWebhookMethod( + 'delete', + webhookData, + NodeExecuteFunctions, + mode, + 'update', + this.testWebhooks, + ); - delete this.webhookUrls[this.getWebhookKey(webhookData.httpMethod, webhookData.path, webhookData.webhookId)]; + delete this.webhookUrls[ + this.getWebhookKey(webhookData.httpMethod, webhookData.path, webhookData.webhookId) + ]; } // Remove also the workflow-webhook entry @@ -195,18 +230,16 @@ export class ActiveWebhooks { return true; } - /** * Removes all the webhooks of the given workflows */ async removeAll(workflows: Workflow[]): Promise { const removePromises = []; + // eslint-disable-next-line no-restricted-syntax for (const workflow of workflows) { removePromises.push(this.removeWorkflow(workflow)); } await Promise.all(removePromises); - return; } - } diff --git a/packages/core/src/ActiveWorkflows.ts b/packages/core/src/ActiveWorkflows.ts index 7b2342b1c6..d68d3ea2da 100644 --- a/packages/core/src/ActiveWorkflows.ts +++ b/packages/core/src/ActiveWorkflows.ts @@ -1,3 +1,6 @@ +/* eslint-disable no-continue */ +/* eslint-disable no-await-in-loop */ +/* eslint-disable no-restricted-syntax */ import { CronJob } from 'cron'; import { @@ -13,18 +16,14 @@ import { WorkflowExecuteMode, } from 'n8n-workflow'; -import { - ITriggerTime, - IWorkflowData, -} from './'; - +// eslint-disable-next-line import/no-cycle +import { ITriggerTime, IWorkflowData } from '.'; export class ActiveWorkflows { private workflowData: { [key: string]: IWorkflowData; } = {}; - /** * Returns if the workflow is active * @@ -33,10 +32,10 @@ export class ActiveWorkflows { * @memberof ActiveWorkflows */ isActive(id: string): boolean { + // eslint-disable-next-line no-prototype-builtins return this.workflowData.hasOwnProperty(id); } - /** * Returns the ids of the currently active workflows * @@ -47,7 +46,6 @@ export class ActiveWorkflows { return Object.keys(this.workflowData); } - /** * Returns the Workflow data for the workflow with * the given id if it is currently active @@ -60,7 +58,6 @@ export class ActiveWorkflows { return this.workflowData[id]; } - /** * Makes a workflow active * @@ -70,16 +67,31 @@ export class ActiveWorkflows { * @returns {Promise} * @memberof ActiveWorkflows */ - async add(id: string, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode, getTriggerFunctions: IGetExecuteTriggerFunctions, getPollFunctions: IGetExecutePollFunctions): Promise { + async add( + id: string, + workflow: Workflow, + additionalData: IWorkflowExecuteAdditionalData, + mode: WorkflowExecuteMode, + activation: WorkflowActivateMode, + getTriggerFunctions: IGetExecuteTriggerFunctions, + getPollFunctions: IGetExecutePollFunctions, + ): Promise { this.workflowData[id] = {}; const triggerNodes = workflow.getTriggerNodes(); let triggerResponse: ITriggerResponse | undefined; this.workflowData[id].triggerResponses = []; for (const triggerNode of triggerNodes) { - triggerResponse = await workflow.runTrigger(triggerNode, getTriggerFunctions, additionalData, mode, activation); + triggerResponse = await workflow.runTrigger( + triggerNode, + getTriggerFunctions, + additionalData, + mode, + activation, + ); if (triggerResponse !== undefined) { // If a response was given save it + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this.workflowData[id].triggerResponses!.push(triggerResponse); } } @@ -88,12 +100,21 @@ export class ActiveWorkflows { if (pollNodes.length) { this.workflowData[id].pollResponses = []; for (const pollNode of pollNodes) { - this.workflowData[id].pollResponses!.push(await this.activatePolling(pollNode, workflow, additionalData, getPollFunctions, mode, activation)); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.workflowData[id].pollResponses!.push( + await this.activatePolling( + pollNode, + workflow, + additionalData, + getPollFunctions, + mode, + activation, + ), + ); } } } - /** * Activates polling for the given node * @@ -104,7 +125,14 @@ export class ActiveWorkflows { * @returns {Promise} * @memberof ActiveWorkflows */ - async activatePolling(node: INode, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, getPollFunctions: IGetExecutePollFunctions, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): Promise { + async activatePolling( + node: INode, + workflow: Workflow, + additionalData: IWorkflowExecuteAdditionalData, + getPollFunctions: IGetExecutePollFunctions, + mode: WorkflowExecuteMode, + activation: WorkflowActivateMode, + ): Promise { const pollFunctions = getPollFunctions(workflow, node, additionalData, mode, activation); const pollTimes = pollFunctions.getNodeParameter('pollTimes') as unknown as { @@ -113,12 +141,12 @@ export class ActiveWorkflows { // Define the order the cron-time-parameter appear const parameterOrder = [ - 'second', // 0 - 59 - 'minute', // 0 - 59 - 'hour', // 0 - 23 + 'second', // 0 - 59 + 'minute', // 0 - 59 + 'hour', // 0 - 23 'dayOfMonth', // 1 - 31 - 'month', // 0 - 11(Jan - Dec) - 'weekday', // 0 - 6(Sun - Sat) + 'month', // 0 - 11(Jan - Dec) + 'weekday', // 0 - 6(Sun - Sat) ]; // Get all the trigger times @@ -165,10 +193,15 @@ export class ActiveWorkflows { // The trigger function to execute when the cron-time got reached const executeTrigger = async () => { - Logger.info(`Polling trigger initiated for workflow "${workflow.name}"`, {workflowName: workflow.name, workflowId: workflow.id}); + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + Logger.info(`Polling trigger initiated for workflow "${workflow.name}"`, { + workflowName: workflow.name, + workflowId: workflow.id, + }); const pollResponse = await workflow.runPoll(node, pollFunctions); if (pollResponse !== null) { + // eslint-disable-next-line no-underscore-dangle pollFunctions.__emit(pollResponse); } }; @@ -180,6 +213,7 @@ export class ActiveWorkflows { // Start the cron-jobs const cronJobs: CronJob[] = []; + // eslint-disable-next-line @typescript-eslint/no-shadow for (const cronTime of cronTimes) { const cronTimeParts = cronTime.split(' '); if (cronTimeParts.length > 0 && cronTimeParts[0].includes('*')) { @@ -201,7 +235,6 @@ export class ActiveWorkflows { }; } - /** * Makes a workflow inactive * @@ -212,7 +245,9 @@ export class ActiveWorkflows { async remove(id: string): Promise { 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`); + throw new Error( + `The workflow with the id "${id}" is currently not active and can so not be removed`, + ); } const workflowData = this.workflowData[id]; @@ -235,5 +270,4 @@ export class ActiveWorkflows { delete this.workflowData[id]; } - } diff --git a/packages/core/src/Credentials.ts b/packages/core/src/Credentials.ts index 6f8f617694..2ba8b10634 100644 --- a/packages/core/src/Credentials.ts +++ b/packages/core/src/Credentials.ts @@ -7,16 +7,13 @@ import { import { AES, enc } from 'crypto-js'; - export class Credentials extends ICredentials { - - /** * Returns if the given nodeType has access to data */ hasNodeAccess(nodeType: string): boolean { + // eslint-disable-next-line no-restricted-syntax for (const accessData of this.nodesAccess) { - if (accessData.nodeType === nodeType) { return true; } @@ -25,7 +22,6 @@ export class Credentials extends ICredentials { return false; } - /** * Sets new credential object */ @@ -33,7 +29,6 @@ export class Credentials extends ICredentials { this.data = AES.encrypt(JSON.stringify(data), encryptionKey).toString(); } - /** * Sets new credentials for given key */ @@ -50,13 +45,14 @@ export class Credentials extends ICredentials { return this.setData(fullData, encryptionKey); } - /** * Returns the decrypted credential object */ getData(encryptionKey: string, nodeType?: string): ICredentialDataDecryptedObject { if (nodeType && !this.hasNodeAccess(nodeType)) { - throw new Error(`The node of type "${nodeType}" does not have access to credentials "${this.name}" of type "${this.type}".`); + throw new Error( + `The node of type "${nodeType}" does not have access to credentials "${this.name}" of type "${this.type}".`, + ); } if (this.data === undefined) { @@ -66,13 +62,15 @@ export class Credentials extends ICredentials { const decryptedData = AES.decrypt(this.data, encryptionKey); try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return JSON.parse(decryptedData.toString(enc.Utf8)); } catch (e) { - throw new Error('Credentials could not be decrypted. The likely reason is that a different "encryptionKey" was used to encrypt the data.'); + throw new Error( + 'Credentials could not be decrypted. The likely reason is that a different "encryptionKey" was used to encrypt the data.', + ); } } - /** * Returns the decrypted credentials for given key */ @@ -83,6 +81,7 @@ export class Credentials extends ICredentials { throw new Error(`No data was set.`); } + // eslint-disable-next-line no-prototype-builtins if (!fullData.hasOwnProperty(key)) { throw new Error(`No data for key "${key}" exists.`); } @@ -90,7 +89,6 @@ export class Credentials extends ICredentials { return fullData[key]; } - /** * Returns the encrypted credentials to be saved */ diff --git a/packages/core/src/DeferredPromise.ts b/packages/core/src/DeferredPromise.ts index 56d333ba82..0f2f9fa035 100644 --- a/packages/core/src/DeferredPromise.ts +++ b/packages/core/src/DeferredPromise.ts @@ -5,10 +5,10 @@ export interface IDeferredPromise { resolve: (result: T) => void; } -export function createDeferredPromise(): Promise> { - return new Promise>(resolveCreate => { +export async function createDeferredPromise(): Promise> { + return new Promise>((resolveCreate) => { const promise = new Promise((resolve, reject) => { - resolveCreate({ promise: () => promise, resolve, reject }); + resolveCreate({ promise: async () => promise, resolve, reject }); }); }); } diff --git a/packages/core/src/Interfaces.ts b/packages/core/src/Interfaces.ts index 52bd80f700..b9783c6a45 100644 --- a/packages/core/src/Interfaces.ts +++ b/packages/core/src/Interfaces.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { IAllExecuteFunctions, IBinaryData, @@ -16,70 +17,116 @@ import { ITriggerResponse, IWebhookFunctions as IWebhookFunctionsBase, IWorkflowSettings as IWorkflowSettingsWorkflow, - } from 'n8n-workflow'; +} from 'n8n-workflow'; import { OptionsWithUri, OptionsWithUrl } from 'request'; import * as requestPromise from 'request-promise-native'; interface Constructable { - new(): T; + new (): T; } export interface IProcessMessage { - data?: any; // tslint:disable-line:no-any + data?: any; type: string; } - export interface IExecuteFunctions extends IExecuteFunctionsBase { helpers: { - prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise; + prepareBinaryData( + binaryData: Buffer, + filePath?: string, + mimeType?: string, + ): Promise; getBinaryDataBuffer(itemIndex: number, propertyName: string): Promise; request: requestPromise.RequestPromiseAPI; - requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise; // tslint:disable-line:no-any - requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise; // tslint:disable-line:no-any + requestOAuth2( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, + oAuth2Options?: IOAuth2Options, + ): Promise; // tslint:disable-line:no-any + requestOAuth1( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions, + ): Promise; // tslint:disable-line:no-any returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; }; } - export interface IExecuteSingleFunctions extends IExecuteSingleFunctionsBase { helpers: { - prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise; - request: requestPromise.RequestPromiseAPI, - requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise, // tslint:disable-line:no-any - requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + prepareBinaryData( + binaryData: Buffer, + filePath?: string, + mimeType?: string, + ): Promise; + request: requestPromise.RequestPromiseAPI; + requestOAuth2( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, + oAuth2Options?: IOAuth2Options, + ): Promise; // tslint:disable-line:no-any + requestOAuth1( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions, + ): Promise; // tslint:disable-line:no-any }; } - export interface IPollFunctions extends IPollFunctionsBase { helpers: { - prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise; - request: requestPromise.RequestPromiseAPI, - requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise, // tslint:disable-line:no-any - requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + prepareBinaryData( + binaryData: Buffer, + filePath?: string, + mimeType?: string, + ): Promise; + request: requestPromise.RequestPromiseAPI; + requestOAuth2( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, + oAuth2Options?: IOAuth2Options, + ): Promise; // tslint:disable-line:no-any + requestOAuth1( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions, + ): Promise; // tslint:disable-line:no-any returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; }; } - export interface IResponseError extends Error { statusCode?: number; } - export interface ITriggerFunctions extends ITriggerFunctionsBase { helpers: { - prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise; - request: requestPromise.RequestPromiseAPI, - requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise, // tslint:disable-line:no-any - requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + prepareBinaryData( + binaryData: Buffer, + filePath?: string, + mimeType?: string, + ): Promise; + request: requestPromise.RequestPromiseAPI; + requestOAuth2( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, + oAuth2Options?: IOAuth2Options, + ): Promise; // tslint:disable-line:no-any + requestOAuth1( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions, + ): Promise; // tslint:disable-line:no-any returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; }; } - export interface ITriggerTime { mode: string; hour: number; @@ -89,7 +136,6 @@ export interface ITriggerTime { [key: string]: string | number; } - export interface IUserSettings { encryptionKey?: string; tunnelSubdomain?: string; @@ -97,28 +143,57 @@ export interface IUserSettings { export interface ILoadOptionsFunctions extends ILoadOptionsFunctionsBase { helpers: { - request?: requestPromise.RequestPromiseAPI, - requestOAuth2?: (this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options) => Promise, // tslint:disable-line:no-any - requestOAuth1?(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + request?: requestPromise.RequestPromiseAPI; + requestOAuth2?: ( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, + oAuth2Options?: IOAuth2Options, + ) => Promise; // tslint:disable-line:no-any + requestOAuth1?( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions, + ): Promise; // tslint:disable-line:no-any }; } - export interface IHookFunctions extends IHookFunctionsBase { helpers: { - request: requestPromise.RequestPromiseAPI, - requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise, // tslint:disable-line:no-any - requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + request: requestPromise.RequestPromiseAPI; + requestOAuth2( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, + oAuth2Options?: IOAuth2Options, + ): Promise; // tslint:disable-line:no-any + requestOAuth1( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions, + ): Promise; // tslint:disable-line:no-any }; } - export interface IWebhookFunctions extends IWebhookFunctionsBase { helpers: { - prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise; - request: requestPromise.RequestPromiseAPI, - requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise, // tslint:disable-line:no-any - requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + prepareBinaryData( + binaryData: Buffer, + filePath?: string, + mimeType?: string, + ): Promise; + request: requestPromise.RequestPromiseAPI; + requestOAuth2( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, + oAuth2Options?: IOAuth2Options, + ): Promise; // tslint:disable-line:no-any + requestOAuth1( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions, + ): Promise; // tslint:disable-line:no-any returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; }; } @@ -129,19 +204,16 @@ export interface IWorkflowSettings extends IWorkflowSettingsWorkflow { saveManualRuns?: boolean; } - // New node definition in file export interface INodeDefinitionFile { [key: string]: Constructable; } - // Is identical to TaskDataConnections but does not allow null value to be used as input for nodes export interface INodeInputDataConnections { [key: string]: INodeExecutionData[][]; } - export interface IWorkflowData { pollResponses?: IPollResponse[]; triggerResponses?: ITriggerResponse[]; diff --git a/packages/core/src/LoadNodeParameterOptions.ts b/packages/core/src/LoadNodeParameterOptions.ts index d112f63bfd..3ff1ad99d1 100644 --- a/packages/core/src/LoadNodeParameterOptions.ts +++ b/packages/core/src/LoadNodeParameterOptions.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ import { INode, INodeCredentials, @@ -8,21 +9,24 @@ import { Workflow, } from 'n8n-workflow'; -import { - NodeExecuteFunctions, -} from './'; - +// eslint-disable-next-line import/no-cycle +import { NodeExecuteFunctions } from '.'; const TEMP_NODE_NAME = 'Temp-Node'; const TEMP_WORKFLOW_NAME = 'Temp-Workflow'; - export class LoadNodeParameterOptions { path: string; + workflow: Workflow; - - constructor(nodeTypeName: string, nodeTypes: INodeTypes, path: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials) { + constructor( + nodeTypeName: string, + nodeTypes: INodeTypes, + path: string, + currentNodeParameters: INodeParameters, + credentials?: INodeCredentials, + ) { this.path = path; const nodeType = nodeTypes.getByName(nodeTypeName); @@ -35,10 +39,7 @@ export class LoadNodeParameterOptions { name: TEMP_NODE_NAME, type: nodeTypeName, typeVersion: 1, - position: [ - 0, - 0, - ], + position: [0, 0], }; if (credentials) { @@ -46,22 +47,25 @@ export class LoadNodeParameterOptions { } const workflowData = { - nodes: [ - nodeData, - ], + nodes: [nodeData], connections: {}, }; - this.workflow = new Workflow({ nodes: workflowData.nodes, connections: workflowData.connections, active: false, nodeTypes }); + this.workflow = new Workflow({ + nodes: workflowData.nodes, + connections: workflowData.connections, + active: false, + nodeTypes, + }); } - /** * Returns data of a fake workflow * * @returns * @memberof LoadNodeParameterOptions */ + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types getWorkflowData() { return { name: TEMP_WORKFLOW_NAME, @@ -73,7 +77,6 @@ export class LoadNodeParameterOptions { }; } - /** * Returns the available options * @@ -82,18 +85,31 @@ export class LoadNodeParameterOptions { * @returns {Promise} * @memberof LoadNodeParameterOptions */ - getOptions(methodName: string, additionalData: IWorkflowExecuteAdditionalData): Promise { + async getOptions( + methodName: string, + additionalData: IWorkflowExecuteAdditionalData, + ): Promise { const node = this.workflow.getNode(TEMP_NODE_NAME); const nodeType = this.workflow.nodeTypes.getByName(node!.type); - if (nodeType!.methods === undefined || nodeType!.methods.loadOptions === undefined || nodeType!.methods.loadOptions[methodName] === undefined) { - throw new Error(`The node-type "${node!.type}" does not have the method "${methodName}" defined!`); + if ( + nodeType!.methods === undefined || + nodeType!.methods.loadOptions === undefined || + nodeType!.methods.loadOptions[methodName] === undefined + ) { + throw new Error( + `The node-type "${node!.type}" does not have the method "${methodName}" defined!`, + ); } - const thisArgs = NodeExecuteFunctions.getLoadOptionsFunctions(this.workflow, node!, this.path, additionalData); + const thisArgs = NodeExecuteFunctions.getLoadOptionsFunctions( + this.workflow, + node!, + this.path, + additionalData, + ); return nodeType!.methods.loadOptions[methodName].call(thisArgs); } - } diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 22dbc311bb..fcdb728db0 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -1,12 +1,17 @@ -import { - BINARY_ENCODING, - IHookFunctions, - ILoadOptionsFunctions, - IResponseError, - IWorkflowSettings, - PLACEHOLDER_EMPTY_EXECUTION_ID, -} from './'; - +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-prototype-builtins */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable new-cap */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/no-shadow */ +/* eslint-disable no-param-reassign */ import { IAllExecuteFunctions, IBinaryData, @@ -40,12 +45,15 @@ import { WorkflowActivateMode, WorkflowDataProxy, WorkflowExecuteMode, + LoggerProxy as Logger, } from 'n8n-workflow'; import * as clientOAuth1 from 'oauth-1.0a'; import { Token } from 'oauth-1.0a'; import * as clientOAuth2 from 'client-oauth2'; +// eslint-disable-next-line import/no-extraneous-dependencies import { get } from 'lodash'; +// eslint-disable-next-line import/no-extraneous-dependencies import * as express from 'express'; import * as path from 'path'; import { OptionsWithUri, OptionsWithUrl } from 'request'; @@ -53,9 +61,16 @@ import * as requestPromise from 'request-promise-native'; import { createHmac } from 'crypto'; import { fromBuffer } from 'file-type'; import { lookup } from 'mime-types'; + +// eslint-disable-next-line import/no-cycle import { - LoggerProxy as Logger, -} from 'n8n-workflow'; + BINARY_ENCODING, + IHookFunctions, + ILoadOptionsFunctions, + IResponseError, + IWorkflowSettings, + PLACEHOLDER_EMPTY_EXECUTION_ID, +} from '.'; const requestPromiseWithDefaults = requestPromise.defaults({ timeout: 300000, // 5 minutes @@ -71,8 +86,13 @@ const requestPromiseWithDefaults = requestPromise.defaults({ * @param {number} inputIndex * @returns {Promise} */ -export async function getBinaryDataBuffer(inputData: ITaskDataConnections, itemIndex: number, propertyName: string, inputIndex: number): Promise { - const binaryData = inputData['main']![inputIndex]![itemIndex]!.binary![propertyName]!; +export async function getBinaryDataBuffer( + inputData: ITaskDataConnections, + itemIndex: number, + propertyName: string, + inputIndex: number, +): Promise { + const binaryData = inputData.main![inputIndex]![itemIndex]!.binary![propertyName]!; return Buffer.from(binaryData.data, BINARY_ENCODING); } @@ -86,7 +106,11 @@ export async function getBinaryDataBuffer(inputData: ITaskDataConnections, itemI * @param {string} [mimeType] * @returns {Promise} */ -export async function prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise { +export async function prepareBinaryData( + binaryData: Buffer, + filePath?: string, + mimeType?: string, +): Promise { if (!mimeType) { // If no mime type is given figure it out @@ -143,8 +167,6 @@ export async function prepareBinaryData(binaryData: Buffer, filePath?: string, m return returnData; } - - /** * Makes a request using OAuth data for authentication * @@ -157,8 +179,17 @@ export async function prepareBinaryData(binaryData: Buffer, filePath?: string, m * * @returns */ -export async function requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, node: INode, additionalData: IWorkflowExecuteAdditionalData, oAuth2Options?: IOAuth2Options) { - const credentials = await this.getCredentials(credentialsType) as ICredentialDataDecryptedObject; +export async function requestOAuth2( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, + node: INode, + additionalData: IWorkflowExecuteAdditionalData, + oAuth2Options?: IOAuth2Options, +) { + const credentials = (await this.getCredentials( + credentialsType, + )) as ICredentialDataDecryptedObject; if (credentials === undefined) { throw new Error('No credentials were returned!'); @@ -176,78 +207,101 @@ export async function requestOAuth2(this: IAllExecuteFunctions, credentialsType: const oauthTokenData = credentials.oauthTokenData as clientOAuth2.Data; - const token = oAuthClient.createToken(get(oauthTokenData, oAuth2Options?.property as string) || oauthTokenData.accessToken, oauthTokenData.refreshToken, oAuth2Options?.tokenType || oauthTokenData.tokenType, oauthTokenData); + const token = oAuthClient.createToken( + get(oauthTokenData, oAuth2Options?.property as string) || oauthTokenData.accessToken, + oauthTokenData.refreshToken, + oAuth2Options?.tokenType || oauthTokenData.tokenType, + oauthTokenData, + ); // Signs the request by adding authorization headers or query parameters depending // 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]; + // @ts-ignore + newRequestOptions?.headers?.Authorization = + // @ts-ignore + newRequestOptions?.headers?.Authorization.split(' ')[1]; } - return this.helpers.request!(newRequestOptions) - .catch(async (error: IResponseError) => { - const statusCodeReturned = oAuth2Options?.tokenExpiredStatusCode === undefined ? 401 : oAuth2Options?.tokenExpiredStatusCode; + return this.helpers.request!(newRequestOptions).catch(async (error: IResponseError) => { + const statusCodeReturned = + oAuth2Options?.tokenExpiredStatusCode === undefined + ? 401 + : oAuth2Options?.tokenExpiredStatusCode; - if (error.statusCode === statusCodeReturned) { - // Token is probably not valid anymore. So try refresh it. + if (error.statusCode === statusCodeReturned) { + // Token is probably not valid anymore. So try refresh it. - const tokenRefreshOptions: IDataObject = {}; + const tokenRefreshOptions: IDataObject = {}; - if (oAuth2Options?.includeCredentialsOnRefreshOnBody) { - const body: IDataObject = { - client_id: credentials.clientId as string, - client_secret: credentials.clientSecret as string, - }; - tokenRefreshOptions.body = body; - // Override authorization property so the credentails are not included in it - tokenRefreshOptions.headers = { - Authorization: '', - }; - } - - Logger.debug(`OAuth2 token for "${credentialsType}" used by node "${node.name}" expired. Should revalidate.`); - - const newToken = await token.refresh(tokenRefreshOptions); - - Logger.debug(`OAuth2 token for "${credentialsType}" used by node "${node.name}" has been renewed.`); - - credentials.oauthTokenData = newToken.data; - - // Find the name of the credentials - if (!node.credentials || !node.credentials[credentialsType]) { - throw new Error(`The node "${node.name}" does not have credentials of type "${credentialsType}"!`); - } - const name = node.credentials[credentialsType]; - - // Save the refreshed token - await additionalData.credentialsHelper.updateCredentials(name, credentialsType, credentials); - - Logger.debug(`OAuth2 token for "${credentialsType}" used by node "${node.name}" has been saved to database successfully.`); - - // Make the request again with the new token - const newRequestOptions = newToken.sign(requestOptions as clientOAuth2.RequestObject); - - return this.helpers.request!(newRequestOptions); + if (oAuth2Options?.includeCredentialsOnRefreshOnBody) { + const body: IDataObject = { + client_id: credentials.clientId as string, + client_secret: credentials.clientSecret as string, + }; + tokenRefreshOptions.body = body; + // Override authorization property so the credentails are not included in it + tokenRefreshOptions.headers = { + Authorization: '', + }; } - // Unknown error so simply throw it - throw error; - }); + Logger.debug( + `OAuth2 token for "${credentialsType}" used by node "${node.name}" expired. Should revalidate.`, + ); + + const newToken = await token.refresh(tokenRefreshOptions); + + Logger.debug( + `OAuth2 token for "${credentialsType}" used by node "${node.name}" has been renewed.`, + ); + + credentials.oauthTokenData = newToken.data; + + // Find the name of the credentials + if (!node.credentials || !node.credentials[credentialsType]) { + throw new Error( + `The node "${node.name}" does not have credentials of type "${credentialsType}"!`, + ); + } + const name = node.credentials[credentialsType]; + + // Save the refreshed token + await additionalData.credentialsHelper.updateCredentials(name, credentialsType, credentials); + + Logger.debug( + `OAuth2 token for "${credentialsType}" used by node "${node.name}" has been saved to database successfully.`, + ); + + // Make the request again with the new token + const newRequestOptions = newToken.sign(requestOptions as clientOAuth2.RequestObject); + + return this.helpers.request!(newRequestOptions); + } + + // Unknown error so simply throw it + throw error; + }); } /* Makes a request using OAuth1 data for authentication -* -* @export -* @param {IAllExecuteFunctions} this -* @param {string} credentialsType -* @param {(OptionsWithUrl | requestPromise.RequestPromiseOptions)} requestOptionså -* @returns -*/ -export async function requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | OptionsWithUri | requestPromise.RequestPromiseOptions) { - const credentials = await this.getCredentials(credentialsType) as ICredentialDataDecryptedObject; + * + * @export + * @param {IAllExecuteFunctions} this + * @param {string} credentialsType + * @param {(OptionsWithUrl | requestPromise.RequestPromiseOptions)} requestOptionså + * @returns + */ +export async function requestOAuth1( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUrl | OptionsWithUri | requestPromise.RequestPromiseOptions, +) { + const credentials = (await this.getCredentials( + credentialsType, + )) as ICredentialDataDecryptedObject; if (credentials === undefined) { throw new Error('No credentials were returned!'); @@ -264,10 +318,8 @@ export async function requestOAuth1(this: IAllExecuteFunctions, credentialsType: }, signature_method: credentials.signatureMethod as string, hash_function(base, key) { - const algorithm = (credentials.signatureMethod === 'HMAC-SHA1') ? 'sha1' : 'sha256'; - return createHmac(algorithm, key) - .update(base) - .digest('base64'); + const algorithm = credentials.signatureMethod === 'HMAC-SHA1' ? 'sha1' : 'sha256'; + return createHmac(algorithm, key).update(base).digest('base64'); }, }); @@ -278,7 +330,7 @@ export async function requestOAuth1(this: IAllExecuteFunctions, credentialsType: secret: oauthTokenData.oauth_token_secret as string, }; - //@ts-ignore + // @ts-ignore requestOptions.data = { ...requestOptions.qs, ...requestOptions.form }; // Fixes issue that OAuth1 library only works with "url" property and not with "uri" @@ -290,17 +342,15 @@ export async function requestOAuth1(this: IAllExecuteFunctions, credentialsType: delete requestOptions.uri; } - //@ts-ignore + // @ts-ignore requestOptions.headers = oauth.toHeader(oauth.authorize(requestOptions, token)); - return this.helpers.request!(requestOptions) - .catch(async (error: IResponseError) => { - // Unknown error so simply throw it - throw error; - }); + return this.helpers.request!(requestOptions).catch(async (error: IResponseError) => { + // Unknown error so simply throw it + throw error; + }); } - /** * Takes generic input data and brings it into the json format n8n uses. * @@ -322,8 +372,6 @@ export function returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExe return returnData; } - - /** * Returns the additional keys for Expressions and Function-Nodes * @@ -331,7 +379,9 @@ export function returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExe * @param {IWorkflowExecuteAdditionalData} additionalData * @returns {(IWorkflowDataProxyAdditionalKeys)} */ -export function getAdditionalKeys(additionalData: IWorkflowExecuteAdditionalData): IWorkflowDataProxyAdditionalKeys { +export function getAdditionalKeys( + additionalData: IWorkflowExecuteAdditionalData, +): IWorkflowDataProxyAdditionalKeys { const executionId = additionalData.executionId || PLACEHOLDER_EMPTY_EXECUTION_ID; return { $executionId: executionId, @@ -339,8 +389,6 @@ export function getAdditionalKeys(additionalData: IWorkflowExecuteAdditionalData }; } - - /** * Returns the requested decrypted credentials if the node has access to them. * @@ -351,24 +399,50 @@ export function getAdditionalKeys(additionalData: IWorkflowExecuteAdditionalData * @param {IWorkflowExecuteAdditionalData} additionalData * @returns {(ICredentialDataDecryptedObject | undefined)} */ -export async function getCredentials(workflow: Workflow, node: INode, type: string, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, runExecutionData?: IRunExecutionData | null, runIndex?: number, connectionInputData?: INodeExecutionData[], itemIndex?: number): Promise { - +export async function getCredentials( + workflow: Workflow, + node: INode, + type: string, + additionalData: IWorkflowExecuteAdditionalData, + mode: WorkflowExecuteMode, + runExecutionData?: IRunExecutionData | null, + runIndex?: number, + connectionInputData?: INodeExecutionData[], + itemIndex?: number, +): Promise { // Get the NodeType as it has the information if the credentials are required const nodeType = workflow.nodeTypes.getByName(node.type); if (nodeType === undefined) { - throw new NodeOperationError(node, `Node type "${node.type}" is not known so can not get credentials!`); + throw new NodeOperationError( + node, + `Node type "${node.type}" is not known so can not get credentials!`, + ); } if (nodeType.description.credentials === undefined) { - throw new NodeOperationError(node, `Node type "${node.type}" does not have any credentials defined!`); + throw new NodeOperationError( + node, + `Node type "${node.type}" does not have any credentials defined!`, + ); } - const nodeCredentialDescription = nodeType.description.credentials.find((credentialTypeDescription) => credentialTypeDescription.name === type); + const nodeCredentialDescription = nodeType.description.credentials.find( + (credentialTypeDescription) => credentialTypeDescription.name === type, + ); if (nodeCredentialDescription === undefined) { - throw new NodeOperationError(node, `Node type "${node.type}" does not have any credentials of type "${type}" defined!`); + throw new NodeOperationError( + node, + `Node type "${node.type}" does not have any credentials of type "${type}" defined!`, + ); } - if (NodeHelpers.displayParameter(additionalData.currentNodeParameters || node.parameters, nodeCredentialDescription, node.parameters) === false) { + if ( + !NodeHelpers.displayParameter( + additionalData.currentNodeParameters || node.parameters, + nodeCredentialDescription, + node.parameters, + ) + ) { // Credentials should not be displayed so return undefined even if they would be defined return undefined; } @@ -380,10 +454,10 @@ export async function getCredentials(workflow: Workflow, node: INode, type: stri if (nodeCredentialDescription.required === true) { // Credentials are required so error if (!node.credentials) { - throw new NodeOperationError(node,'Node does not have any credentials set!'); + throw new NodeOperationError(node, 'Node does not have any credentials set!'); } if (!node.credentials[type]) { - throw new NodeOperationError(node,`Node does not have any credentials set for "${type}"!`); + throw new NodeOperationError(node, `Node does not have any credentials set for "${type}"!`); } } else { // Credentials are not required so resolve with undefined @@ -408,16 +482,29 @@ export async function getCredentials(workflow: Workflow, node: INode, type: stri if (name.charAt(0) === '=') { // If the credential name is an expression resolve it const additionalKeys = getAdditionalKeys(additionalData); - name = workflow.expression.getParameterValue(name, runExecutionData || null, runIndex || 0, itemIndex || 0, node.name, connectionInputData || [], mode, additionalKeys) as string; + name = workflow.expression.getParameterValue( + name, + runExecutionData || null, + runIndex || 0, + itemIndex || 0, + node.name, + connectionInputData || [], + mode, + additionalKeys, + ) as string; } - const decryptedDataObject = await additionalData.credentialsHelper.getDecrypted(name, type, mode, false, expressionResolveValues); + const decryptedDataObject = await additionalData.credentialsHelper.getDecrypted( + name, + type, + mode, + false, + expressionResolveValues, + ); return decryptedDataObject; } - - /** * Returns a copy of the node * @@ -429,8 +516,6 @@ export function getNode(node: INode): INode { return JSON.parse(JSON.stringify(node)); } - - /** * Returns the requested resolved (all expressions replaced) node parameters. * @@ -445,7 +530,18 @@ export function getNode(node: INode): INode { * @param {*} [fallbackValue] * @returns {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object)} */ -export function getNodeParameter(workflow: Workflow, runExecutionData: IRunExecutionData | null, runIndex: number, connectionInputData: INodeExecutionData[], node: INode, parameterName: string, itemIndex: number, mode: WorkflowExecuteMode, additionalKeys: IWorkflowDataProxyAdditionalKeys, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object { //tslint:disable-line:no-any +export function getNodeParameter( + workflow: Workflow, + runExecutionData: IRunExecutionData | null, + runIndex: number, + connectionInputData: INodeExecutionData[], + node: INode, + parameterName: string, + itemIndex: number, + mode: WorkflowExecuteMode, + additionalKeys: IWorkflowDataProxyAdditionalKeys, + fallbackValue?: any, +): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object { const nodeType = workflow.nodeTypes.getByName(node.type); if (nodeType === undefined) { throw new Error(`Node type "${node.type}" is not known so can not return paramter value!`); @@ -459,7 +555,16 @@ export function getNodeParameter(workflow: Workflow, runExecutionData: IRunExecu let returnData; try { - returnData = workflow.expression.getParameterValue(value, runExecutionData, runIndex, itemIndex, node.name, connectionInputData, mode, additionalKeys); + returnData = workflow.expression.getParameterValue( + value, + runExecutionData, + runIndex, + itemIndex, + node.name, + connectionInputData, + mode, + additionalKeys, + ); } catch (e) { e.message += ` [Error in parameter: "${parameterName}"]`; throw e; @@ -468,8 +573,6 @@ export function getNodeParameter(workflow: Workflow, runExecutionData: IRunExecu return returnData; } - - /** * Returns if execution should be continued even if there was an error. * @@ -481,8 +584,6 @@ export function continueOnFail(node: INode): boolean { return get(node, 'continueOnFail', false); } - - /** * Returns the webhook URL of the webhook with the given name * @@ -494,28 +595,46 @@ export function continueOnFail(node: INode): boolean { * @param {boolean} [isTest] * @returns {(string | undefined)} */ -export function getNodeWebhookUrl(name: string, workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, additionalKeys: IWorkflowDataProxyAdditionalKeys, isTest?: boolean): string | undefined { +export function getNodeWebhookUrl( + name: string, + workflow: Workflow, + node: INode, + additionalData: IWorkflowExecuteAdditionalData, + mode: WorkflowExecuteMode, + additionalKeys: IWorkflowDataProxyAdditionalKeys, + isTest?: boolean, +): string | undefined { let baseUrl = additionalData.webhookBaseUrl; if (isTest === true) { baseUrl = additionalData.webhookTestBaseUrl; } + // eslint-disable-next-line @typescript-eslint/no-use-before-define const webhookDescription = getWebhookDescription(name, workflow, node); if (webhookDescription === undefined) { return undefined; } - const path = workflow.expression.getSimpleParameterValue(node, webhookDescription['path'], mode, additionalKeys); + const path = workflow.expression.getSimpleParameterValue( + node, + webhookDescription.path, + mode, + additionalKeys, + ); if (path === undefined) { return undefined; } - const isFullPath: boolean = workflow.expression.getSimpleParameterValue(node, webhookDescription['isFullPath'], mode, additionalKeys, false) as boolean; + const isFullPath: boolean = workflow.expression.getSimpleParameterValue( + node, + webhookDescription.isFullPath, + mode, + additionalKeys, + false, + ) as boolean; return NodeHelpers.getNodeWebhookUrl(baseUrl, workflow.id!, node, path.toString(), isFullPath); } - - /** * Returns the timezone for the workflow * @@ -524,15 +643,17 @@ export function getNodeWebhookUrl(name: string, workflow: Workflow, node: INode, * @param {IWorkflowExecuteAdditionalData} additionalData * @returns {string} */ -export function getTimezone(workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData):string { +export function getTimezone( + workflow: Workflow, + additionalData: IWorkflowExecuteAdditionalData, +): string { + // eslint-disable-next-line @typescript-eslint/prefer-optional-chain if (workflow.settings !== undefined && workflow.settings.timezone !== undefined) { return (workflow.settings as IWorkflowSettings).timezone as string; } return additionalData.timezone; } - - /** * Returns the full webhook description of the webhook with the given name * @@ -542,7 +663,11 @@ export function getTimezone(workflow: Workflow, additionalData: IWorkflowExecute * @param {INode} node * @returns {(IWebhookDescription | undefined)} */ -export function getWebhookDescription(name: string, workflow: Workflow, node: INode): IWebhookDescription | undefined { +export function getWebhookDescription( + name: string, + workflow: Workflow, + node: INode, +): IWebhookDescription | undefined { const nodeType = workflow.nodeTypes.getByName(node.type) as INodeType; if (nodeType.description.webhooks === undefined) { @@ -550,6 +675,7 @@ export function getWebhookDescription(name: string, workflow: Workflow, node: IN return undefined; } + // eslint-disable-next-line no-restricted-syntax for (const webhookDescription of nodeType.description.webhooks) { if (webhookDescription.name === name) { return webhookDescription; @@ -559,8 +685,6 @@ export function getWebhookDescription(name: string, workflow: Workflow, node: IN return undefined; } - - /** * Returns the workflow metadata * @@ -576,8 +700,6 @@ export function getWorkflowMetadata(workflow: Workflow): IWorkflowMetadata { }; } - - /** * Returns the execute functions the poll nodes have access to. * @@ -589,14 +711,20 @@ export function getWorkflowMetadata(workflow: Workflow): IWorkflowMetadata { * @returns {ITriggerFunctions} */ // TODO: Check if I can get rid of: additionalData, and so then maybe also at ActiveWorkflowRunner.add -export function getExecutePollFunctions(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): IPollFunctions { +export function getExecutePollFunctions( + workflow: Workflow, + node: INode, + additionalData: IWorkflowExecuteAdditionalData, + mode: WorkflowExecuteMode, + activation: WorkflowActivateMode, +): IPollFunctions { return ((workflow: Workflow, node: INode) => { return { __emit: (data: INodeExecutionData[][]): void => { throw new Error('Overwrite NodeExecuteFunctions.getExecutePullFunctions.__emit function!'); }, async getCredentials(type: string): Promise { - return await getCredentials(workflow, node, type, additionalData, mode); + return getCredentials(workflow, node, type, additionalData, mode); }, getMode: (): WorkflowExecuteMode => { return mode; @@ -607,13 +735,32 @@ export function getExecutePollFunctions(workflow: Workflow, node: INode, additio getNode: () => { return getNode(node); }, - getNodeParameter: (parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object => { //tslint:disable-line:no-any + getNodeParameter: ( + parameterName: string, + fallbackValue?: any, + ): + | NodeParameterValue + | INodeParameters + | NodeParameterValue[] + | INodeParameters[] + | object => { const runExecutionData: IRunExecutionData | null = null; const itemIndex = 0; const runIndex = 0; const connectionInputData: INodeExecutionData[] = []; - return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, mode, getAdditionalKeys(additionalData), fallbackValue); + return getNodeParameter( + workflow, + runExecutionData, + runIndex, + connectionInputData, + node, + parameterName, + itemIndex, + mode, + getAdditionalKeys(additionalData), + fallbackValue, + ); }, getRestApiUrl: (): string => { return additionalData.restApiUrl; @@ -630,10 +777,26 @@ export function getExecutePollFunctions(workflow: Workflow, node: INode, additio helpers: { prepareBinaryData, request: requestPromiseWithDefaults, - requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise { // tslint:disable-line:no-any - return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, oAuth2Options); + async requestOAuth2( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, + oAuth2Options?: IOAuth2Options, + ): Promise { + return requestOAuth2.call( + this, + credentialsType, + requestOptions, + node, + additionalData, + oAuth2Options, + ); }, - requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any + async requestOAuth1( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions, + ): Promise { return requestOAuth1.call(this, credentialsType, requestOptions); }, returnJsonArray, @@ -642,8 +805,6 @@ export function getExecutePollFunctions(workflow: Workflow, node: INode, additio })(workflow, node); } - - /** * Returns the execute functions the trigger nodes have access to. * @@ -655,14 +816,20 @@ export function getExecutePollFunctions(workflow: Workflow, node: INode, additio * @returns {ITriggerFunctions} */ // TODO: Check if I can get rid of: additionalData, and so then maybe also at ActiveWorkflowRunner.add -export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): ITriggerFunctions { +export function getExecuteTriggerFunctions( + workflow: Workflow, + node: INode, + additionalData: IWorkflowExecuteAdditionalData, + mode: WorkflowExecuteMode, + activation: WorkflowActivateMode, +): ITriggerFunctions { return ((workflow: Workflow, node: INode) => { return { emit: (data: INodeExecutionData[][]): void => { throw new Error('Overwrite NodeExecuteFunctions.getExecuteTriggerFunctions.emit function!'); }, async getCredentials(type: string): Promise { - return await getCredentials(workflow, node, type, additionalData, mode); + return getCredentials(workflow, node, type, additionalData, mode); }, getNode: () => { return getNode(node); @@ -673,13 +840,32 @@ export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, addi getActivationMode: (): WorkflowActivateMode => { return activation; }, - getNodeParameter: (parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object => { //tslint:disable-line:no-any + getNodeParameter: ( + parameterName: string, + fallbackValue?: any, + ): + | NodeParameterValue + | INodeParameters + | NodeParameterValue[] + | INodeParameters[] + | object => { const runExecutionData: IRunExecutionData | null = null; const itemIndex = 0; const runIndex = 0; const connectionInputData: INodeExecutionData[] = []; - return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, mode, getAdditionalKeys(additionalData), fallbackValue); + return getNodeParameter( + workflow, + runExecutionData, + runIndex, + connectionInputData, + node, + parameterName, + itemIndex, + mode, + getAdditionalKeys(additionalData), + fallbackValue, + ); }, getRestApiUrl: (): string => { return additionalData.restApiUrl; @@ -696,20 +882,34 @@ export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, addi helpers: { prepareBinaryData, request: requestPromiseWithDefaults, - requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise { // tslint:disable-line:no-any - return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, oAuth2Options); + async requestOAuth2( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, + oAuth2Options?: IOAuth2Options, + ): Promise { + return requestOAuth2.call( + this, + credentialsType, + requestOptions, + node, + additionalData, + oAuth2Options, + ); }, - requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any + async requestOAuth1( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions, + ): Promise { return requestOAuth1.call(this, credentialsType, requestOptions); }, returnJsonArray, }, }; - }) (workflow, node); + })(workflow, node); } - - /** * Returns the execute functions regular nodes have access to. * @@ -724,29 +924,63 @@ export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, addi * @param {WorkflowExecuteMode} mode * @returns {IExecuteFunctions} */ -export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunExecutionData, runIndex: number, connectionInputData: INodeExecutionData[], inputData: ITaskDataConnections, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): IExecuteFunctions { +export function getExecuteFunctions( + workflow: Workflow, + runExecutionData: IRunExecutionData, + runIndex: number, + connectionInputData: INodeExecutionData[], + inputData: ITaskDataConnections, + node: INode, + additionalData: IWorkflowExecuteAdditionalData, + mode: WorkflowExecuteMode, +): IExecuteFunctions { return ((workflow, runExecutionData, connectionInputData, inputData, node) => { return { continueOnFail: () => { return continueOnFail(node); }, evaluateExpression: (expression: string, itemIndex: number) => { - return workflow.expression.resolveSimpleParameterValue('=' + expression, {}, runExecutionData, runIndex, itemIndex, node.name, connectionInputData, mode, getAdditionalKeys(additionalData)); + return workflow.expression.resolveSimpleParameterValue( + `=${expression}`, + {}, + runExecutionData, + runIndex, + itemIndex, + node.name, + connectionInputData, + mode, + getAdditionalKeys(additionalData), + ); }, - async executeWorkflow(workflowInfo: IExecuteWorkflowInfo, inputData?: INodeExecutionData[]): Promise { // tslint:disable-line:no-any + async executeWorkflow( + workflowInfo: IExecuteWorkflowInfo, + inputData?: INodeExecutionData[], + ): Promise { return additionalData.executeWorkflow(workflowInfo, additionalData, inputData); }, getContext(type: string): IContextObject { return NodeHelpers.getContext(runExecutionData, type, node); }, - async getCredentials(type: string, itemIndex?: number): Promise { - return await getCredentials(workflow, node, type, additionalData, mode, runExecutionData, runIndex, connectionInputData, itemIndex); + async getCredentials( + type: string, + itemIndex?: number, + ): Promise { + return getCredentials( + workflow, + node, + type, + additionalData, + mode, + runExecutionData, + runIndex, + connectionInputData, + itemIndex, + ); }, getExecutionId: (): string => { return additionalData.executionId!; }, getInputData: (inputIndex = 0, inputName = 'main') => { - if (!inputData.hasOwnProperty(inputName)) { // Return empty array because else it would throw error when nothing is connected to input return []; @@ -764,8 +998,28 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx return inputData[inputName][inputIndex] as INodeExecutionData[]; }, - getNodeParameter: (parameterName: string, itemIndex: number, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object => { //tslint:disable-line:no-any - return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, mode, getAdditionalKeys(additionalData), fallbackValue); + getNodeParameter: ( + parameterName: string, + itemIndex: number, + fallbackValue?: any, + ): + | NodeParameterValue + | INodeParameters + | NodeParameterValue[] + | INodeParameters[] + | object => { + return getNodeParameter( + workflow, + runExecutionData, + runIndex, + connectionInputData, + node, + parameterName, + itemIndex, + mode, + getAdditionalKeys(additionalData), + fallbackValue, + ); }, getMode: (): WorkflowExecuteMode => { return mode; @@ -783,7 +1037,17 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx return getWorkflowMetadata(workflow); }, getWorkflowDataProxy: (itemIndex: number): IWorkflowDataProxyData => { - const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, node.name, connectionInputData, {}, mode, getAdditionalKeys(additionalData)); + const dataProxy = new WorkflowDataProxy( + workflow, + runExecutionData, + runIndex, + itemIndex, + node.name, + connectionInputData, + {}, + mode, + getAdditionalKeys(additionalData), + ); return dataProxy.getDataProxy(); }, getWorkflowStaticData(type: string): IDataObject { @@ -793,7 +1057,7 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx async putExecutionToWait(waitTill: Date): Promise { runExecutionData.waitTill = waitTill; }, - sendMessageToUI(message : any): void { // tslint:disable-line:no-any + sendMessageToUI(message: any): void { if (mode !== 'manual') { return; } @@ -802,19 +1066,40 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx additionalData.sendMessageToUI(node.name, message); } } catch (error) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions Logger.warn(`There was a problem sending messsage to UI: ${error.message}`); } }, helpers: { prepareBinaryData, - getBinaryDataBuffer(itemIndex: number, propertyName: string, inputIndex = 0): Promise { + async getBinaryDataBuffer( + itemIndex: number, + propertyName: string, + inputIndex = 0, + ): Promise { return getBinaryDataBuffer.call(this, inputData, itemIndex, propertyName, inputIndex); }, request: requestPromiseWithDefaults, - requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise { // tslint:disable-line:no-any - return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, oAuth2Options); + async requestOAuth2( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, + oAuth2Options?: IOAuth2Options, + ): Promise { + return requestOAuth2.call( + this, + credentialsType, + requestOptions, + node, + additionalData, + oAuth2Options, + ); }, - requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any + async requestOAuth1( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions, + ): Promise { return requestOAuth1.call(this, credentialsType, requestOptions); }, returnJsonArray, @@ -823,8 +1108,6 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx })(workflow, runExecutionData, connectionInputData, inputData, node); } - - /** * Returns the execute functions regular nodes have access to when single-function is defined. * @@ -840,7 +1123,17 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx * @param {WorkflowExecuteMode} mode * @returns {IExecuteSingleFunctions} */ -export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData: IRunExecutionData, runIndex: number, connectionInputData: INodeExecutionData[], inputData: ITaskDataConnections, node: INode, itemIndex: number, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): IExecuteSingleFunctions { +export function getExecuteSingleFunctions( + workflow: Workflow, + runExecutionData: IRunExecutionData, + runIndex: number, + connectionInputData: INodeExecutionData[], + inputData: ITaskDataConnections, + node: INode, + itemIndex: number, + additionalData: IWorkflowExecuteAdditionalData, + mode: WorkflowExecuteMode, +): IExecuteSingleFunctions { return ((workflow, runExecutionData, connectionInputData, inputData, node, itemIndex) => { return { continueOnFail: () => { @@ -848,18 +1141,38 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData: }, evaluateExpression: (expression: string, evaluateItemIndex: number | undefined) => { evaluateItemIndex = evaluateItemIndex === undefined ? itemIndex : evaluateItemIndex; - return workflow.expression.resolveSimpleParameterValue('=' + expression, {}, runExecutionData, runIndex, evaluateItemIndex, node.name, connectionInputData, mode, getAdditionalKeys(additionalData)); + return workflow.expression.resolveSimpleParameterValue( + `=${expression}`, + {}, + runExecutionData, + runIndex, + evaluateItemIndex, + node.name, + connectionInputData, + mode, + getAdditionalKeys(additionalData), + ); }, getContext(type: string): IContextObject { return NodeHelpers.getContext(runExecutionData, type, node); }, async getCredentials(type: string): Promise { - return await getCredentials(workflow, node, type, additionalData, mode, runExecutionData, runIndex, connectionInputData, itemIndex); + return getCredentials( + workflow, + node, + type, + additionalData, + mode, + runExecutionData, + runIndex, + connectionInputData, + itemIndex, + ); }, getInputData: (inputIndex = 0, inputName = 'main') => { if (!inputData.hasOwnProperty(inputName)) { // Return empty array because else it would throw error when nothing is connected to input - return {json: {}}; + return { json: {} }; } // TODO: Check if nodeType has input with that index defined @@ -876,10 +1189,12 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData: if (allItems[itemIndex] === null) { // return []; - throw new Error(`Value "${inputIndex}" of input "${inputName}" with itemIndex "${itemIndex}" did not get set!`); + throw new Error( + `Value "${inputIndex}" of input "${inputName}" with itemIndex "${itemIndex}" did not get set!`, + ); } - return allItems[itemIndex] as INodeExecutionData; + return allItems[itemIndex]; }, getMode: (): WorkflowExecuteMode => { return mode; @@ -893,14 +1208,43 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData: getTimezone: (): string => { return getTimezone(workflow, additionalData); }, - getNodeParameter: (parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object => { //tslint:disable-line:no-any - return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, mode, getAdditionalKeys(additionalData), fallbackValue); + getNodeParameter: ( + parameterName: string, + fallbackValue?: any, + ): + | NodeParameterValue + | INodeParameters + | NodeParameterValue[] + | INodeParameters[] + | object => { + return getNodeParameter( + workflow, + runExecutionData, + runIndex, + connectionInputData, + node, + parameterName, + itemIndex, + mode, + getAdditionalKeys(additionalData), + fallbackValue, + ); }, getWorkflow: () => { return getWorkflowMetadata(workflow); }, getWorkflowDataProxy: (): IWorkflowDataProxyData => { - const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, node.name, connectionInputData, {}, mode, getAdditionalKeys(additionalData)); + const dataProxy = new WorkflowDataProxy( + workflow, + runExecutionData, + runIndex, + itemIndex, + node.name, + connectionInputData, + {}, + mode, + getAdditionalKeys(additionalData), + ); return dataProxy.getDataProxy(); }, getWorkflowStaticData(type: string): IDataObject { @@ -909,10 +1253,26 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData: helpers: { prepareBinaryData, request: requestPromiseWithDefaults, - requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise { // tslint:disable-line:no-any - return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, oAuth2Options); + async requestOAuth2( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, + oAuth2Options?: IOAuth2Options, + ): Promise { + return requestOAuth2.call( + this, + credentialsType, + requestOptions, + node, + additionalData, + oAuth2Options, + ); }, - requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any + async requestOAuth1( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions, + ): Promise { return requestOAuth1.call(this, credentialsType, requestOptions); }, }, @@ -920,7 +1280,6 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData: })(workflow, runExecutionData, connectionInputData, inputData, node, itemIndex); } - /** * Returns the execute functions regular nodes have access to in load-options-function. * @@ -930,13 +1289,26 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData: * @param {IWorkflowExecuteAdditionalData} additionalData * @returns {ILoadOptionsFunctions} */ -export function getLoadOptionsFunctions(workflow: Workflow, node: INode, path: string, additionalData: IWorkflowExecuteAdditionalData): ILoadOptionsFunctions { +export function getLoadOptionsFunctions( + workflow: Workflow, + node: INode, + path: string, + additionalData: IWorkflowExecuteAdditionalData, +): ILoadOptionsFunctions { return ((workflow: Workflow, node: INode, path: string) => { const that = { async getCredentials(type: string): Promise { - return await getCredentials(workflow, node, type, additionalData, 'internal'); + return getCredentials(workflow, node, type, additionalData, 'internal'); }, - getCurrentNodeParameter: (parameterPath: string): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object | undefined => { + getCurrentNodeParameter: ( + parameterPath: string, + ): + | NodeParameterValue + | INodeParameters + | NodeParameterValue[] + | INodeParameters[] + | object + | undefined => { const nodeParameters = additionalData.currentNodeParameters; if (parameterPath.charAt(0) === '&') { @@ -951,13 +1323,32 @@ export function getLoadOptionsFunctions(workflow: Workflow, node: INode, path: s getNode: () => { return getNode(node); }, - getNodeParameter: (parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object => { //tslint:disable-line:no-any + getNodeParameter: ( + parameterName: string, + fallbackValue?: any, + ): + | NodeParameterValue + | INodeParameters + | NodeParameterValue[] + | INodeParameters[] + | object => { const runExecutionData: IRunExecutionData | null = null; const itemIndex = 0; const runIndex = 0; const connectionInputData: INodeExecutionData[] = []; - return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, 'internal' as WorkflowExecuteMode, getAdditionalKeys(additionalData), fallbackValue); + return getNodeParameter( + workflow, + runExecutionData, + runIndex, + connectionInputData, + node, + parameterName, + itemIndex, + 'internal' as WorkflowExecuteMode, + getAdditionalKeys(additionalData), + fallbackValue, + ); }, getTimezone: (): string => { return getTimezone(workflow, additionalData); @@ -967,20 +1358,34 @@ export function getLoadOptionsFunctions(workflow: Workflow, node: INode, path: s }, helpers: { request: requestPromiseWithDefaults, - requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise { // tslint:disable-line:no-any - return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, oAuth2Options); + async requestOAuth2( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, + oAuth2Options?: IOAuth2Options, + ): Promise { + return requestOAuth2.call( + this, + credentialsType, + requestOptions, + node, + additionalData, + oAuth2Options, + ); }, - requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any + async requestOAuth1( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions, + ): Promise { return requestOAuth1.call(this, credentialsType, requestOptions); }, }, }; return that; })(workflow, node, path); - } - /** * Returns the execute functions regular nodes have access to in hook-function. * @@ -991,11 +1396,19 @@ export function getLoadOptionsFunctions(workflow: Workflow, node: INode, path: s * @param {WorkflowExecuteMode} mode * @returns {IHookFunctions} */ -export function getExecuteHookFunctions(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode, isTest?: boolean, webhookData?: IWebhookData): IHookFunctions { +export function getExecuteHookFunctions( + workflow: Workflow, + node: INode, + additionalData: IWorkflowExecuteAdditionalData, + mode: WorkflowExecuteMode, + activation: WorkflowActivateMode, + isTest?: boolean, + webhookData?: IWebhookData, +): IHookFunctions { return ((workflow: Workflow, node: INode) => { const that = { async getCredentials(type: string): Promise { - return await getCredentials(workflow, node, type, additionalData, mode); + return getCredentials(workflow, node, type, additionalData, mode); }, getMode: (): WorkflowExecuteMode => { return mode; @@ -1006,16 +1419,43 @@ export function getExecuteHookFunctions(workflow: Workflow, node: INode, additio getNode: () => { return getNode(node); }, - getNodeParameter: (parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object => { //tslint:disable-line:no-any + getNodeParameter: ( + parameterName: string, + fallbackValue?: any, + ): + | NodeParameterValue + | INodeParameters + | NodeParameterValue[] + | INodeParameters[] + | object => { const runExecutionData: IRunExecutionData | null = null; const itemIndex = 0; const runIndex = 0; const connectionInputData: INodeExecutionData[] = []; - return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, mode, getAdditionalKeys(additionalData), fallbackValue); + return getNodeParameter( + workflow, + runExecutionData, + runIndex, + connectionInputData, + node, + parameterName, + itemIndex, + mode, + getAdditionalKeys(additionalData), + fallbackValue, + ); }, getNodeWebhookUrl: (name: string): string | undefined => { - return getNodeWebhookUrl(name, workflow, node, additionalData, mode, getAdditionalKeys(additionalData), isTest); + return getNodeWebhookUrl( + name, + workflow, + node, + additionalData, + mode, + getAdditionalKeys(additionalData), + isTest, + ); }, getTimezone: (): string => { return getTimezone(workflow, additionalData); @@ -1037,20 +1477,34 @@ export function getExecuteHookFunctions(workflow: Workflow, node: INode, additio }, helpers: { request: requestPromiseWithDefaults, - requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise { // tslint:disable-line:no-any - return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, oAuth2Options); + async requestOAuth2( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, + oAuth2Options?: IOAuth2Options, + ): Promise { + return requestOAuth2.call( + this, + credentialsType, + requestOptions, + node, + additionalData, + oAuth2Options, + ); }, - requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any + async requestOAuth1( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions, + ): Promise { return requestOAuth1.call(this, credentialsType, requestOptions); }, }, }; return that; })(workflow, node); - } - /** * Returns the execute functions regular nodes have access to when webhook-function is defined. * @@ -1062,7 +1516,13 @@ export function getExecuteHookFunctions(workflow: Workflow, node: INode, additio * @param {WorkflowExecuteMode} mode * @returns {IWebhookFunctions} */ -export function getExecuteWebhookFunctions(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, webhookData: IWebhookData): IWebhookFunctions { +export function getExecuteWebhookFunctions( + workflow: Workflow, + node: INode, + additionalData: IWorkflowExecuteAdditionalData, + mode: WorkflowExecuteMode, + webhookData: IWebhookData, +): IWebhookFunctions { return ((workflow: Workflow, node: INode) => { return { getBodyData(): IDataObject { @@ -1072,7 +1532,7 @@ export function getExecuteWebhookFunctions(workflow: Workflow, node: INode, addi return additionalData.httpRequest.body; }, async getCredentials(type: string): Promise { - return await getCredentials(workflow, node, type, additionalData, mode); + return getCredentials(workflow, node, type, additionalData, mode); }, getHeaderData(): object { if (additionalData.httpRequest === undefined) { @@ -1086,13 +1546,32 @@ export function getExecuteWebhookFunctions(workflow: Workflow, node: INode, addi getNode: () => { return getNode(node); }, - getNodeParameter: (parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object => { //tslint:disable-line:no-any + getNodeParameter: ( + parameterName: string, + fallbackValue?: any, + ): + | NodeParameterValue + | INodeParameters + | NodeParameterValue[] + | INodeParameters[] + | object => { const runExecutionData: IRunExecutionData | null = null; const itemIndex = 0; const runIndex = 0; const connectionInputData: INodeExecutionData[] = []; - return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, mode, getAdditionalKeys(additionalData), fallbackValue); + return getNodeParameter( + workflow, + runExecutionData, + runIndex, + connectionInputData, + node, + parameterName, + itemIndex, + mode, + getAdditionalKeys(additionalData), + fallbackValue, + ); }, getParamsData(): object { if (additionalData.httpRequest === undefined) { @@ -1119,7 +1598,14 @@ export function getExecuteWebhookFunctions(workflow: Workflow, node: INode, addi return additionalData.httpResponse; }, getNodeWebhookUrl: (name: string): string | undefined => { - return getNodeWebhookUrl(name, workflow, node, additionalData, mode, getAdditionalKeys(additionalData)); + return getNodeWebhookUrl( + name, + workflow, + node, + additionalData, + mode, + getAdditionalKeys(additionalData), + ); }, getTimezone: (): string => { return getTimezone(workflow, additionalData); @@ -1137,15 +1623,30 @@ export function getExecuteWebhookFunctions(workflow: Workflow, node: INode, addi helpers: { prepareBinaryData, request: requestPromiseWithDefaults, - requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise { // tslint:disable-line:no-any - return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, oAuth2Options); + async requestOAuth2( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, + oAuth2Options?: IOAuth2Options, + ): Promise { + return requestOAuth2.call( + this, + credentialsType, + requestOptions, + node, + additionalData, + oAuth2Options, + ); }, - requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any + async requestOAuth1( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions, + ): Promise { return requestOAuth1.call(this, credentialsType, requestOptions); }, returnJsonArray, }, }; })(workflow, node); - } diff --git a/packages/core/src/UserSettings.ts b/packages/core/src/UserSettings.ts index 13fce496f4..f3afc2b242 100644 --- a/packages/core/src/UserSettings.ts +++ b/packages/core/src/UserSettings.ts @@ -1,3 +1,11 @@ +/* eslint-disable no-param-reassign */ +/* eslint-disable @typescript-eslint/no-use-before-define */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import * as fs from 'fs'; +import * as path from 'path'; +import { randomBytes } from 'crypto'; +// eslint-disable-next-line import/no-cycle import { ENCRYPTION_KEY_ENV_OVERWRITE, EXTENSIONS_SUBDIRECTORY, @@ -7,20 +15,15 @@ import { USER_SETTINGS_SUBFOLDER, } from '.'; - -import * as fs from 'fs'; -import * as path from 'path'; -import { randomBytes } from 'crypto'; +// eslint-disable-next-line @typescript-eslint/no-var-requires const { promisify } = require('util'); + const fsAccess = promisify(fs.access); const fsReadFile = promisify(fs.readFile); const fsMkdir = promisify(fs.mkdir); const fsWriteFile = promisify(fs.writeFile); - - -let settingsCache: IUserSettings | undefined = undefined; - +let settingsCache: IUserSettings | undefined; /** * Creates the user settings if they do not exist yet @@ -49,12 +52,12 @@ export async function prepareUserSettings(): Promise { userSettings.encryptionKey = randomBytes(24).toString('base64'); } + // eslint-disable-next-line no-console console.log(`UserSettings were generated and saved to: ${settingsPath}`); return writeUserSettings(userSettings, settingsPath); } - /** * Returns the encryption key which is used to encrypt * the credentials. @@ -62,6 +65,7 @@ export async function prepareUserSettings(): Promise { * @export * @returns */ +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export async function getEncryptionKey() { if (process.env[ENCRYPTION_KEY_ENV_OVERWRITE] !== undefined) { return process.env[ENCRYPTION_KEY_ENV_OVERWRITE]; @@ -80,7 +84,6 @@ export async function getEncryptionKey() { return userSettings.encryptionKey; } - /** * Adds/Overwrite the given settings in the currently * saved user settings @@ -90,7 +93,10 @@ export async function getEncryptionKey() { * @param {string} [settingsPath] Optional settings file path * @returns {Promise} */ -export async function addToUserSettings(addSettings: IUserSettings, settingsPath?: string): Promise { +export async function addToUserSettings( + addSettings: IUserSettings, + settingsPath?: string, +): Promise { if (settingsPath === undefined) { settingsPath = getUserSettingsPath(); } @@ -107,7 +113,6 @@ export async function addToUserSettings(addSettings: IUserSettings, settingsPath return writeUserSettings(userSettings, settingsPath); } - /** * Writes a user settings file * @@ -116,7 +121,10 @@ export async function addToUserSettings(addSettings: IUserSettings, settingsPath * @param {string} [settingsPath] Optional settings file path * @returns {Promise} */ -export async function writeUserSettings(userSettings: IUserSettings, settingsPath?: string): Promise { +export async function writeUserSettings( + userSettings: IUserSettings, + settingsPath?: string, +): Promise { if (settingsPath === undefined) { settingsPath = getUserSettingsPath(); } @@ -139,14 +147,16 @@ export async function writeUserSettings(userSettings: IUserSettings, settingsPat return userSettings; } - /** * Returns the content of the user settings * * @export * @returns {UserSettings} */ -export async function getUserSettings(settingsPath?: string, ignoreCache?: boolean): Promise { +export async function getUserSettings( + settingsPath?: string, + ignoreCache?: boolean, +): Promise { if (settingsCache !== undefined && ignoreCache !== true) { return settingsCache; } @@ -167,13 +177,14 @@ export async function getUserSettings(settingsPath?: string, ignoreCache?: boole try { settingsCache = JSON.parse(settingsFile); } catch (error) { - throw new Error(`Error parsing n8n-config file "${settingsPath}". It does not seem to be valid JSON.`); + throw new Error( + `Error parsing n8n-config file "${settingsPath}". It does not seem to be valid JSON.`, + ); } return settingsCache as IUserSettings; } - /** * Returns the path to the user settings * @@ -186,8 +197,6 @@ export function getUserSettingsPath(): string { return path.join(n8nFolder, USER_SETTINGS_FILE_NAME); } - - /** * Retruns the path to the n8n folder in which all n8n * related data gets saved @@ -206,7 +215,6 @@ export function getUserN8nFolderPath(): string { return path.join(userFolder, USER_SETTINGS_SUBFOLDER); } - /** * Returns the path to the n8n user folder with the custom * extensions like nodes and credentials @@ -218,7 +226,6 @@ export function getUserN8nFolderCustomExtensionPath(): string { return path.join(getUserN8nFolderPath(), EXTENSIONS_SUBDIRECTORY); } - /** * Returns the home folder path of the user if * none can be found it falls back to the current diff --git a/packages/core/src/WorkflowExecute.ts b/packages/core/src/WorkflowExecute.ts index a3e78b022b..c9c00fbf1e 100644 --- a/packages/core/src/WorkflowExecute.ts +++ b/packages/core/src/WorkflowExecute.ts @@ -1,3 +1,14 @@ +/* eslint-disable @typescript-eslint/prefer-optional-chain */ +/* eslint-disable no-await-in-loop */ +/* eslint-disable no-labels */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable no-continue */ +/* eslint-disable no-prototype-builtins */ +/* eslint-disable no-restricted-syntax */ +/* eslint-disable no-param-reassign */ +/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ import * as PCancelable from 'p-cancelable'; import { @@ -20,23 +31,27 @@ import { WorkflowExecuteMode, WorkflowOperationError, } from 'n8n-workflow'; -import { - NodeExecuteFunctions, -} from './'; - +// eslint-disable-next-line import/no-extraneous-dependencies import { get } from 'lodash'; +// eslint-disable-next-line import/no-cycle +import { NodeExecuteFunctions } from '.'; export class WorkflowExecute { runExecutionData: IRunExecutionData; + private additionalData: IWorkflowExecuteAdditionalData; + private mode: WorkflowExecuteMode; - constructor(additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, runExecutionData?: IRunExecutionData) { + constructor( + additionalData: IWorkflowExecuteAdditionalData, + mode: WorkflowExecuteMode, + runExecutionData?: IRunExecutionData, + ) { this.additionalData = additionalData; this.mode = mode; this.runExecutionData = runExecutionData || { - startData: { - }, + startData: {}, resultData: { runData: {}, }, @@ -48,8 +63,6 @@ export class WorkflowExecute { }; } - - /** * Executes the given workflow. * @@ -59,7 +72,8 @@ export class WorkflowExecute { * @returns {(Promise)} * @memberof WorkflowExecute */ - run(workflow: Workflow, startNode?: INode, destinationNode?: string): PCancelable { + // @ts-ignore + async run(workflow: Workflow, startNode?: INode, destinationNode?: string): PCancelable { // Get the nodes to start workflow execution from startNode = startNode || workflow.getStartNode(destinationNode); @@ -68,7 +82,7 @@ export class WorkflowExecute { } // If a destination node is given we only run the direct parent nodes and no others - let runNodeFilter: string[] | undefined = undefined; + let runNodeFilter: string[] | undefined; if (destinationNode) { runNodeFilter = workflow.getParentNodes(destinationNode); runNodeFilter.push(destinationNode); @@ -108,8 +122,6 @@ export class WorkflowExecute { return this.processRunExecutionData(workflow); } - - /** * Executes the given workflow but only * @@ -121,7 +133,13 @@ export class WorkflowExecute { * @memberof WorkflowExecute */ // @ts-ignore - async runPartialWorkflow(workflow: Workflow, runData: IRunData, startNodes: string[], destinationNode: string): PCancelable { + async runPartialWorkflow( + workflow: Workflow, + runData: IRunData, + startNodes: string[], + destinationNode: string, + // @ts-ignore + ): PCancelable { let incomingNodeConnections: INodeConnections | undefined; let connection: IConnection; @@ -149,7 +167,8 @@ export class WorkflowExecute { for (let inputIndex = 0; inputIndex < connections.length; inputIndex++) { connection = connections[inputIndex]; incomingData.push( - runData[connection.node!][runIndex].data![connection.type][connection.index]!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + runData[connection.node][runIndex].data![connection.type][connection.index]!, ); } } @@ -182,11 +201,12 @@ export class WorkflowExecute { waitingExecution[destinationNode][runIndex][connection.type] = []; } - - if (runData[connection.node!] !== undefined) { + if (runData[connection.node] !== undefined) { // Input data exists so add as waiting // incomingDataDestination.push(runData[connection.node!][runIndex].data![connection.type][connection.index]); - waitingExecution[destinationNode][runIndex][connection.type].push(runData[connection.node!][runIndex].data![connection.type][connection.index]); + waitingExecution[destinationNode][runIndex][connection.type].push( + runData[connection.node][runIndex].data![connection.type][connection.index], + ); } else { waitingExecution[destinationNode][runIndex][connection.type].push(null); } @@ -196,7 +216,8 @@ export class WorkflowExecute { } // Only run the parent nodes and no others - let runNodeFilter: string[] | undefined = undefined; + let runNodeFilter: string[] | undefined; + // eslint-disable-next-line prefer-const runNodeFilter = workflow.getParentNodes(destinationNode); runNodeFilter.push(destinationNode); @@ -218,8 +239,6 @@ export class WorkflowExecute { return this.processRunExecutionData(workflow); } - - /** * Executes the hook with the given name * @@ -228,22 +247,31 @@ export class WorkflowExecute { * @returns {Promise} * @memberof WorkflowExecute */ - async executeHook(hookName: string, parameters: any[]): Promise { // tslint:disable-line:no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async executeHook(hookName: string, parameters: any[]): Promise { + // tslint:disable-line:no-any if (this.additionalData.hooks === undefined) { return; } + // eslint-disable-next-line consistent-return return this.additionalData.hooks.executeHookFunctions(hookName, parameters); } - /** * Checks the incoming connection does not receive any data */ - incomingConnectionIsEmpty(runData: IRunData, inputConnections: IConnection[], runIndex: number): boolean { + incomingConnectionIsEmpty( + runData: IRunData, + inputConnections: IConnection[], + runIndex: number, + ): boolean { // for (const inputConnection of workflow.connectionsByDestinationNode[nodeToAdd].main[0]) { for (const inputConnection of inputConnections) { - const nodeIncomingData = get(runData, `[${inputConnection.node}][${runIndex}].data.main[${inputConnection.index}]`); + const nodeIncomingData = get( + runData, + `[${inputConnection.node}][${runIndex}].data.main[${inputConnection.index}]`, + ); if (nodeIncomingData !== undefined && (nodeIncomingData as object[]).length !== 0) { return false; } @@ -251,79 +279,117 @@ export class WorkflowExecute { return true; } - - addNodeToBeExecuted(workflow: Workflow, connectionData: IConnection, outputIndex: number, parentNodeName: string, nodeSuccessData: INodeExecutionData[][], runIndex: number): void { + addNodeToBeExecuted( + workflow: Workflow, + connectionData: IConnection, + outputIndex: number, + parentNodeName: string, + nodeSuccessData: INodeExecutionData[][], + runIndex: number, + ): void { let stillDataMissing = false; // Check if node has multiple inputs as then we have to wait for all input data // to be present before we can add it to the node-execution-stack - if (workflow.connectionsByDestinationNode[connectionData.node]['main'].length > 1) { + if (workflow.connectionsByDestinationNode[connectionData.node].main.length > 1) { // Node has multiple inputs let nodeWasWaiting = true; // Check if there is already data for the node - if (this.runExecutionData.executionData!.waitingExecution[connectionData.node] === undefined) { + if ( + this.runExecutionData.executionData!.waitingExecution[connectionData.node] === undefined + ) { // Node does not have data yet so create a new empty one this.runExecutionData.executionData!.waitingExecution[connectionData.node] = {}; nodeWasWaiting = false; } - if (this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] === undefined) { + 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: [], }; - for (let i = 0; i < workflow.connectionsByDestinationNode[connectionData.node]['main'].length; i++) { - this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main.push(null); + for ( + let i = 0; + i < workflow.connectionsByDestinationNode[connectionData.node].main.length; + i++ + ) { + this.runExecutionData.executionData!.waitingExecution[connectionData.node][ + runIndex + ].main.push(null); } } // Add the new data if (nodeSuccessData === null) { - this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main[connectionData.index] = null; + this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main[ + connectionData.index + ] = null; } else { - this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main[connectionData.index] = nodeSuccessData[outputIndex]; + this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main[ + connectionData.index + ] = nodeSuccessData[outputIndex]; } // Check if all data exists now let thisExecutionData: INodeExecutionData[] | null; let allDataFound = true; - for (let i = 0; i < this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main.length; i++) { - thisExecutionData = this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main[i]; + for ( + let i = 0; + i < + this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main + .length; + i++ + ) { + thisExecutionData = + this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main[ + i + ]; if (thisExecutionData === null) { allDataFound = false; break; } } - if (allDataFound === true) { + if (allDataFound) { // All data exists for node to be executed // 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 delete this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex]; - if (Object.keys(this.runExecutionData.executionData!.waitingExecution[connectionData.node]).length === 0) { + if ( + Object.keys(this.runExecutionData.executionData!.waitingExecution[connectionData.node]) + .length === 0 + ) { // No more data left for the node so also delete that one delete this.runExecutionData.executionData!.waitingExecution[connectionData.node]; } return; - } else { - stillDataMissing = true; } + stillDataMissing = true; - if (nodeWasWaiting === false) { - + if (!nodeWasWaiting) { // Get a list of all the output nodes that we can check for siblings easier const checkOutputNodes = []; + // eslint-disable-next-line @typescript-eslint/no-for-in-array for (const outputIndexParent in workflow.connectionsBySourceNode[parentNodeName].main) { - if (!workflow.connectionsBySourceNode[parentNodeName].main.hasOwnProperty(outputIndexParent)) { + if ( + !workflow.connectionsBySourceNode[parentNodeName].main.hasOwnProperty(outputIndexParent) + ) { continue; } - for (const connectionDataCheck of workflow.connectionsBySourceNode[parentNodeName].main[outputIndexParent]) { + for (const connectionDataCheck of workflow.connectionsBySourceNode[parentNodeName].main[ + outputIndexParent + ]) { checkOutputNodes.push(connectionDataCheck.node); } } @@ -332,14 +398,22 @@ export class WorkflowExecute { // checked. So we have to go through all the inputs and check if they // are already on the list to be processed. // If that is not the case add it. - for (let inputIndex = 0; inputIndex < workflow.connectionsByDestinationNode[connectionData.node]['main'].length; inputIndex++) { - for (const inputData of workflow.connectionsByDestinationNode[connectionData.node]['main'][inputIndex]) { + for ( + let inputIndex = 0; + inputIndex < workflow.connectionsByDestinationNode[connectionData.node].main.length; + inputIndex++ + ) { + for (const inputData of workflow.connectionsByDestinationNode[connectionData.node].main[ + inputIndex + ]) { if (inputData.node === parentNodeName) { // Is the node we come from so its data will be available for sure continue; } - const executionStackNodes = this.runExecutionData.executionData!.nodeExecutionStack.map((stackData) => stackData.node.name); + const executionStackNodes = this.runExecutionData.executionData!.nodeExecutionStack.map( + (stackData) => stackData.node.name, + ); // Check if that node is also an output connection of the // previously processed one @@ -348,7 +422,13 @@ export class WorkflowExecute { // will then process this node next. So nothing to do // unless the incoming data of the node is empty // because then it would not be executed - if (!this.incomingConnectionIsEmpty(this.runExecutionData.resultData.runData, workflow.connectionsByDestinationNode[inputData.node].main[0], runIndex)) { + if ( + !this.incomingConnectionIsEmpty( + this.runExecutionData.resultData.runData, + workflow.connectionsByDestinationNode[inputData.node].main[0], + runIndex, + ) + ) { continue; } } @@ -401,7 +481,10 @@ export class WorkflowExecute { nodeToAdd = parentNode; } const parentNodesNodeToAdd = workflow.getParentNodes(nodeToAdd as string); - if (parentNodesNodeToAdd.includes(parentNodeName) && nodeSuccessData[outputIndex].length === 0) { + if ( + parentNodesNodeToAdd.includes(parentNodeName) && + nodeSuccessData[outputIndex].length === 0 + ) { // We do not add the node if there is no input data and the node that should be connected // is a child of the parent node. Because else it would run a node even though it should be // specifically not run, as it did not receive any data. @@ -418,30 +501,32 @@ export class WorkflowExecute { if (workflow.connectionsByDestinationNode[nodeToAdd] === undefined) { // Add empty item if the node does not have any input connections addEmptyItem = true; - } else { - if (this.incomingConnectionIsEmpty(this.runExecutionData.resultData.runData, workflow.connectionsByDestinationNode[nodeToAdd].main[0], runIndex)) { - // Add empty item also if the input data is empty - addEmptyItem = true; - } + } else if ( + this.incomingConnectionIsEmpty( + this.runExecutionData.resultData.runData, + workflow.connectionsByDestinationNode[nodeToAdd].main[0], + runIndex, + ) + ) { + // Add empty item also if the input data is empty + addEmptyItem = true; } - if (addEmptyItem === true) { + if (addEmptyItem) { // Add only node if it does not have any inputs because else it will // be added by its input node later anyway. - this.runExecutionData.executionData!.nodeExecutionStack.push( - { - node: workflow.getNode(nodeToAdd) as INode, - data: { - main: [ - [ - { - json: {}, - }, - ], + this.runExecutionData.executionData!.nodeExecutionStack.push({ + node: workflow.getNode(nodeToAdd) as INode, + data: { + main: [ + [ + { + json: {}, + }, ], - }, + ], }, - ); + }); } } } @@ -461,9 +546,11 @@ export class WorkflowExecute { connectionDataArray[connectionData.index] = nodeSuccessData[outputIndex]; } - if (stillDataMissing === true) { + if (stillDataMissing) { // Additional data is needed to run node so add it to waiting - if (!this.runExecutionData.executionData!.waitingExecution.hasOwnProperty(connectionData.node)) { + if ( + !this.runExecutionData.executionData!.waitingExecution.hasOwnProperty(connectionData.node) + ) { this.runExecutionData.executionData!.waitingExecution[connectionData.node] = {}; } this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] = { @@ -480,7 +567,6 @@ export class WorkflowExecute { } } - /** * Runs the given execution data. * @@ -488,14 +574,17 @@ export class WorkflowExecute { * @returns {Promise} * @memberof WorkflowExecute */ - processRunExecutionData(workflow: Workflow): PCancelable { + // @ts-ignore + async processRunExecutionData(workflow: Workflow): PCancelable { Logger.verbose('Workflow execution started', { workflowId: workflow.id }); const startedAt = new Date(); const workflowIssues = workflow.checkReadyForExecution(); if (workflowIssues !== null) { - throw new Error('The workflow has issues and can for that reason not be executed. Please fix them first.'); + throw new Error( + 'The workflow has issues and can for that reason not be executed. Please fix them first.', + ); } // Variables which hold temporary data for each node-execution @@ -521,7 +610,7 @@ export class WorkflowExecute { let currentExecutionTry = ''; let lastExecutionTry = ''; - return new PCancelable((resolve, reject, onCancel) => { + return new PCancelable(async (resolve, reject, onCancel) => { let gotCancel = false; onCancel.shouldReject = false; @@ -533,7 +622,6 @@ export class WorkflowExecute { try { await this.executeHook('workflowExecuteBefore', [workflow]); } catch (error) { - // Set the error that it can be saved correctly executionError = { ...error, @@ -542,16 +630,17 @@ export class WorkflowExecute { }; // Set the incoming data of the node that it can be saved correctly - executionData = this.runExecutionData.executionData!.nodeExecutionStack[0] as IExecuteData; + // eslint-disable-next-line prefer-destructuring + executionData = this.runExecutionData.executionData!.nodeExecutionStack[0]; this.runExecutionData.resultData = { runData: { [executionData.node.name]: [ { startTime, - executionTime: (new Date().getTime()) - startTime, - data: ({ - 'main': executionData.data.main, - } as ITaskDataConnections), + executionTime: new Date().getTime() - startTime, + data: { + main: executionData.data.main, + } as ITaskDataConnections, }, ], }, @@ -562,24 +651,31 @@ export class WorkflowExecute { throw error; } - executionLoop: - while (this.runExecutionData.executionData!.nodeExecutionStack.length !== 0) { - - if (this.additionalData.executionTimeoutTimestamp !== undefined && Date.now() >= this.additionalData.executionTimeoutTimestamp) { + executionLoop: while ( + this.runExecutionData.executionData!.nodeExecutionStack.length !== 0 + ) { + if ( + this.additionalData.executionTimeoutTimestamp !== undefined && + Date.now() >= this.additionalData.executionTimeoutTimestamp + ) { gotCancel = true; } // @ts-ignore - if (gotCancel === true) { + if (gotCancel) { return Promise.resolve(); } nodeSuccessData = null; executionError = undefined; - executionData = this.runExecutionData.executionData!.nodeExecutionStack.shift() as IExecuteData; + executionData = + this.runExecutionData.executionData!.nodeExecutionStack.shift() as IExecuteData; executionNode = executionData.node; - Logger.debug(`Start processing node "${executionNode.name}"`, { node: executionNode.name, workflowId: workflow.id }); + Logger.debug(`Start processing node "${executionNode.name}"`, { + node: executionNode.name, + workflowId: workflow.id, + }); await this.executeHook('nodeExecuteBefore', [executionNode.name]); // Get the index of the current run @@ -594,7 +690,10 @@ export class WorkflowExecute { throw new Error('Did stop execution because execution seems to be in endless loop.'); } - if (this.runExecutionData.startData!.runNodeFilter !== undefined && this.runExecutionData.startData!.runNodeFilter!.indexOf(executionNode.name) === -1) { + if ( + this.runExecutionData.startData!.runNodeFilter !== undefined && + this.runExecutionData.startData!.runNodeFilter.indexOf(executionNode.name) === -1 + ) { // If filter is set and node is not on filter skip it, that avoids the problem that it executes // leafs that are parallel to a selected destinationNode. Normally it would execute them because // they have the same parent and it executes all child nodes. @@ -608,17 +707,24 @@ export class WorkflowExecute { let inputConnections: IConnection[][]; let connectionIndex: number; - inputConnections = workflow.connectionsByDestinationNode[executionNode.name]['main']; + // eslint-disable-next-line prefer-const + inputConnections = workflow.connectionsByDestinationNode[executionNode.name].main; - for (connectionIndex = 0; connectionIndex < inputConnections.length; connectionIndex++) { - if (workflow.getHighestNode(executionNode.name, 'main', connectionIndex).length === 0) { + for ( + connectionIndex = 0; + connectionIndex < inputConnections.length; + connectionIndex++ + ) { + if ( + workflow.getHighestNode(executionNode.name, 'main', connectionIndex).length === 0 + ) { // If there is no valid incoming node (if all are disabled) // then ignore that it has inputs and simply execute it as it is without // any data continue; } - if (!executionData.data!.hasOwnProperty('main')) { + if (!executionData.data.hasOwnProperty('main')) { // ExecutionData does not even have the connection set up so can // not have that data, so add it again to be executed later this.runExecutionData.executionData!.nodeExecutionStack.push(executionData); @@ -629,7 +735,10 @@ export class WorkflowExecute { // Check if it has the data for all the inputs // The most nodes just have one but merge node for example has two and data // of both inputs has to be available to be able to process the node. - if (executionData.data!.main!.length < connectionIndex || executionData.data!.main![connectionIndex] === null) { + if ( + executionData.data.main!.length < connectionIndex || + executionData.data.main![connectionIndex] === null + ) { // Does not have the data of the connections so add back to stack this.runExecutionData.executionData!.nodeExecutionStack.push(executionData); lastExecutionTry = currentExecutionTry; @@ -653,22 +762,25 @@ export class WorkflowExecute { let waitBetweenTries = 0; if (executionData.node.retryOnFail === true) { // TODO: Remove the hardcoded default-values here and also in NodeSettings.vue - waitBetweenTries = Math.min(5000, Math.max(0, executionData.node.waitBetweenTries || 1000)); + waitBetweenTries = Math.min( + 5000, + Math.max(0, executionData.node.waitBetweenTries || 1000), + ); } for (let tryIndex = 0; tryIndex < maxTries; tryIndex++) { // @ts-ignore - if (gotCancel === true) { + if (gotCancel) { return Promise.resolve(); } try { - if (tryIndex !== 0) { // Reset executionError from previous error try executionError = undefined; if (waitBetweenTries !== 0) { // TODO: Improve that in the future and check if other nodes can // be executed in the meantime + // eslint-disable-next-line @typescript-eslint/no-shadow await new Promise((resolve) => { setTimeout(() => { resolve(undefined); @@ -677,9 +789,23 @@ export class WorkflowExecute { } } - Logger.debug(`Running node "${executionNode.name}" started`, { node: executionNode.name, workflowId: workflow.id }); - nodeSuccessData = await workflow.runNode(executionData.node, executionData.data, this.runExecutionData, runIndex, this.additionalData, NodeExecuteFunctions, this.mode); - Logger.debug(`Running node "${executionNode.name}" finished successfully`, { node: executionNode.name, workflowId: workflow.id }); + Logger.debug(`Running node "${executionNode.name}" started`, { + node: executionNode.name, + workflowId: workflow.id, + }); + nodeSuccessData = await workflow.runNode( + executionData.node, + executionData.data, + this.runExecutionData, + runIndex, + this.additionalData, + NodeExecuteFunctions, + this.mode, + ); + Logger.debug(`Running node "${executionNode.name}" finished successfully`, { + node: executionNode.name, + workflowId: workflow.id, + }); if (nodeSuccessData === undefined) { // Node did not get executed @@ -699,7 +825,7 @@ export class WorkflowExecute { } } - if (nodeSuccessData === null && !this.runExecutionData.waitTill!!) { + if (nodeSuccessData === null && !this.runExecutionData.waitTill!) { // If null gets returned it means that the node did succeed // but did not have any data. So the branch should end // (meaning the nodes afterwards should not be processed) @@ -708,7 +834,6 @@ export class WorkflowExecute { break; } catch (error) { - this.runExecutionData.resultData.lastNodeExecuted = executionData.node.name; executionError = { @@ -717,7 +842,10 @@ export class WorkflowExecute { stack: error.stack, }; - Logger.debug(`Running node "${executionNode.name}" finished with error`, { node: executionNode.name, workflowId: workflow.id }); + Logger.debug(`Running node "${executionNode.name}" finished with error`, { + node: executionNode.name, + workflowId: workflow.id, + }); } } @@ -729,7 +857,7 @@ export class WorkflowExecute { } taskData = { startTime, - executionTime: (new Date().getTime()) - startTime, + executionTime: new Date().getTime() - startTime, }; if (executionError !== undefined) { @@ -741,7 +869,7 @@ export class WorkflowExecute { // Simply get the input data of the node if it has any and pass it through // to the next node if (executionData.data.main[0] !== null) { - nodeSuccessData = [executionData.data.main[0] as INodeExecutionData[]]; + nodeSuccessData = [executionData.data.main[0]]; } } } else { @@ -751,30 +879,46 @@ export class WorkflowExecute { // Add the execution data again so that it can get restarted this.runExecutionData.executionData!.nodeExecutionStack.unshift(executionData); - await this.executeHook('nodeExecuteAfter', [executionNode.name, taskData, this.runExecutionData]); + await this.executeHook('nodeExecuteAfter', [ + executionNode.name, + taskData, + this.runExecutionData, + ]); break; } } // Node executed successfully. So add data and go on. - taskData.data = ({ - 'main': nodeSuccessData, - } as ITaskDataConnections); + taskData.data = { + main: nodeSuccessData, + } as ITaskDataConnections; this.runExecutionData.resultData.runData[executionNode.name].push(taskData); - if (this.runExecutionData.startData && this.runExecutionData.startData.destinationNode && this.runExecutionData.startData.destinationNode === executionNode.name) { + if ( + this.runExecutionData.startData && + this.runExecutionData.startData.destinationNode && + this.runExecutionData.startData.destinationNode === executionNode.name + ) { // Before stopping, make sure we are executing hooks so // That frontend is notified for example for manual executions. - await this.executeHook('nodeExecuteAfter', [executionNode.name, taskData, this.runExecutionData]); + await this.executeHook('nodeExecuteAfter', [ + executionNode.name, + taskData, + this.runExecutionData, + ]); // If destination node is defined and got executed stop execution continue; } - if (this.runExecutionData.waitTill!!) { - await this.executeHook('nodeExecuteAfter', [executionNode.name, taskData, this.runExecutionData]); + if (this.runExecutionData.waitTill!) { + await this.executeHook('nodeExecuteAfter', [ + executionNode.name, + taskData, + this.runExecutionData, + ]); // Add the node back to the stack that the workflow can start to execute again from that node this.runExecutionData.executionData!.nodeExecutionStack.unshift(executionData); @@ -786,24 +930,46 @@ export class WorkflowExecute { // be executed next if (workflow.connectionsBySourceNode.hasOwnProperty(executionNode.name)) { if (workflow.connectionsBySourceNode[executionNode.name].hasOwnProperty('main')) { - let outputIndex: string, connectionData: IConnection; + let outputIndex: string; + let connectionData: IConnection; // Iterate over all the outputs // Add the nodes to be executed - for (outputIndex in workflow.connectionsBySourceNode[executionNode.name]['main']) { - if (!workflow.connectionsBySourceNode[executionNode.name]['main'].hasOwnProperty(outputIndex)) { + // eslint-disable-next-line @typescript-eslint/no-for-in-array + for (outputIndex in workflow.connectionsBySourceNode[executionNode.name].main) { + if ( + !workflow.connectionsBySourceNode[executionNode.name].main.hasOwnProperty( + outputIndex, + ) + ) { continue; } // Iterate over all the different connections of this output - for (connectionData of workflow.connectionsBySourceNode[executionNode.name]['main'][outputIndex]) { + for (connectionData of workflow.connectionsBySourceNode[executionNode.name].main[ + outputIndex + ]) { if (!workflow.nodes.hasOwnProperty(connectionData.node)) { - return Promise.reject(new Error(`The node "${executionNode.name}" connects to not found node "${connectionData.node}"`)); + return Promise.reject( + new Error( + `The node "${executionNode.name}" connects to not found node "${connectionData.node}"`, + ), + ); } - if (nodeSuccessData![outputIndex] && (nodeSuccessData![outputIndex].length !== 0 || connectionData.index > 0)) { + if ( + nodeSuccessData![outputIndex] && + (nodeSuccessData![outputIndex].length !== 0 || connectionData.index > 0) + ) { // Add the node only if it did execute or if connected to second "optional" input - this.addNodeToBeExecuted(workflow, connectionData, parseInt(outputIndex, 10), executionNode.name, nodeSuccessData!, runIndex); + this.addNodeToBeExecuted( + workflow, + connectionData, + parseInt(outputIndex, 10), + executionNode.name, + nodeSuccessData!, + runIndex, + ); } } } @@ -814,58 +980,79 @@ export class WorkflowExecute { // Execute hooks now to make sure that all hooks are executed properly // Await is needed to make sure that we don't fall into concurrency problems // When saving node execution data - await this.executeHook('nodeExecuteAfter', [executionNode.name, taskData, this.runExecutionData]); - + await this.executeHook('nodeExecuteAfter', [ + executionNode.name, + taskData, + this.runExecutionData, + ]); } return Promise.resolve(); })() - .then(async () => { - if (gotCancel && executionError === undefined) { - return this.processSuccessExecution(startedAt, workflow, new WorkflowOperationError('Workflow has been canceled or timed out!')); - } - return this.processSuccessExecution(startedAt, workflow, executionError); - }) - .catch(async (error) => { - const fullRunData = this.getFullRunData(startedAt); + .then(async () => { + if (gotCancel && executionError === undefined) { + return this.processSuccessExecution( + startedAt, + workflow, + new WorkflowOperationError('Workflow has been canceled or timed out!'), + ); + } + return this.processSuccessExecution(startedAt, workflow, executionError); + }) + .catch(async (error) => { + const fullRunData = this.getFullRunData(startedAt); - fullRunData.data.resultData.error = { - ...error, - message: error.message, - stack: error.stack, - }; + fullRunData.data.resultData.error = { + ...error, + message: error.message, + stack: error.stack, + }; - // Check if static data changed - let newStaticData: IDataObject | undefined; - if (workflow.staticData.__dataChanged === true) { - // Static data of workflow changed - newStaticData = workflow.staticData; - } - await this.executeHook('workflowExecuteAfter', [fullRunData, newStaticData]).catch(error => { - console.error('There was a problem running hook "workflowExecuteAfter"', error); + // Check if static data changed + let newStaticData: IDataObject | undefined; + // eslint-disable-next-line no-underscore-dangle + if (workflow.staticData.__dataChanged === true) { + // Static data of workflow changed + newStaticData = workflow.staticData; + } + await this.executeHook('workflowExecuteAfter', [fullRunData, newStaticData]).catch( + // eslint-disable-next-line @typescript-eslint/no-shadow + (error) => { + // eslint-disable-next-line no-console + console.error('There was a problem running hook "workflowExecuteAfter"', error); + }, + ); + + return fullRunData; }); - return fullRunData; - }); - return returnPromise.then(resolve); }); } - - // @ts-ignore - async processSuccessExecution(startedAt: Date, workflow: Workflow, executionError?: ExecutionError): PCancelable { + async processSuccessExecution( + startedAt: Date, + workflow: Workflow, + executionError?: ExecutionError, + // @ts-ignore + ): PCancelable { const fullRunData = this.getFullRunData(startedAt); if (executionError !== undefined) { - Logger.verbose(`Workflow execution finished with error`, { error: executionError, workflowId: workflow.id }); + Logger.verbose(`Workflow execution finished with error`, { + error: executionError, + workflowId: workflow.id, + }); fullRunData.data.resultData.error = { ...executionError, message: executionError.message, stack: executionError.stack, } as ExecutionError; - } else if (this.runExecutionData.waitTill!!) { - Logger.verbose(`Workflow execution will wait until ${this.runExecutionData.waitTill}`, { workflowId: workflow.id }); + } else if (this.runExecutionData.waitTill!) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + Logger.verbose(`Workflow execution will wait until ${this.runExecutionData.waitTill}`, { + workflowId: workflow.id, + }); fullRunData.waitTill = this.runExecutionData.waitTill; } else { Logger.verbose(`Workflow execution finished successfully`, { workflowId: workflow.id }); @@ -874,6 +1061,7 @@ export class WorkflowExecute { // Check if static data changed let newStaticData: IDataObject | undefined; + // eslint-disable-next-line no-underscore-dangle if (workflow.staticData.__dataChanged === true) { // Static data of workflow changed newStaticData = workflow.staticData; @@ -894,5 +1082,4 @@ export class WorkflowExecute { return fullRunData; } - } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 55ab02a529..15d1cce2b8 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,8 +1,12 @@ -try { - require('source-map-support').install(); -} catch (error) { +/* eslint-disable import/no-cycle */ +import * as NodeExecuteFunctions from './NodeExecuteFunctions'; +import * as UserSettings from './UserSettings'; -} +try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, import/no-extraneous-dependencies, global-require, @typescript-eslint/no-var-requires + require('source-map-support').install(); + // eslint-disable-next-line no-empty +} catch (error) {} export * from './ActiveWorkflows'; export * from './ActiveWebhooks'; @@ -13,10 +17,4 @@ export * from './Interfaces'; export * from './LoadNodeParameterOptions'; export * from './NodeExecuteFunctions'; export * from './WorkflowExecute'; - -import * as NodeExecuteFunctions from './NodeExecuteFunctions'; -import * as UserSettings from './UserSettings'; -export { - NodeExecuteFunctions, - UserSettings, -}; +export { NodeExecuteFunctions, UserSettings }; diff --git a/packages/core/test/Credentials.test.ts b/packages/core/test/Credentials.test.ts index 5fbcfc7be1..c5fe7f57c8 100644 --- a/packages/core/test/Credentials.test.ts +++ b/packages/core/test/Credentials.test.ts @@ -1,88 +1,83 @@ - import { Credentials } from '../src'; describe('Credentials', () => { + describe('without nodeType set', () => { + test('should be able to set and read key data without initial data set', () => { + const credentials = new Credentials('testName', 'testType', []); - describe('without nodeType set', () => { + const key = 'key1'; + const password = 'password'; + // const nodeType = 'base.noOp'; + const newData = 1234; - test('should be able to set and read key data without initial data set', () => { - - const credentials = new Credentials('testName', 'testType', []); - - const key = 'key1'; - const password = 'password'; - // const nodeType = 'base.noOp'; - const newData = 1234; - - credentials.setDataKey(key, newData, password); - - expect(credentials.getDataKey(key, password)).toEqual(newData); - }); - - test('should be able to set and read key data with initial data set', () => { - - const key = 'key2'; - const password = 'password'; - - // Saved under "key1" - const initialData = 4321; - const initialDataEncoded = 'U2FsdGVkX1+0baznXt+Ag/ub8A2kHLyoLxn/rR9h4XQ='; - - const credentials = new Credentials('testName', 'testType', [], initialDataEncoded); - - const newData = 1234; - - // 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); - }); + credentials.setDataKey(key, newData, password); + expect(credentials.getDataKey(key, password)).toEqual(newData); }); - describe('with nodeType set', () => { + test('should be able to set and read key data with initial data set', () => { + const key = 'key2'; + const password = 'password'; - test('should be able to set and read key data without initial data set', () => { + // Saved under "key1" + const initialData = 4321; + const initialDataEncoded = 'U2FsdGVkX1+0baznXt+Ag/ub8A2kHLyoLxn/rR9h4XQ='; - const nodeAccess = [ - { - nodeType: 'base.noOp', - user: 'userName', - date: new Date(), - }, - ]; + const credentials = new Credentials('testName', 'testType', [], initialDataEncoded); - const credentials = new Credentials('testName', 'testType', nodeAccess); + const newData = 1234; - const key = 'key1'; - const password = 'password'; - const nodeType = 'base.noOp'; - const newData = 1234; + // Set and read new data + credentials.setDataKey(key, newData, password); + expect(credentials.getDataKey(key, password)).toEqual(newData); - credentials.setDataKey(key, newData, password); - - // 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".'); - } - - // 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)); - }); + // Read the data which got provided encrypted on init + expect(credentials.getDataKey('key1', password)).toEqual(initialData); }); + }); + describe('with nodeType 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 credentials = new Credentials('testName', 'testType', nodeAccess); + + const key = 'key1'; + const password = 'password'; + const nodeType = 'base.noOp'; + const newData = 1234; + + credentials.setDataKey(key, newData, password); + + // 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".', + ); + } + + // 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 ce59151abc..5af6b8793e 100644 --- a/packages/core/test/Helpers.ts +++ b/packages/core/test/Helpers.ts @@ -18,30 +18,27 @@ import { WorkflowHooks, } from 'n8n-workflow'; -import { - Credentials, - IDeferredPromise, - IExecuteFunctions, -} from '../src'; - +import { Credentials, IDeferredPromise, IExecuteFunctions } from '../src'; export class CredentialsHelper extends ICredentialsHelper { getDecrypted(name: string, type: string): Promise { - return new Promise(res => res({})); + return new Promise((res) => res({})); } getCredentials(name: string, type: string): Promise { - return new Promise(res => { + return new Promise((res) => { res(new Credentials('', '', [], '')); }); } - async updateCredentials(name: string, type: string, data: ICredentialDataDecryptedObject): Promise {} + async updateCredentials( + name: string, + type: string, + data: ICredentialDataDecryptedObject, + ): Promise {} } - class NodeTypesClass implements INodeTypes { - nodeTypes: INodeTypeData = { 'n8n-nodes-base.if': { sourcePath: '', @@ -161,9 +158,7 @@ class NodeTypesClass implements INodeTypes { type: 'number', displayOptions: { hide: { - operation: [ - 'isEmpty', - ], + operation: ['isEmpty'], }, }, default: 0, @@ -229,10 +224,7 @@ class NodeTypesClass implements INodeTypes { type: 'string', displayOptions: { hide: { - operation: [ - 'isEmpty', - 'regex', - ], + operation: ['isEmpty', 'regex'], }, }, default: '', @@ -244,9 +236,7 @@ class NodeTypesClass implements INodeTypes { type: 'string', displayOptions: { show: { - operation: [ - 'regex', - ], + operation: ['regex'], }, }, default: '', @@ -274,7 +264,8 @@ class NodeTypesClass implements INodeTypes { }, ], default: 'all', - description: 'If multiple rules got set this settings decides if it is true as soon as ANY condition matches or only if ALL get meet.', + description: + 'If multiple rules got set this settings decides if it is true as soon as ANY condition matches or only if ALL get meet.', }, ], }, @@ -291,19 +282,30 @@ class NodeTypesClass implements INodeTypes { const compareOperationFunctions: { [key: string]: (value1: NodeParameterValue, value2: NodeParameterValue) => boolean; } = { - contains: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || '').toString().includes((value2 || '').toString()), - notContains: (value1: NodeParameterValue, value2: NodeParameterValue) => !(value1 || '').toString().includes((value2 || '').toString()), - endsWith: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 as string).endsWith(value2 as string), + contains: (value1: NodeParameterValue, value2: NodeParameterValue) => + (value1 || '').toString().includes((value2 || '').toString()), + notContains: (value1: NodeParameterValue, value2: NodeParameterValue) => + !(value1 || '').toString().includes((value2 || '').toString()), + endsWith: (value1: NodeParameterValue, value2: NodeParameterValue) => + (value1 as string).endsWith(value2 as string), equal: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 === value2, notEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 !== value2, - larger: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) > (value2 || 0), - largerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) >= (value2 || 0), - smaller: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) < (value2 || 0), - smallerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) <= (value2 || 0), - startsWith: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 as string).startsWith(value2 as string), - isEmpty: (value1: NodeParameterValue) => [undefined, null, ''].includes(value1 as string), + larger: (value1: NodeParameterValue, value2: NodeParameterValue) => + (value1 || 0) > (value2 || 0), + largerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => + (value1 || 0) >= (value2 || 0), + smaller: (value1: NodeParameterValue, value2: NodeParameterValue) => + (value1 || 0) < (value2 || 0), + smallerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => + (value1 || 0) <= (value2 || 0), + startsWith: (value1: NodeParameterValue, value2: NodeParameterValue) => + (value1 as string).startsWith(value2 as string), + isEmpty: (value1: NodeParameterValue) => + [undefined, null, ''].includes(value1 as string), regex: (value1: NodeParameterValue, value2: NodeParameterValue) => { - const regexMatch = (value2 || '').toString().match(new RegExp('^/(.*?)/([gimusy]*)$')); + const regexMatch = (value2 || '') + .toString() + .match(new RegExp('^/(.*?)/([gimusy]*)$')); let regex: RegExp; if (!regexMatch) { @@ -319,18 +321,13 @@ class NodeTypesClass implements INodeTypes { }; // The different dataTypes to check the values in - const dataTypes = [ - 'boolean', - 'number', - 'string', - ]; + const dataTypes = ['boolean', 'number', 'string']; // Itterate over all items to check which ones should be output as via output "true" and // which ones via output "false" let dataType: string; let compareOperationResult: boolean; - itemLoop: - for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { + itemLoop: for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { item = items[itemIndex]; let compareData: INodeParameters; @@ -340,9 +337,16 @@ class NodeTypesClass implements INodeTypes { // Check all the values of the different dataTypes for (dataType of dataTypes) { // Check all the values of the current dataType - for (compareData of this.getNodeParameter(`conditions.${dataType}`, itemIndex, []) as INodeParameters[]) { + for (compareData of this.getNodeParameter( + `conditions.${dataType}`, + itemIndex, + [], + ) as INodeParameters[]) { // Check if the values passes - compareOperationResult = compareOperationFunctions[compareData.operation as string](compareData.value1 as NodeParameterValue, compareData.value2 as NodeParameterValue); + compareOperationResult = compareOperationFunctions[compareData.operation as string]( + compareData.value1 as NodeParameterValue, + compareData.value2 as NodeParameterValue, + ); if (compareOperationResult === true && combineOperation === 'any') { // If it passes and the operation is "any" we do not have to check any @@ -397,21 +401,25 @@ class NodeTypesClass implements INodeTypes { { name: 'Append', value: 'append', - description: 'Combines data of both inputs. The output will contain items of input 1 and input 2.', + description: + 'Combines data of both inputs. The output will contain items of input 1 and input 2.', }, { name: 'Pass-through', value: 'passThrough', - description: 'Passes through data of one input. The output will conain only items of the defined input.', + description: + 'Passes through data of one input. The output will conain only items of the defined input.', }, { name: 'Wait', value: 'wait', - description: 'Waits till data of both inputs is available and will then output a single empty item.', + description: + 'Waits till data of both inputs is available and will then output a single empty item.', }, ], default: 'append', - description: 'How data should be merged. If it should simply
be appended or merged depending on a property.', + description: + 'How data should be merged. If it should simply
be appended or merged depending on a property.', }, { displayName: 'Output Data', @@ -419,9 +427,7 @@ class NodeTypesClass implements INodeTypes { type: 'options', displayOptions: { show: { - mode: [ - 'passThrough', - ], + mode: ['passThrough'], }, }, options: [ @@ -512,7 +518,8 @@ class NodeTypesClass implements INodeTypes { name: 'keepOnlySet', type: 'boolean', default: false, - description: 'If only the values set on this node should be
kept and all others removed.', + description: + 'If only the values set on this node should be
kept and all others removed.', }, { displayName: 'Values to Set', @@ -534,7 +541,8 @@ class NodeTypesClass implements INodeTypes { name: 'name', type: 'string', default: 'propertyName', - description: 'Name of the property to write data to.
Supports dot-notation.
Example: "data.person[0].name"', + description: + 'Name of the property to write data to.
Supports dot-notation.
Example: "data.person[0].name"', }, { displayName: 'Value', @@ -554,7 +562,8 @@ class NodeTypesClass implements INodeTypes { name: 'name', type: 'string', default: 'propertyName', - description: 'Name of the property to write data to.
Supports dot-notation.
Example: "data.person[0].name"', + description: + 'Name of the property to write data to.
Supports dot-notation.
Example: "data.person[0].name"', }, { displayName: 'Value', @@ -574,7 +583,8 @@ class NodeTypesClass implements INodeTypes { name: 'name', type: 'string', default: 'propertyName', - description: 'Name of the property to write data to.
Supports dot-notation.
Example: "data.person[0].name"', + description: + 'Name of the property to write data to.
Supports dot-notation.
Example: "data.person[0].name"', }, { displayName: 'Value', @@ -610,7 +620,6 @@ class NodeTypesClass implements INodeTypes { ], }, execute(this: IExecuteFunctions): Promise { - const items = this.getInputData(); if (items.length === 0) { @@ -643,31 +652,37 @@ class NodeTypesClass implements INodeTypes { } // Add boolean values - (this.getNodeParameter('values.boolean', itemIndex, []) as INodeParameters[]).forEach((setItem) => { - if (options.dotNotation === false) { - newItem.json[setItem.name as string] = !!setItem.value; - } else { - set(newItem.json, setItem.name as string, !!setItem.value); - } - }); + (this.getNodeParameter('values.boolean', itemIndex, []) as INodeParameters[]).forEach( + (setItem) => { + if (options.dotNotation === false) { + newItem.json[setItem.name as string] = !!setItem.value; + } else { + set(newItem.json, setItem.name as string, !!setItem.value); + } + }, + ); // Add number values - (this.getNodeParameter('values.number', itemIndex, []) as INodeParameters[]).forEach((setItem) => { - if (options.dotNotation === false) { - newItem.json[setItem.name as string] = setItem.value; - } else { - set(newItem.json, setItem.name as string, setItem.value); - } - }); + (this.getNodeParameter('values.number', itemIndex, []) as INodeParameters[]).forEach( + (setItem) => { + if (options.dotNotation === false) { + newItem.json[setItem.name as string] = setItem.value; + } else { + set(newItem.json, setItem.name as string, setItem.value); + } + }, + ); // Add string values - (this.getNodeParameter('values.string', itemIndex, []) as INodeParameters[]).forEach((setItem) => { - if (options.dotNotation === false) { - newItem.json[setItem.name as string] = setItem.value; - } else { - set(newItem.json, setItem.name as string, setItem.value); - } - }); + (this.getNodeParameter('values.string', itemIndex, []) as INodeParameters[]).forEach( + (setItem) => { + if (options.dotNotation === false) { + newItem.json[setItem.name as string] = setItem.value; + } else { + set(newItem.json, setItem.name as string, setItem.value); + } + }, + ); returnData.push(newItem); } @@ -702,7 +717,7 @@ class NodeTypesClass implements INodeTypes { }, }; - async init(nodeTypes: INodeTypeData): Promise { } + async init(nodeTypes: INodeTypeData): Promise {} getAll(): INodeType[] { return Object.values(this.nodeTypes).map((data) => data.type); @@ -715,7 +730,6 @@ class NodeTypesClass implements INodeTypes { let nodeTypesInstance: NodeTypesClass | undefined; - export function NodeTypes(): NodeTypesClass { if (nodeTypesInstance === undefined) { nodeTypesInstance = new NodeTypesClass(); @@ -725,8 +739,10 @@ export function NodeTypes(): NodeTypesClass { return nodeTypesInstance; } - -export function WorkflowExecuteAdditionalData(waitPromise: IDeferredPromise, nodeExecutionOrder: string[]): IWorkflowExecuteAdditionalData { +export function WorkflowExecuteAdditionalData( + waitPromise: IDeferredPromise, + nodeExecutionOrder: string[], +): IWorkflowExecuteAdditionalData { const hookFunctions = { nodeExecuteAfter: [ async (nodeName: string, data: ITaskData): Promise => { @@ -752,7 +768,7 @@ export function WorkflowExecuteAdditionalData(waitPromise: IDeferredPromise => {}, // tslint:disable-line:no-any + executeWorkflow: async (workflowInfo: IExecuteWorkflowInfo): Promise => {}, sendMessageToUI: (message: string) => {}, restApiUrl: '', encryptionKey: 'test', diff --git a/packages/core/test/WorkflowExecute.test.ts b/packages/core/test/WorkflowExecute.test.ts index 2bbdb83be8..364fb23d5a 100644 --- a/packages/core/test/WorkflowExecute.test.ts +++ b/packages/core/test/WorkflowExecute.test.ts @@ -1,39 +1,25 @@ +import { IConnections, ILogger, INode, IRun, LoggerProxy, Workflow } from 'n8n-workflow'; -import { - IConnections, - ILogger, - INode, - IRun, - LoggerProxy, - Workflow, -} from 'n8n-workflow'; - -import { - createDeferredPromise, - WorkflowExecute, -} from '../src'; +import { createDeferredPromise, WorkflowExecute } from '../src'; import * as Helpers from './Helpers'; - describe('WorkflowExecute', () => { - describe('run', () => { - const tests: Array<{ description: string; input: { workflowData: { - nodes: INode[], - connections: IConnections, - } - }, + nodes: INode[]; + connections: IConnections; + }; + }; output: { nodeExecutionOrder: string[]; nodeData: { - [key: string]: any[][]; // tslint:disable-line:no-any + [key: string]: any[][]; }; - }, + }; }> = [ { description: 'should run basic two node workflow', @@ -41,45 +27,39 @@ describe('WorkflowExecute', () => { // Leave the workflowData in regular JSON to be able to easily // copy it from/in the UI workflowData: { - "nodes": [ + nodes: [ { - "parameters": {}, - "name": "Start", - "type": "n8n-nodes-base.start", - "typeVersion": 1, - "position": [ - 100, - 300, - ], + parameters: {}, + name: 'Start', + type: 'n8n-nodes-base.start', + typeVersion: 1, + position: [100, 300], }, { - "parameters": { - "values": { - "number": [ + parameters: { + values: { + number: [ { - "name": "value1", - "value": 1, + name: 'value1', + value: 1, }, ], }, }, - "name": "Set", - "type": "n8n-nodes-base.set", - "typeVersion": 1, - "position": [ - 280, - 300, - ], + name: 'Set', + type: 'n8n-nodes-base.set', + typeVersion: 1, + position: [280, 300], }, ], - "connections": { - "Start": { - "main": [ + connections: { + Start: { + main: [ [ { - "node": "Set", - "type": "main", - "index": 0, + node: 'Set', + type: 'main', + index: 0, }, ], ], @@ -88,10 +68,7 @@ describe('WorkflowExecute', () => { }, }, output: { - nodeExecutionOrder: [ - 'Start', - 'Set', - ], + nodeExecutionOrder: ['Start', 'Set'], nodeData: { Set: [ [ @@ -109,80 +86,71 @@ describe('WorkflowExecute', () => { // Leave the workflowData in regular JSON to be able to easily // copy it from/in the UI workflowData: { - "nodes": [ + nodes: [ { - "parameters": {}, - "name": "Start", - "type": "n8n-nodes-base.start", - "typeVersion": 1, - "position": [ - 100, - 300, - ], + parameters: {}, + name: 'Start', + type: 'n8n-nodes-base.start', + typeVersion: 1, + position: [100, 300], }, { - "parameters": { - "values": { - "number": [ + parameters: { + values: { + number: [ { - "name": "value1", - "value": 1, + name: 'value1', + value: 1, }, ], }, }, - "name": "Set1", - "type": "n8n-nodes-base.set", - "typeVersion": 1, - "position": [ - 300, - 250, - ], + name: 'Set1', + type: 'n8n-nodes-base.set', + typeVersion: 1, + position: [300, 250], }, { - "parameters": { - "values": { - "number": [ + parameters: { + values: { + number: [ { - "name": "value2", - "value": 2, + name: 'value2', + value: 2, }, ], }, }, - "name": "Set2", - "type": "n8n-nodes-base.set", - "typeVersion": 1, - "position": [ - 500, - 400, - ], + name: 'Set2', + type: 'n8n-nodes-base.set', + typeVersion: 1, + position: [500, 400], }, ], - "connections": { - "Start": { - "main": [ + connections: { + Start: { + main: [ [ { - "node": "Set1", - "type": "main", - "index": 0, + node: 'Set1', + type: 'main', + index: 0, }, { - "node": "Set2", - "type": "main", - "index": 0, + node: 'Set2', + type: 'main', + index: 0, }, ], ], }, - "Set1": { - "main": [ + Set1: { + main: [ [ { - "node": "Set2", - "type": "main", - "index": 0, + node: 'Set2', + type: 'main', + index: 0, }, ], ], @@ -191,12 +159,7 @@ describe('WorkflowExecute', () => { }, }, output: { - nodeExecutionOrder: [ - 'Start', - 'Set1', - 'Set2', - 'Set2', - ], + nodeExecutionOrder: ['Start', 'Set1', 'Set2', 'Set2'], nodeData: { Set1: [ [ @@ -227,256 +190,226 @@ describe('WorkflowExecute', () => { // Leave the workflowData in regular JSON to be able to easily // copy it from/in the UI workflowData: { - "nodes": [ + nodes: [ { - "parameters": { - "mode": "passThrough", + parameters: { + mode: 'passThrough', }, - "name": "Merge4", - "type": "n8n-nodes-base.merge", - "typeVersion": 1, - "position": [ - 1150, - 500, - ], + name: 'Merge4', + type: 'n8n-nodes-base.merge', + typeVersion: 1, + position: [1150, 500], }, { - "parameters": { - "values": { - "number": [ + parameters: { + values: { + number: [ { - "name": "value2", - "value": 2, + name: 'value2', + value: 2, }, ], }, }, - "name": "Set2", - "type": "n8n-nodes-base.set", - "typeVersion": 1, - "position": [ - 290, - 400, - ], + name: 'Set2', + type: 'n8n-nodes-base.set', + typeVersion: 1, + position: [290, 400], }, { - "parameters": { - "values": { - "number": [ + parameters: { + values: { + number: [ { - "name": "value4", - "value": 4, + name: 'value4', + value: 4, }, ], }, }, - "name": "Set4", - "type": "n8n-nodes-base.set", - "typeVersion": 1, - "position": [ - 850, - 200, - ], + name: 'Set4', + type: 'n8n-nodes-base.set', + typeVersion: 1, + position: [850, 200], }, { - "parameters": { - "values": { - "number": [ + parameters: { + values: { + number: [ { - "name": "value3", - "value": 3, + name: 'value3', + value: 3, }, ], }, }, - "name": "Set3", - "type": "n8n-nodes-base.set", - "typeVersion": 1, - "position": [ - 650, - 200, - ], + name: 'Set3', + type: 'n8n-nodes-base.set', + typeVersion: 1, + position: [650, 200], }, { - "parameters": { - "mode": "passThrough", + parameters: { + mode: 'passThrough', }, - "name": "Merge4", - "type": "n8n-nodes-base.merge", - "typeVersion": 1, - "position": [ - 1150, - 500, - ], + name: 'Merge4', + type: 'n8n-nodes-base.merge', + typeVersion: 1, + position: [1150, 500], }, { - "parameters": {}, - "name": "Merge3", - "type": "n8n-nodes-base.merge", - "typeVersion": 1, - "position": [ - 1000, - 400, - ], + parameters: {}, + name: 'Merge3', + type: 'n8n-nodes-base.merge', + typeVersion: 1, + position: [1000, 400], }, { - "parameters": { - "mode": "passThrough", - "output": "input2", + parameters: { + mode: 'passThrough', + output: 'input2', }, - "name": "Merge2", - "type": "n8n-nodes-base.merge", - "typeVersion": 1, - "position": [ - 700, - 400, - ], + name: 'Merge2', + type: 'n8n-nodes-base.merge', + typeVersion: 1, + position: [700, 400], }, { - "parameters": {}, - "name": "Merge1", - "type": "n8n-nodes-base.merge", - "typeVersion": 1, - "position": [ - 500, - 300, - ], + parameters: {}, + name: 'Merge1', + type: 'n8n-nodes-base.merge', + typeVersion: 1, + position: [500, 300], }, { - "parameters": { - "values": { - "number": [ + parameters: { + values: { + number: [ { - "name": "value1", - "value": 1, + name: 'value1', + value: 1, }, ], }, }, - "name": "Set1", - "type": "n8n-nodes-base.set", - "typeVersion": 1, - "position": [ - 300, - 200, - ], + name: 'Set1', + type: 'n8n-nodes-base.set', + typeVersion: 1, + position: [300, 200], }, { - "parameters": {}, - "name": "Start", - "type": "n8n-nodes-base.start", - "typeVersion": 1, - "position": [ - 100, - 300, - ], + parameters: {}, + name: 'Start', + type: 'n8n-nodes-base.start', + typeVersion: 1, + position: [100, 300], }, ], - "connections": { - "Set2": { - "main": [ + connections: { + Set2: { + main: [ [ { - "node": "Merge1", - "type": "main", - "index": 1, + node: 'Merge1', + type: 'main', + index: 1, }, { - "node": "Merge2", - "type": "main", - "index": 1, + node: 'Merge2', + type: 'main', + index: 1, }, ], ], }, - "Set4": { - "main": [ + Set4: { + main: [ [ { - "node": "Merge3", - "type": "main", - "index": 0, + node: 'Merge3', + type: 'main', + index: 0, }, ], ], }, - "Set3": { - "main": [ + Set3: { + main: [ [ { - "node": "Set4", - "type": "main", - "index": 0, + node: 'Set4', + type: 'main', + index: 0, }, ], ], }, - "Merge3": { - "main": [ + Merge3: { + main: [ [ { - "node": "Merge4", - "type": "main", - "index": 0, + node: 'Merge4', + type: 'main', + index: 0, }, ], ], }, - "Merge2": { - "main": [ + Merge2: { + main: [ [ { - "node": "Merge3", - "type": "main", - "index": 1, + node: 'Merge3', + type: 'main', + index: 1, }, ], ], }, - "Merge1": { - "main": [ + Merge1: { + main: [ [ { - "node": "Merge2", - "type": "main", - "index": 0, + node: 'Merge2', + type: 'main', + index: 0, }, ], ], }, - "Set1": { - "main": [ + Set1: { + main: [ [ { - "node": "Merge1", - "type": "main", - "index": 0, + node: 'Merge1', + type: 'main', + index: 0, }, { - "node": "Set3", - "type": "main", - "index": 0, + node: 'Set3', + type: 'main', + index: 0, }, ], ], }, - "Start": { - "main": [ + Start: { + main: [ [ { - "node": "Set1", - "type": "main", - "index": 0, + node: 'Set1', + type: 'main', + index: 0, }, { - "node": "Set2", - "type": "main", - "index": 0, + node: 'Set2', + type: 'main', + index: 0, }, { - "node": "Merge4", - "type": "main", - "index": 1, + node: 'Merge4', + type: 'main', + index: 1, }, ], ], @@ -573,146 +506,132 @@ describe('WorkflowExecute', () => { }, }, { - description: 'should run workflow also if node has multiple input connections and one is empty', + description: + 'should run workflow also if node has multiple input connections and one is empty', input: { // Leave the workflowData in regular JSON to be able to easily // copy it from/in the UI workflowData: { - "nodes": [ + nodes: [ { - "parameters": {}, - "name": "Start", - "type": "n8n-nodes-base.start", - "typeVersion": 1, - "position": [ - 250, - 450, - ], + parameters: {}, + name: 'Start', + type: 'n8n-nodes-base.start', + typeVersion: 1, + position: [250, 450], }, { - "parameters": { - "conditions": { - "boolean": [], - "number": [ + parameters: { + conditions: { + boolean: [], + number: [ { - "value1": "={{Object.keys($json).length}}", - "operation": "notEqual", + value1: '={{Object.keys($json).length}}', + operation: 'notEqual', }, ], }, }, - "name": "IF", - "type": "n8n-nodes-base.if", - "typeVersion": 1, - "position": [ - 650, - 350, - ], + name: 'IF', + type: 'n8n-nodes-base.if', + typeVersion: 1, + position: [650, 350], }, { - "parameters": {}, - "name": "Merge1", - "type": "n8n-nodes-base.merge", - "typeVersion": 1, - "position": [ - 1150, - 450, - ], + parameters: {}, + name: 'Merge1', + type: 'n8n-nodes-base.merge', + typeVersion: 1, + position: [1150, 450], }, { - "parameters": { - "values": { - "string": [ + parameters: { + values: { + string: [ { - "name": "test1", - "value": "a", + name: 'test1', + value: 'a', }, ], }, - "options": {}, + options: {}, }, - "name": "Set1", - "type": "n8n-nodes-base.set", - "typeVersion": 1, - "position": [ - 450, - 450, - ], + name: 'Set1', + type: 'n8n-nodes-base.set', + typeVersion: 1, + position: [450, 450], }, { - "parameters": { - "values": { - "string": [ + parameters: { + values: { + string: [ { - "name": "test2", - "value": "b", + name: 'test2', + value: 'b', }, ], }, - "options": {}, + options: {}, }, - "name": "Set2", - "type": "n8n-nodes-base.set", - "typeVersion": 1, - "position": [ - 800, - 250, - ], + name: 'Set2', + type: 'n8n-nodes-base.set', + typeVersion: 1, + position: [800, 250], }, ], - "connections": { - "Start": { - "main": [ + connections: { + Start: { + main: [ [ { - "node": "Set1", - "type": "main", - "index": 0, + node: 'Set1', + type: 'main', + index: 0, }, ], ], }, - "IF": { - "main": [ + IF: { + main: [ [ { - "node": "Set2", - "type": "main", - "index": 0, + node: 'Set2', + type: 'main', + index: 0, }, ], [ { - "node": "Merge1", - "type": "main", - "index": 0, + node: 'Merge1', + type: 'main', + index: 0, }, ], ], }, - "Set1": { - "main": [ + Set1: { + main: [ [ { - "node": "IF", - "type": "main", - "index": 0, + node: 'IF', + type: 'main', + index: 0, }, { - "node": "Merge1", - "type": "main", - "index": 1, + node: 'Merge1', + type: 'main', + index: 1, }, ], ], }, - "Set2": { - "main": [ + Set2: { + main: [ [ { - "node": "Merge1", - "type": "main", - "index": 0, + node: 'Merge1', + type: 'main', + index: 0, }, ], ], @@ -721,13 +640,7 @@ describe('WorkflowExecute', () => { }, }, output: { - nodeExecutionOrder: [ - 'Start', - 'Set1', - 'IF', - 'Set2', - 'Merge1', - ], + nodeExecutionOrder: ['Start', 'Set1', 'IF', 'Set2', 'Merge1'], nodeData: { Merge1: [ [ @@ -749,204 +662,183 @@ describe('WorkflowExecute', () => { // Leave the workflowData in regular JSON to be able to easily // copy it from/in the UI workflowData: { - "nodes": [ + nodes: [ { - "parameters": {}, - "name": "Start", - "type": "n8n-nodes-base.start", - "typeVersion": 1, - "position": [ - 250, - 300, - ], + parameters: {}, + name: 'Start', + type: 'n8n-nodes-base.start', + typeVersion: 1, + position: [250, 300], }, { - "parameters": {}, - "name": "Merge", - "type": "n8n-nodes-base.merge", - "typeVersion": 1, - "position": [ - 800, - 450, - ], + parameters: {}, + name: 'Merge', + type: 'n8n-nodes-base.merge', + typeVersion: 1, + position: [800, 450], }, { - "parameters": {}, - "name": "Merge1", - "type": "n8n-nodes-base.merge", - "typeVersion": 1, - "position": [ - 1000, - 300, - ], + parameters: {}, + name: 'Merge1', + type: 'n8n-nodes-base.merge', + typeVersion: 1, + position: [1000, 300], }, { - "parameters": { - "conditions": { - "boolean": [ + parameters: { + conditions: { + boolean: [ { - "value2": true, + value2: true, }, ], - "string": [ + string: [ { - "value1": "={{$json[\"key\"]}}", - "value2": "a", + value1: '={{$json["key"]}}', + value2: 'a', }, ], }, - "combineOperation": "any", + combineOperation: 'any', }, - "name": "IF", - "type": "n8n-nodes-base.if", - "typeVersion": 1, - "position": [ - 600, - 600, - ], - "alwaysOutputData": false, + name: 'IF', + type: 'n8n-nodes-base.if', + typeVersion: 1, + position: [600, 600], + alwaysOutputData: false, }, { - "parameters": { - "values": { - "number": [ + parameters: { + values: { + number: [ { - "name": "number0", + name: 'number0', }, ], - "string": [ + string: [ { - "name": "key", - "value": "a", + name: 'key', + value: 'a', }, ], }, - "options": {}, + options: {}, }, - "name": "Set0", - "type": "n8n-nodes-base.set", - "typeVersion": 1, - "position": [ - 450, - 300, - ], + name: 'Set0', + type: 'n8n-nodes-base.set', + typeVersion: 1, + position: [450, 300], }, { - "parameters": { - "values": { - "number": [ + parameters: { + values: { + number: [ { - "name": "number1", - "value": 1, + name: 'number1', + value: 1, }, ], - "string": [ + string: [ { - "name": "key", - "value": "b", + name: 'key', + value: 'b', }, ], }, - "options": {}, + options: {}, }, - "name": "Set1", - "type": "n8n-nodes-base.set", - "typeVersion": 1, - "position": [ - 450, - 450, - ], + name: 'Set1', + type: 'n8n-nodes-base.set', + typeVersion: 1, + position: [450, 450], }, { - "parameters": { - "values": { - "number": [ + parameters: { + values: { + number: [ { - "name": "number2", - "value": 2, + name: 'number2', + value: 2, }, ], - "string": [ + string: [ { - "name": "key", - "value": "c", + name: 'key', + value: 'c', }, ], }, - "options": {}, + options: {}, }, - "name": "Set2", - "type": "n8n-nodes-base.set", - "typeVersion": 1, - "position": [ - 450, - 600, - ], + name: 'Set2', + type: 'n8n-nodes-base.set', + typeVersion: 1, + position: [450, 600], }, ], - "connections": { - "Start": { - "main": [ + connections: { + Start: { + main: [ [ { - "node": "Set0", - "type": "main", - "index": 0, + node: 'Set0', + type: 'main', + index: 0, }, ], ], }, - "Merge": { - "main": [ + Merge: { + main: [ [ { - "node": "Merge1", - "type": "main", - "index": 1, + node: 'Merge1', + type: 'main', + index: 1, }, ], ], }, - "IF": { - "main": [ + IF: { + main: [ [ { - "node": "Merge", - "type": "main", - "index": 1, + node: 'Merge', + type: 'main', + index: 1, }, ], ], }, - "Set0": { - "main": [ + Set0: { + main: [ [ { - "node": "Merge1", - "type": "main", - "index": 0, + node: 'Merge1', + type: 'main', + index: 0, }, ], ], }, - "Set1": { - "main": [ + Set1: { + main: [ [ { - "node": "Merge", - "type": "main", - "index": 0, + node: 'Merge', + type: 'main', + index: 0, }, ], ], }, - "Set2": { - "main": [ + Set2: { + main: [ [ { - "node": "IF", - "type": "main", - "index": 0, + node: 'IF', + type: 'main', + index: 0, }, ], ], @@ -955,21 +847,13 @@ describe('WorkflowExecute', () => { }, }, output: { - nodeExecutionOrder: [ - 'Start', - 'Set0', - 'Set2', - 'IF', - 'Set1', - 'Merge', - 'Merge1', - ], + nodeExecutionOrder: ['Start', 'Set0', 'Set2', 'IF', 'Set1', 'Merge', 'Merge1'], nodeData: { Merge: [ [ { number1: 1, - key: "b", + key: 'b', }, ], ], @@ -977,11 +861,11 @@ describe('WorkflowExecute', () => { [ { number0: 0, - key: "a", + key: 'a', }, { number1: 1, - key: "b", + key: 'b', }, ], ], @@ -989,142 +873,128 @@ describe('WorkflowExecute', () => { }, }, { - description: 'should use empty data if input of sibling does not receive any data from parent', + description: + 'should use empty data if input of sibling does not receive any data from parent', input: { // Leave the workflowData in regular JSON to be able to easily // copy it from/in the UI workflowData: { - "nodes": [ + nodes: [ { - "parameters": {}, - "name": "Start", - "type": "n8n-nodes-base.start", - "typeVersion": 1, - "position": [ - 250, - 300, - ], + parameters: {}, + name: 'Start', + type: 'n8n-nodes-base.start', + typeVersion: 1, + position: [250, 300], }, { - "parameters": { - "conditions": { - "number": [ + parameters: { + conditions: { + number: [ { - "value1": "={{$json[\"value1\"]}}", - "operation": "equal", - "value2": 1, + value1: '={{$json["value1"]}}', + operation: 'equal', + value2: 1, }, ], }, }, - "name": "IF", - "type": "n8n-nodes-base.if", - "typeVersion": 1, - "position": [ - 650, - 300, - ], + name: 'IF', + type: 'n8n-nodes-base.if', + typeVersion: 1, + position: [650, 300], }, { - "parameters": { - "values": { - "string": [], - "number": [ + parameters: { + values: { + string: [], + number: [ { - "name": "value2", - "value": 2, + name: 'value2', + value: 2, }, ], }, - "options": {}, + options: {}, }, - "name": "Set2", - "type": "n8n-nodes-base.set", - "typeVersion": 1, - "position": [ - 850, - 450, - ], + name: 'Set2', + type: 'n8n-nodes-base.set', + typeVersion: 1, + position: [850, 450], }, { - "parameters": { - "values": { - "number": [ + parameters: { + values: { + number: [ { - "name": "value1", - "value": 1, + name: 'value1', + value: 1, }, ], }, - "options": {}, + options: {}, }, - "name": "Set1", - "type": "n8n-nodes-base.set", - "typeVersion": 1, - "position": [ - 450, - 300, - ], + name: 'Set1', + type: 'n8n-nodes-base.set', + typeVersion: 1, + position: [450, 300], }, { - "parameters": {}, - "name": "Merge", - "type": "n8n-nodes-base.merge", - "typeVersion": 1, - "position": [ - 1050, - 300, - ], + parameters: {}, + name: 'Merge', + type: 'n8n-nodes-base.merge', + typeVersion: 1, + position: [1050, 300], }, ], - "connections": { - "Start": { - "main": [ + connections: { + Start: { + main: [ [ { - "node": "Set1", - "type": "main", - "index": 0, + node: 'Set1', + type: 'main', + index: 0, }, ], ], }, - "IF": { - "main": [ + IF: { + main: [ [ { - "node": "Merge", - "type": "main", - "index": 0, + node: 'Merge', + type: 'main', + index: 0, }, ], [ { - "node": "Set2", - "type": "main", - "index": 0, + node: 'Set2', + type: 'main', + index: 0, }, ], ], }, - "Set2": { - "main": [ + Set2: { + main: [ [ { - "node": "Merge", - "type": "main", - "index": 1, + node: 'Merge', + type: 'main', + index: 1, }, ], ], }, - "Set1": { - "main": [ + Set1: { + main: [ [ { - "node": "IF", - "type": "main", - "index": 0, + node: 'IF', + type: 'main', + index: 0, }, ], ], @@ -1133,13 +1003,7 @@ describe('WorkflowExecute', () => { }, }, output: { - nodeExecutionOrder: [ - 'Start', - 'Set1', - 'IF', - 'Set2', - 'Merge', - ], + nodeExecutionOrder: ['Start', 'Set1', 'IF', 'Set2', 'Merge'], nodeData: { Merge: [ [ @@ -1160,140 +1024,122 @@ describe('WorkflowExecute', () => { // Leave the workflowData in regular JSON to be able to easily // copy it from/in the UI workflowData: { - "nodes": [ + nodes: [ { - "parameters": {}, - "name": "Start", - "type": "n8n-nodes-base.start", - "typeVersion": 1, - "position": [ - 250, - 300, - ], + parameters: {}, + name: 'Start', + type: 'n8n-nodes-base.start', + typeVersion: 1, + position: [250, 300], }, { - "parameters": { - "values": { - "number": [ + parameters: { + values: { + number: [ { - "name": "value1", + name: 'value1', }, ], }, - "options": {}, + options: {}, }, - "name": "Set", - "type": "n8n-nodes-base.set", - "typeVersion": 1, - "position": [ - 450, - 300, - ], + name: 'Set', + type: 'n8n-nodes-base.set', + typeVersion: 1, + position: [450, 300], }, { - "parameters": {}, - "name": "Merge", - "type": "n8n-nodes-base.merge", - "typeVersion": 1, - "position": [ - 1050, - 250, - ], + parameters: {}, + name: 'Merge', + type: 'n8n-nodes-base.merge', + typeVersion: 1, + position: [1050, 250], }, { - "parameters": { - "conditions": { - "number": [ + parameters: { + conditions: { + number: [ { - "value1": "={{$json[\"value1\"]}}", - "operation": "equal", - "value2": 1, + value1: '={{$json["value1"]}}', + operation: 'equal', + value2: 1, }, ], }, }, - "name": "IF", - "type": "n8n-nodes-base.if", - "typeVersion": 1, - "position": [ - 650, - 300, - ], + name: 'IF', + type: 'n8n-nodes-base.if', + typeVersion: 1, + position: [650, 300], }, { - "parameters": {}, - "name": "NoOpTrue", - "type": "n8n-nodes-base.noOp", - "typeVersion": 1, - "position": [ - 850, - 150, - ], + parameters: {}, + name: 'NoOpTrue', + type: 'n8n-nodes-base.noOp', + typeVersion: 1, + position: [850, 150], }, { - "parameters": {}, - "name": "NoOpFalse", - "type": "n8n-nodes-base.noOp", - "typeVersion": 1, - "position": [ - 850, - 400, - ], + parameters: {}, + name: 'NoOpFalse', + type: 'n8n-nodes-base.noOp', + typeVersion: 1, + position: [850, 400], }, ], - "connections": { - "Start": { - "main": [ + connections: { + Start: { + main: [ [ { - "node": "Set", - "type": "main", - "index": 0, + node: 'Set', + type: 'main', + index: 0, }, ], ], }, - "Set": { - "main": [ + Set: { + main: [ [ { - "node": "IF", - "type": "main", - "index": 0, + node: 'IF', + type: 'main', + index: 0, }, ], ], }, - "IF": { - "main": [ + IF: { + main: [ [ { - "node": "NoOpTrue", - "type": "main", - "index": 0, + node: 'NoOpTrue', + type: 'main', + index: 0, }, { - "node": "Merge", - "type": "main", - "index": 1, + node: 'Merge', + type: 'main', + index: 1, }, ], [ { - "node": "NoOpFalse", - "type": "main", - "index": 0, + node: 'NoOpFalse', + type: 'main', + index: 0, }, ], ], }, - "NoOpTrue": { - "main": [ + NoOpTrue: { + main: [ [ { - "node": "Merge", - "type": "main", - "index": 0, + node: 'Merge', + type: 'main', + index: 0, }, ], ], @@ -1302,16 +1148,9 @@ describe('WorkflowExecute', () => { }, }, output: { - nodeExecutionOrder: [ - 'Start', - 'Set', - 'IF', - 'NoOpFalse', - ], + nodeExecutionOrder: ['Start', 'Set', 'IF', 'NoOpFalse'], nodeData: { - IF: [ - [], - ], + IF: [[]], NoOpFalse: [ [ { @@ -1326,11 +1165,11 @@ describe('WorkflowExecute', () => { const fakeLogger = { log: () => {}, - debug: () => {}, - verbose: () => {}, - info: () => {}, - warn: () => {}, - error: () => {}, + debug: () => {}, + verbose: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, } as ILogger; const executionMode = 'manual'; @@ -1339,12 +1178,20 @@ describe('WorkflowExecute', () => { for (const testData of tests) { test(testData.description, async () => { - - const workflowInstance = new Workflow({ id: 'test', nodes: testData.input.workflowData.nodes, connections: testData.input.workflowData.connections, active: false, nodeTypes }); + const workflowInstance = new Workflow({ + id: 'test', + nodes: testData.input.workflowData.nodes, + connections: testData.input.workflowData.connections, + active: false, + nodeTypes, + }); const waitPromise = await createDeferredPromise(); const nodeExecutionOrder: string[] = []; - const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise, nodeExecutionOrder); + const additionalData = Helpers.WorkflowExecuteAdditionalData( + waitPromise, + nodeExecutionOrder, + ); const workflowExecute = new WorkflowExecute(additionalData, executionMode); @@ -1366,7 +1213,7 @@ describe('WorkflowExecute', () => { if (nodeData.data === undefined) { return null; } - return nodeData.data.main[0]!.map((entry) => entry.json ); + return nodeData.data.main[0]!.map((entry) => entry.json); }); // expect(resultData).toEqual(testData.output.nodeData[nodeName]); @@ -1382,7 +1229,5 @@ describe('WorkflowExecute', () => { expect(result.data.executionData!.nodeExecutionStack).toEqual([]); }); } - }); - }); diff --git a/packages/design-system/package.json b/packages/design-system/package.json index 399ab82047..173f56f087 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -20,11 +20,10 @@ "build:storybook": "build-storybook", "storybook": "start-storybook -p 6006", "test:unit": "vue-cli-service test:unit --passWithNoTests", - "lint": "vue-cli-service lint", + "lint": "tslint -p tsconfig.json -c tslint.json", + "lintfix": "tslint --fix -p tsconfig.json -c tslint.json", "build:theme": "gulp build:theme", - "watch:theme": "gulp watch:theme", - "tslint": "tslint -p tsconfig.json -c tslint.json", - "tslintfix": "tslint --fix -p tsconfig.json -c tslint.json" + "watch:theme": "gulp watch:theme" }, "peerDependencies": { "@fortawesome/fontawesome-svg-core": "1.x", @@ -49,27 +48,27 @@ "@storybook/addon-links": "^6.3.6", "@storybook/vue": "^6.3.6", "@types/jest": "^26.0.13", - "@typescript-eslint/eslint-plugin": "^4.18.0", - "@typescript-eslint/parser": "^4.18.0", + "@typescript-eslint/eslint-plugin": "^4.29.0", + "@typescript-eslint/parser": "^4.29.0", "@vue/cli-plugin-babel": "~4.5.0", "@vue/cli-plugin-eslint": "~4.5.0", - "@vue/cli-plugin-typescript": "~4.5.0", + "@vue/cli-plugin-typescript": "~4.5.6", "@vue/cli-plugin-unit-jest": "~4.5.0", "@vue/cli-service": "~4.5.0", "@vue/eslint-config-prettier": "^6.0.0", "@vue/eslint-config-typescript": "^7.0.0", "@vue/test-utils": "^1.0.3", "babel-loader": "^8.2.2", - "eslint": "^6.8.0", - "eslint-plugin-prettier": "^3.3.1", - "eslint-plugin-vue": "^6.2.2", + "eslint": "^7.32.0", + "eslint-plugin-prettier": "^3.4.0", + "eslint-plugin-vue": "^7.16.0", "fibers": "^5.0.0", "gulp": "^4.0.0", - "prettier": "^2.2.1", + "prettier": "^2.3.2", "sass": "^1.26.5", "sass-loader": "^8.0.2", "storybook-addon-designs": "^6.0.1", - "typescript": "~3.9.7", + "typescript": "~4.3.5", "vue-loader": "^15.9.7", "vue-template-compiler": "^2.6.11", "gulp-autoprefixer": "^4.0.0", diff --git a/packages/editor-ui/.editorconfig b/packages/editor-ui/.editorconfig deleted file mode 100644 index 9e24325d41..0000000000 --- a/packages/editor-ui/.editorconfig +++ /dev/null @@ -1,15 +0,0 @@ -root = true - -[*] -charset = utf-8 -indent_style = tab -end_of_line = lf -insert_final_newline = true -trim_trailing_whitespace = true - -[package.json] -indent_style = space -indent_size = 2 - -[*.ts] -quote_type = single diff --git a/packages/editor-ui/babel.config.js b/packages/editor-ui/babel.config.js index b78ce80044..0ee6ce4bbe 100644 --- a/packages/editor-ui/babel.config.js +++ b/packages/editor-ui/babel.config.js @@ -6,5 +6,8 @@ module.exports = { // transpileDependencies: [ // /\/node_modules\/quill/ // ] + plugins: [ + "@babel/plugin-proposal-class-properties", + ], }; // // https://stackoverflow.com/questions/44625868/es6-babel-class-constructor-cannot-be-invoked-without-new diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 1b4f190d98..df1b1c3c34 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -16,11 +16,11 @@ "scripts": { "build": "cross-env VUE_APP_PUBLIC_PATH=\"/%BASE_PATH%/\" vue-cli-service build", "dev": "npm run serve", - "lint": "vue-cli-service lint", + "format": "cd ../.. && node_modules/prettier/bin-prettier.js packages/editor-ui/**/**.ts --write", + "lint": "tslint -p tsconfig.json -c tslint.json", + "lintfix": "tslint --fix -p tsconfig.json -c tslint.json", "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" }, @@ -44,11 +44,11 @@ "@types/node": "^14.14.40", "@types/quill": "^2.0.1", "@types/uuid": "^8.3.0", - "@typescript-eslint/eslint-plugin": "^4.18.0", - "@typescript-eslint/parser": "^4.18.0", + "@typescript-eslint/eslint-plugin": "^4.29.0", + "@typescript-eslint/parser": "^4.29.0", "@vue/cli-plugin-babel": "~4.5.0", "@vue/cli-plugin-eslint": "~4.5.0", - "@vue/cli-plugin-typescript": "~4.5.0", + "@vue/cli-plugin-typescript": "~4.5.6", "@vue/cli-plugin-unit-jest": "~4.5.0", "@vue/cli-service": "~4.5.0", "@vue/eslint-config-standard": "^5.0.1", @@ -60,9 +60,9 @@ "cross-env": "^7.0.2", "dateformat": "^3.0.3", "element-ui": "~2.13.0", - "eslint": "^6.8.0", - "eslint-plugin-import": "^2.19.1", - "eslint-plugin-vue": "^6.2.2", + "eslint": "^7.32.0", + "eslint-plugin-import": "^2.23.4", + "eslint-plugin-vue": "^7.16.0", "file-saver": "^2.0.2", "flatted": "^2.0.0", "jquery": "^3.4.1", @@ -81,7 +81,7 @@ "string-template-parser": "^1.2.6", "ts-jest": "^26.3.0", "tslint": "^6.1.2", - "typescript": "~3.9.7", + "typescript": "~4.3.5", "uuid": "^8.3.0", "vue": "^2.6.11", "vue-cli-plugin-webpack-bundle-analyzer": "^2.0.0", diff --git a/packages/editor-ui/src/components/VersionCard.vue b/packages/editor-ui/src/components/VersionCard.vue index 2d77dd8438..3ae29e09be 100644 --- a/packages/editor-ui/src/components/VersionCard.vue +++ b/packages/editor-ui/src/components/VersionCard.vue @@ -1,4 +1,5 @@