🎨 Set up linting and formatting (#2120)

* ⬆️ Upgrade TS to 4.3.5

* 👕 Add ESLint configs

* 🎨 Add Prettier config

* 📦 Add deps and commands

*  Adjust global .editorconfig to new ruleset

* 🔥 Remove unneeded local .editorconfig

* 📦 Update deps in editor-ui

* 🔨 Limit Prettier to only TS files

*  Add recommended VSCode extensions

* 👕 Fix build

* 🔥 Remove Vue setting from global config

*  Disable prefer-default-export per feedback

* ✏️ Add forgotten divider

* 👕 Disable no-plusplus

* 👕 Disable class-methods-use-this

* ✏️ Alphabetize overrides

* 👕 Add one-var consecutive override

*  Revert one-var consecutive override

This reverts commit b9252cf935.

* 🎨 👕 Lint and format workflow package (#2121)

* 🎨 Format /workflow package

* 👕 Lint /workflow package

* 🎨 Re-format /workflow package

* 👕 Re-lint /workflow package

* ✏️ Fix typo

*  Consolidate if-checks

* 🔥 Remove prefer-default-export exceptions

* 🔥 Remove no-plusplus exceptions

* 🔥 Remove class-methods-use-this exceptions

* 🎨 👕 Lint and format node-dev package (#2122)

* 🎨 Format /node-dev package

*  Exclude templates from ESLint config

This keeps the templates consistent with the codebase while preventing lint exceptions from being made part of the templates.

* 👕 Lint /node-dev package

* 🔥 Remove prefer-default-export exceptions

* 🔥 Remove no-plusplus exceptions

* 🎨 👕 Lint and format core package (#2123)

* 🎨 Format /core package

* 👕 Lint /core package

* 🎨 Re-format /core package

* 👕 Re-lint /core package

* 🔥 Remove prefer-default-export exceptions

* 🔥 Remove no-plusplus exceptions

* 🔥 Remove class-methods-use-this exceptions

* 🎨 👕 Lint and format cli package (#2124)

* 🎨 Format /cli package

* 👕 Exclude migrations from linting

* 👕 Lint /cli package

* 🎨 Re-format /cli package

* 👕 Re-lint /cli package

* 👕 Fix build

* 🔥 Remove prefer-default-export exceptions

*  Update exceptions in ActiveExecutions

* 🔥 Remove no-plusplus exceptions

* 🔥 Remove class-methods-use-this exceptions

* 👕 fix lint issues

* 🔧 use package specific linter, remove tslint command

* 🔨 resolve build issue, sync dependencies

* 🔧 change lint command

Co-authored-by: Ben Hesseldieck <b.hesseldieck@gmail.com>
This commit is contained in:
Iván Ovejero 2021-08-29 20:58:11 +02:00 committed by GitHub
parent 223cd75685
commit 56c4c6991f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
108 changed files with 11832 additions and 8416 deletions

View file

@ -12,9 +12,6 @@ trim_trailing_whitespace = true
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
[*.ts]
quote_type = single
[*.yml] [*.yml]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2

354
.eslintrc.js Normal file
View file

@ -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',
},
};

View file

@ -23,6 +23,6 @@ jobs:
npm run bootstrap npm run bootstrap
npm run build --if-present npm run build --if-present
npm test npm test
npm run tslint npm run lint
env: env:
CI: true CI: true

4
.gitignore vendored
View file

@ -10,8 +10,8 @@ yarn.lock
google-generated-credentials.json google-generated-credentials.json
_START_PACKAGE _START_PACKAGE
.env .env
.vscode .vscode/*
!.vscode/extensions.json
.idea .idea
.prettierrc.js
vetur.config.js vetur.config.js
nodelinter.config.json nodelinter.config.json

51
.prettierrc.js Normal file
View file

@ -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,
};

8
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,8 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"esbenp.prettier-vscode",
"octref.vetur"
]
}

View file

@ -7,12 +7,14 @@
"build": "lerna exec npm run build", "build": "lerna exec npm run build",
"dev": "lerna exec npm run dev --parallel", "dev": "lerna exec npm run dev --parallel",
"clean:dist": "lerna exec -- rimraf ./dist", "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", "optimize-svg": "find ./packages -name '*.svg' ! -name 'pipedrive.svg' -print0 | xargs -0 -P16 -L20 npx svgo",
"start": "run-script-os", "start": "run-script-os",
"start:default": "cd packages/cli/bin && ./n8n", "start:default": "cd packages/cli/bin && ./n8n",
"start:windows": "cd packages/cli/bin && n8n", "start:windows": "cd packages/cli/bin && n8n",
"test": "lerna run test", "test": "lerna run test",
"tslint": "lerna exec npm run tslint",
"watch": "lerna run --parallel watch", "watch": "lerna run --parallel watch",
"webhook": "./packages/cli/bin/n8n webhook", "webhook": "./packages/cli/bin/n8n webhook",
"worker": "./packages/cli/bin/n8n worker" "worker": "./packages/cli/bin/n8n worker"

View file

@ -1,14 +1,14 @@
interface IResult { interface IResult {
totalWorkflows: number; totalWorkflows: number;
summary: { summary: {
failedExecutions: number, failedExecutions: number;
successfulExecutions: number, successfulExecutions: number;
warningExecutions: number, warningExecutions: number;
errors: IExecutionError[], errors: IExecutionError[];
warnings: IExecutionError[], warnings: IExecutionError[];
}; };
coveredNodes: { coveredNodes: {
[nodeType: string]: number [nodeType: string]: number;
}; };
executions: IExecutionResult[]; executions: IExecutionResult[];
} }
@ -21,7 +21,7 @@ interface IExecutionResult {
error?: string; error?: string;
changes?: string; changes?: string;
coveredNodes: { coveredNodes: {
[nodeType: string]: number [nodeType: string]: number;
}; };
} }

View file

@ -1,11 +1,9 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable no-console */
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { Command, flags } from '@oclif/command'; import { Command, flags } from '@oclif/command';
import { import { UserSettings } from 'n8n-core';
UserSettings, import { INode, LoggerProxy } from 'n8n-workflow';
} from 'n8n-core';
import {
INode,
} from 'n8n-workflow';
import { import {
ActiveExecutions, ActiveExecutions,
@ -17,26 +15,18 @@ import {
IWorkflowExecutionDataProcess, IWorkflowExecutionDataProcess,
LoadNodesAndCredentials, LoadNodesAndCredentials,
NodeTypes, NodeTypes,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
WorkflowCredentials, WorkflowCredentials,
WorkflowHelpers, WorkflowHelpers,
WorkflowRunner, WorkflowRunner,
} from '../src'; } from '../src';
import { import { getLogger } from '../src/Logger';
getLogger,
} from '../src/Logger';
import {
LoggerProxy,
} from 'n8n-workflow';
export class Execute extends Command { export class Execute extends Command {
static description = '\nExecutes a given workflow'; static description = '\nExecutes a given workflow';
static examples = [ static examples = [`$ n8n execute --id=5`, `$ n8n execute --file=workflow.json`];
`$ n8n execute --id=5`,
`$ n8n execute --file=workflow.json`,
];
static flags = { static flags = {
help: flags.help({ char: 'h' }), 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() { async run() {
const logger = getLogger(); const logger = getLogger();
LoggerProxy.init(logger); LoggerProxy.init(logger);
// eslint-disable-next-line @typescript-eslint/no-shadow
const { flags } = this.parse(Execute); const { flags } = this.parse(Execute);
// Start directly with the init of the database to improve startup time // 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 workflowId: string | undefined;
let workflowData: IWorkflowBase | undefined = undefined; let workflowData: IWorkflowBase | undefined;
if (flags.file) { if (flags.file) {
// Path to workflow is given // Path to workflow is given
try { try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
workflowData = JSON.parse(await fs.readFile(flags.file, 'utf8')); workflowData = JSON.parse(await fs.readFile(flags.file, 'utf8'));
} catch (error) { } catch (error) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (error.code === 'ENOENT') { if (error.code === 'ENOENT') {
console.info(`The file "${flags.file}" could not be found.`); console.info(`The file "${flags.file}" could not be found.`);
return; return;
@ -92,10 +85,15 @@ export class Execute extends Command {
// Do a basic check if the data in the file looks right // 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 // 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.`); console.info(`The file "${flags.file}" does not contain valid workflow data.`);
return; return;
} }
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
workflowId = workflowData.id!.toString(); workflowId = workflowData.id!.toString();
} }
@ -105,7 +103,8 @@ export class Execute extends Command {
if (flags.id) { if (flags.id) {
// Id of workflow is given // Id of workflow is given
workflowId = flags.id; 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) { if (workflowData === undefined) {
console.info(`The workflow with the id "${workflowId}" does not exist.`); console.info(`The workflow with the id "${workflowId}" does not exist.`);
process.exit(1); process.exit(1);
@ -139,7 +138,8 @@ export class Execute extends Command {
// Check if the workflow contains the required "Start" node // Check if the workflow contains the required "Start" node
// "requiredNodeTypes" are also defined in editor-ui/views/NodeView.vue // "requiredNodeTypes" are also defined in editor-ui/views/NodeView.vue
const requiredNodeTypes = ['n8n-nodes-base.start']; 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) { for (const node of workflowData!.nodes) {
if (requiredNodeTypes.includes(node.type)) { if (requiredNodeTypes.includes(node.type)) {
startNode = node; 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 // If the workflow does not contain a start-node we can not know what
// should be executed and with which data to start. // 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.`); 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(); return Promise.resolve();
} }
@ -158,6 +159,7 @@ export class Execute extends Command {
const runData: IWorkflowExecutionDataProcess = { const runData: IWorkflowExecutionDataProcess = {
executionMode: 'cli', executionMode: 'cli',
startNodes: [startNode.name], startNodes: [startNode.name],
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
workflowData: workflowData!, workflowData: workflowData!,
}; };
@ -178,6 +180,7 @@ export class Execute extends Command {
logger.info(JSON.stringify(data, null, 2)); logger.info(JSON.stringify(data, null, 2));
const { error } = data.data.resultData; const { error } = data.data.resultData;
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw { throw {
...error, ...error,
stack: error.stack, stack: error.stack,

View file

@ -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 * as fs from 'fs';
import { import { Command, flags } from '@oclif/command';
Command,
flags,
} from '@oclif/command';
import { import { UserSettings } from 'n8n-core';
UserSettings,
} from 'n8n-core';
import { // eslint-disable-next-line @typescript-eslint/no-unused-vars
INode, import { INode, INodeExecutionData, ITaskData, LoggerProxy } from 'n8n-workflow';
INodeExecutionData,
ITaskData, import { sep } from 'path';
} from 'n8n-workflow';
import { diff } from 'json-diff';
// eslint-disable-next-line import/no-extraneous-dependencies
import { pick } from 'lodash';
import { getLogger } from '../src/Logger';
import { import {
ActiveExecutions, ActiveExecutions,
@ -20,35 +28,17 @@ import {
CredentialTypes, CredentialTypes,
Db, Db,
ExternalHooks, ExternalHooks,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
IExecutionsCurrentSummary, IExecutionsCurrentSummary,
IWorkflowDb, IWorkflowDb,
IWorkflowExecutionDataProcess, IWorkflowExecutionDataProcess,
LoadNodesAndCredentials, LoadNodesAndCredentials,
NodeTypes, NodeTypes,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
WorkflowCredentials, WorkflowCredentials,
WorkflowRunner, WorkflowRunner,
} from '../src'; } 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 { export class ExecuteBatch extends Command {
static description = '\nExecutes multiple workflows once'; static description = '\nExecutes multiple workflows once';
@ -87,19 +77,24 @@ export class ExecuteBatch extends Command {
}), }),
concurrency: flags.integer({ concurrency: flags.integer({
default: 1, 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({ 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({ 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({ 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({ 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({ skipList: flags.string({
description: 'File containing a comma separated list of workflow IDs to skip.', description: 'File containing a comma separated list of workflow IDs to skip.',
@ -117,15 +112,16 @@ export class ExecuteBatch extends Command {
* Gracefully handles exit. * Gracefully handles exit.
* @param {boolean} skipExit Whether to skip exit or number according to received signal * @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) { static async stopProcess(skipExit: boolean | number = false) {
if (ExecuteBatch.cancelled) {
if (ExecuteBatch.cancelled === true) {
process.exit(0); process.exit(0);
} }
ExecuteBatch.cancelled = true; ExecuteBatch.cancelled = true;
const activeExecutionsInstance = ActiveExecutions.getInstance(); 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); activeExecutionsInstance.stopExecution(execution.id);
}); });
@ -135,16 +131,17 @@ export class ExecuteBatch extends Command {
process.exit(0); process.exit(0);
}, 30000); }, 30000);
let executingWorkflows = activeExecutionsInstance.getActiveExecutions() as IExecutionsCurrentSummary[]; let executingWorkflows = activeExecutionsInstance.getActiveExecutions();
let count = 0; let count = 0;
while (executingWorkflows.length !== 0) { while (executingWorkflows.length !== 0) {
if (count++ % 4 === 0) { if (count++ % 4 === 0) {
console.log(`Waiting for ${executingWorkflows.length} active executions to finish...`); 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}`); console.log(` - Execution ID ${execution.id}, workflow ID: ${execution.workflowId}`);
}); });
} }
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => { await new Promise((resolve) => {
setTimeout(resolve, 500); 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) { formatJsonOutput(data: object) {
return JSON.stringify(data, null, 2); return JSON.stringify(data, null, 2);
} }
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
shouldBeConsideredAsWarning(errorMessage: string) { shouldBeConsideredAsWarning(errorMessage: string) {
const warningStrings = [ const warningStrings = [
'refresh token is invalid', 'refresh token is invalid',
'unable to connect to', 'unable to connect to',
@ -174,6 +172,7 @@ export class ExecuteBatch extends Command {
'request timed out', 'request timed out',
]; ];
// eslint-disable-next-line no-param-reassign
errorMessage = errorMessage.toLowerCase(); errorMessage = errorMessage.toLowerCase();
for (let i = 0; i < warningStrings.length; i++) { for (let i = 0; i < warningStrings.length; i++) {
@ -185,18 +184,18 @@ export class ExecuteBatch extends Command {
return false; return false;
} }
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async run() { async run() {
process.on('SIGTERM', ExecuteBatch.stopProcess); process.on('SIGTERM', ExecuteBatch.stopProcess);
process.on('SIGINT', ExecuteBatch.stopProcess); process.on('SIGINT', ExecuteBatch.stopProcess);
const logger = getLogger(); const logger = getLogger();
LoggerProxy.init(logger); LoggerProxy.init(logger);
// eslint-disable-next-line @typescript-eslint/no-shadow
const { flags } = this.parse(ExecuteBatch); const { flags } = this.parse(ExecuteBatch);
ExecuteBatch.debug = flags.debug === true; ExecuteBatch.debug = flags.debug;
ExecuteBatch.concurrency = flags.concurrency || 1; ExecuteBatch.concurrency = flags.concurrency || 1;
const ids: number[] = []; const ids: number[] = [];
@ -241,7 +240,7 @@ export class ExecuteBatch extends Command {
if (flags.ids !== undefined) { if (flags.ids !== undefined) {
const paramIds = flags.ids.split(','); const paramIds = flags.ids.split(',');
const re = /\d+/; 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) { if (matchedIds.length === 0) {
console.log(`The parameter --ids must be a list of numeric IDs separated by a comma.`); 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 (flags.skipList !== undefined) {
if (fs.existsSync(flags.skipList)) { if (fs.existsSync(flags.skipList)) {
const contents = fs.readFileSync(flags.skipList, { encoding: 'utf-8' }); 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 { } else {
console.log('Skip list file not found. Exiting.'); console.log('Skip list file not found. Exiting.');
return; return;
} }
} }
if (flags.shallow === true) { if (flags.shallow) {
ExecuteBatch.shallow = true; ExecuteBatch.shallow = true;
} }
// Start directly with the init of the database to improve startup time // Start directly with the init of the database to improve startup time
const startDbInitPromise = Db.init(); const startDbInitPromise = Db.init();
@ -281,7 +279,7 @@ export class ExecuteBatch extends Command {
let allWorkflows; let allWorkflows;
const query = Db.collections!.Workflow!.createQueryBuilder('workflows'); const query = Db.collections.Workflow!.createQueryBuilder('workflows');
if (ids.length > 0) { if (ids.length > 0) {
query.andWhere(`workflows.id in (:...ids)`, { ids }); query.andWhere(`workflows.id in (:...ids)`, { ids });
@ -291,9 +289,10 @@ export class ExecuteBatch extends Command {
query.andWhere(`workflows.id not in (:...skipIds)`, { skipIds }); 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`); process.stdout.write(`Found ${allWorkflows.length} workflows to execute.\n`);
} }
@ -318,12 +317,19 @@ export class ExecuteBatch extends Command {
let { retries } = flags; let { retries } = flags;
while (retries > 0 && (results.summary.warningExecutions + results.summary.failedExecutions > 0) && ExecuteBatch.cancelled === false) { while (
const failedWorkflowIds = results.summary.errors.map(execution => execution.workflowId); retries > 0 &&
failedWorkflowIds.push(...results.summary.warnings.map(execution => execution.workflowId)); 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); const retryResults = await this.runTests(newWorkflowList);
this.mergeResults(results, retryResults); this.mergeResults(results, retryResults);
@ -343,13 +349,18 @@ export class ExecuteBatch extends Command {
console.log(`\t${nodeName}: ${nodeCount}`); console.log(`\t${nodeName}: ${nodeCount}`);
}); });
console.log('\nCheck the JSON file for more details.'); console.log('\nCheck the JSON file for more details.');
} else { } else if (flags.shortOutput) {
if (flags.shortOutput === true) { console.log(
console.log(this.formatJsonOutput({ ...results, executions: results.executions.filter(execution => execution.executionStatus !== 'success') })); this.formatJsonOutput({
...results,
executions: results.executions.filter(
(execution) => execution.executionStatus !== 'success',
),
}),
);
} else { } else {
console.log(this.formatJsonOutput(results)); console.log(this.formatJsonOutput(results));
} }
}
await ExecuteBatch.stopProcess(true); await ExecuteBatch.stopProcess(true);
@ -357,23 +368,26 @@ export class ExecuteBatch extends Command {
this.exit(1); this.exit(1);
} }
this.exit(0); this.exit(0);
} }
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
mergeResults(results: IResult, retryResults: IResult) { mergeResults(results: IResult, retryResults: IResult) {
if (retryResults.summary.successfulExecutions === 0) { if (retryResults.summary.successfulExecutions === 0) {
// Nothing to replace. // Nothing to replace.
return; return;
} }
// Find successful executions and replace them on previous result. // Find successful executions and replace them on previous result.
retryResults.executions.forEach(newExecution => { retryResults.executions.forEach((newExecution) => {
if (newExecution.executionStatus === 'success') { if (newExecution.executionStatus === 'success') {
// Remove previous execution from list. // 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) { if (errorIndex !== -1) {
// This workflow errored previously. Decrement error count. // This workflow errored previously. Decrement error count.
results.summary.failedExecutions--; results.summary.failedExecutions--;
@ -381,7 +395,9 @@ export class ExecuteBatch extends Command {
results.summary.errors.splice(errorIndex, 1); 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) { if (warningIndex !== -1) {
// This workflow errored previously. Decrement error count. // This workflow errored previously. Decrement error count.
results.summary.warningExecutions--; results.summary.warningExecutions--;
@ -420,7 +436,7 @@ export class ExecuteBatch extends Command {
let workflow: IWorkflowDb | undefined; let workflow: IWorkflowDb | undefined;
while (allWorkflows.length > 0) { while (allWorkflows.length > 0) {
workflow = allWorkflows.shift(); workflow = allWorkflows.shift();
if (ExecuteBatch.cancelled === true) { if (ExecuteBatch.cancelled) {
process.stdout.write(`Thread ${i + 1} resolving and quitting.`); process.stdout.write(`Thread ${i + 1} resolving and quitting.`);
resolve(true); resolve(true);
break; break;
@ -440,6 +456,7 @@ export class ExecuteBatch extends Command {
this.updateStatus(); this.updateStatus();
} }
// eslint-disable-next-line @typescript-eslint/no-loop-func
await this.startThread(workflow).then((executionResult) => { await this.startThread(workflow).then((executionResult) => {
if (ExecuteBatch.debug) { if (ExecuteBatch.debug) {
ExecuteBatch.workflowExecutionsProgress[i].pop(); ExecuteBatch.workflowExecutionsProgress[i].pop();
@ -456,7 +473,7 @@ export class ExecuteBatch extends Command {
result.summary.successfulExecutions++; result.summary.successfulExecutions++;
const nodeNames = Object.keys(executionResult.coveredNodes); const nodeNames = Object.keys(executionResult.coveredNodes);
nodeNames.map(nodeName => { nodeNames.map((nodeName) => {
if (result.coveredNodes[nodeName] === undefined) { if (result.coveredNodes[nodeName] === undefined) {
result.coveredNodes[nodeName] = 0; result.coveredNodes[nodeName] = 0;
} }
@ -506,19 +523,18 @@ export class ExecuteBatch extends Command {
}); });
} }
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
updateStatus() { updateStatus() {
if (ExecuteBatch.cancelled) {
if (ExecuteBatch.cancelled === true) {
return; return;
} }
if (process.stdout.isTTY === true) { if (process.stdout.isTTY) {
process.stdout.moveCursor(0, - (ExecuteBatch.concurrency)); process.stdout.moveCursor(0, -ExecuteBatch.concurrency);
process.stdout.cursorTo(0); process.stdout.cursorTo(0);
process.stdout.clearLine(0); process.stdout.clearLine(0);
} }
ExecuteBatch.workflowExecutionsProgress.map((concurrentThread, index) => { ExecuteBatch.workflowExecutionsProgress.map((concurrentThread, index) => {
let message = `${index + 1}: `; let message = `${index + 1}: `;
concurrentThread.map((executionItem, workflowIndex) => { concurrentThread.map((executionItem, workflowIndex) => {
@ -537,16 +553,19 @@ export class ExecuteBatch extends Command {
default: default:
break; 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.cursorTo(0);
process.stdout.clearLine(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() { initializeLogs() {
process.stdout.write('**********************************************\n'); process.stdout.write('**********************************************\n');
process.stdout.write(' n8n test workflows\n'); process.stdout.write(' n8n test workflows\n');
@ -560,7 +579,7 @@ export class ExecuteBatch extends Command {
} }
} }
startThread(workflowData: IWorkflowDb): Promise<IExecutionResult> { async startThread(workflowData: IWorkflowDb): Promise<IExecutionResult> {
// This will be the object returned by the promise. // This will be the object returned by the promise.
// It will be updated according to execution progress below. // It will be updated according to execution progress below.
const executionResult: IExecutionResult = { const executionResult: IExecutionResult = {
@ -572,10 +591,9 @@ export class ExecuteBatch extends Command {
coveredNodes: {}, coveredNodes: {},
}; };
const requiredNodeTypes = ['n8n-nodes-base.start']; 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) { for (const node of workflowData.nodes) {
if (requiredNodeTypes.includes(node.type)) { if (requiredNodeTypes.includes(node.type)) {
startNode = node; startNode = node;
@ -593,10 +611,10 @@ export class ExecuteBatch extends Command {
// properties from the JSON object (useful for optional properties that can // properties from the JSON object (useful for optional properties that can
// cause the comparison to detect changes when not true). // cause the comparison to detect changes when not true).
const nodeEdgeCases = {} as INodeSpecialCases; const nodeEdgeCases = {} as INodeSpecialCases;
workflowData.nodes.forEach(node => { workflowData.nodes.forEach((node) => {
executionResult.coveredNodes[node.type] = (executionResult.coveredNodes[node.type] || 0) + 1; executionResult.coveredNodes[node.type] = (executionResult.coveredNodes[node.type] || 0) + 1;
if (node.notes !== undefined && node.notes !== '') { if (node.notes !== undefined && node.notes !== '') {
node.notes.split('\n').forEach(note => { node.notes.split('\n').forEach((note) => {
const parts = note.split('='); const parts = note.split('=');
if (parts.length === 2) { if (parts.length === 2) {
if (nodeEdgeCases[node.name] === undefined) { if (nodeEdgeCases[node.name] === undefined) {
@ -605,9 +623,13 @@ export class ExecuteBatch extends Command {
if (parts[0] === 'CAP_RESULTS_LENGTH') { if (parts[0] === 'CAP_RESULTS_LENGTH') {
nodeEdgeCases[node.name].capResults = parseInt(parts[1], 10); nodeEdgeCases[node.name].capResults = parseInt(parts[1], 10);
} else if (parts[0] === 'IGNORED_PROPERTIES') { } 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') { } 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); resolve(executionResult);
}, ExecuteBatch.executionTimeout); }, ExecuteBatch.executionTimeout);
try { try {
const runData: IWorkflowExecutionDataProcess = { const runData: IWorkflowExecutionDataProcess = {
executionMode: 'cli', executionMode: 'cli',
startNodes: [startNode!.name], startNodes: [startNode!.name],
workflowData: workflowData!, workflowData,
}; };
const workflowRunner = new WorkflowRunner(); const workflowRunner = new WorkflowRunner();
@ -647,7 +667,7 @@ export class ExecuteBatch extends Command {
const activeExecutions = ActiveExecutions.getInstance(); const activeExecutions = ActiveExecutions.getInstance();
const data = await activeExecutions.getPostExecutePromise(executionId); const data = await activeExecutions.getPostExecutePromise(executionId);
if (gotCancel || ExecuteBatch.cancelled === true) { if (gotCancel || ExecuteBatch.cancelled) {
clearTimeout(timeoutTimer); clearTimeout(timeoutTimer);
// The promise was settled already so we simply ignore. // The promise was settled already so we simply ignore.
return; return;
@ -657,14 +677,18 @@ export class ExecuteBatch extends Command {
executionResult.error = 'Workflow did not return any data.'; executionResult.error = 'Workflow did not return any data.';
executionResult.executionStatus = 'error'; executionResult.executionStatus = 'error';
} else { } else {
executionResult.executionTime = (Date.parse(data.stoppedAt as unknown as string) - Date.parse(data.startedAt as unknown as string)) / 1000; executionResult.executionTime =
executionResult.finished = (data?.finished !== undefined) as boolean; (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) { if (data.data.resultData.error) {
executionResult.error = // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, no-prototype-builtins
data.data.resultData.error.hasOwnProperty('description') ? executionResult.error = data.data.resultData.error.hasOwnProperty('description')
// @ts-ignore ? // @ts-ignore
data.data.resultData.error.description : data.data.resultData.error.message; data.data.resultData.error.description
: data.data.resultData.error.message;
if (data.data.resultData.lastNodeExecuted !== undefined) { if (data.data.resultData.lastNodeExecuted !== undefined) {
executionResult.error += ` on node ${data.data.resultData.lastNodeExecuted}`; executionResult.error += ` on node ${data.data.resultData.lastNodeExecuted}`;
} }
@ -674,7 +698,7 @@ export class ExecuteBatch extends Command {
executionResult.executionStatus = 'warning'; executionResult.executionStatus = 'warning';
} }
} else { } else {
if (ExecuteBatch.shallow === true) { if (ExecuteBatch.shallow) {
// What this does is guarantee that top-level attributes // What this does is guarantee that top-level attributes
// from the JSON are kept and the are the same type. // from the JSON are kept and the are the same type.
@ -688,34 +712,48 @@ export class ExecuteBatch extends Command {
if (taskData.data === undefined) { if (taskData.data === undefined) {
return; return;
} }
Object.keys(taskData.data).map(connectionName => { Object.keys(taskData.data).map((connectionName) => {
const connection = taskData.data![connectionName] as Array<INodeExecutionData[] | null>; const connection = taskData.data![connectionName];
connection.map(executionDataArray => { connection.map((executionDataArray) => {
if (executionDataArray === null) { if (executionDataArray === null) {
return; return;
} }
if (nodeEdgeCases[nodeName] !== undefined && nodeEdgeCases[nodeName].capResults !== undefined) { if (
nodeEdgeCases[nodeName] !== undefined &&
nodeEdgeCases[nodeName].capResults !== undefined
) {
executionDataArray.splice(nodeEdgeCases[nodeName].capResults!); executionDataArray.splice(nodeEdgeCases[nodeName].capResults!);
} }
executionDataArray.map(executionData => { executionDataArray.map((executionData) => {
if (executionData.json === undefined) { if (executionData.json === undefined) {
return; return;
} }
if (nodeEdgeCases[nodeName] !== undefined && nodeEdgeCases[nodeName].ignoredProperties !== undefined) { if (
nodeEdgeCases[nodeName].ignoredProperties!.forEach(ignoredProperty => delete executionData.json[ignoredProperty]); nodeEdgeCases[nodeName] !== undefined &&
nodeEdgeCases[nodeName].ignoredProperties !== undefined
) {
nodeEdgeCases[nodeName].ignoredProperties!.forEach(
(ignoredProperty) => delete executionData.json[ignoredProperty],
);
} }
let keepOnlyFields = [] as string[]; let keepOnlyFields = [] as string[];
if (nodeEdgeCases[nodeName] !== undefined && nodeEdgeCases[nodeName].keepOnlyProperties !== undefined) { if (
nodeEdgeCases[nodeName] !== undefined &&
nodeEdgeCases[nodeName].keepOnlyProperties !== undefined
) {
keepOnlyFields = nodeEdgeCases[nodeName].keepOnlyProperties!; 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 jsonProperties = executionData.json;
const nodeOutputAttributes = Object.keys(jsonProperties); const nodeOutputAttributes = Object.keys(jsonProperties);
nodeOutputAttributes.map(attributeName => { nodeOutputAttributes.map((attributeName) => {
if (Array.isArray(jsonProperties[attributeName])) { if (Array.isArray(jsonProperties[attributeName])) {
jsonProperties[attributeName] = ['json array']; jsonProperties[attributeName] = ['json array'];
} else if (typeof jsonProperties[attributeName] === 'object') { } 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. // If not using shallow comparison then we only treat nodeEdgeCases.
const specialCases = Object.keys(nodeEdgeCases); const specialCases = Object.keys(nodeEdgeCases);
specialCases.forEach(nodeName => { specialCases.forEach((nodeName) => {
data.data.resultData.runData[nodeName].map((taskData: ITaskData) => { data.data.resultData.runData[nodeName].map((taskData: ITaskData) => {
if (taskData.data === undefined) { if (taskData.data === undefined) {
return; return;
} }
Object.keys(taskData.data).map(connectionName => { Object.keys(taskData.data).map((connectionName) => {
const connection = taskData.data![connectionName] as Array<INodeExecutionData[] | null>; const connection = taskData.data![connectionName];
connection.map(executionDataArray => { connection.map((executionDataArray) => {
if (executionDataArray === null) { if (executionDataArray === null) {
return; return;
} }
@ -749,15 +786,16 @@ export class ExecuteBatch extends Command {
} }
if (nodeEdgeCases[nodeName].ignoredProperties !== undefined) { if (nodeEdgeCases[nodeName].ignoredProperties !== undefined) {
executionDataArray.map(executionData => { executionDataArray.map((executionData) => {
if (executionData.json === undefined) { if (executionData.json === undefined) {
return; 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) { if (ExecuteBatch.compare === undefined) {
executionResult.executionStatus = 'success'; executionResult.executionStatus = 'success';
} else { } else {
const fileName = (ExecuteBatch.compare.endsWith(sep) ? ExecuteBatch.compare : ExecuteBatch.compare + sep) + `${workflowData.id}-snapshot.json`; const fileName = `${
if (fs.existsSync(fileName) === true) { 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 contents = fs.readFileSync(fileName, { encoding: 'utf-8' });
const changes = diff(JSON.parse(contents), data, { keysOnly: true }); 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 // Save snapshots only after comparing - this is to make sure we're updating
// After comparing to existing verion. // After comparing to existing verion.
if (ExecuteBatch.snapshot !== undefined) { 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); fs.writeFileSync(fileName, serializedData);
} }
} }
@ -803,5 +848,4 @@ export class ExecuteBatch extends Command {
resolve(executionResult); resolve(executionResult);
}); });
} }
} }

View file

@ -1,32 +1,16 @@
import { /* eslint-disable @typescript-eslint/restrict-plus-operands */
Command, /* eslint-disable @typescript-eslint/no-unsafe-member-access */
flags, /* eslint-disable no-console */
} from '@oclif/command'; import { Command, flags } from '@oclif/command';
import { import { Credentials, UserSettings } from 'n8n-core';
Credentials,
UserSettings,
} from 'n8n-core';
import { import { IDataObject, LoggerProxy } from 'n8n-workflow';
IDataObject
} from 'n8n-workflow';
import {
Db,
ICredentialsDecryptedDb,
} from '../../src';
import {
getLogger,
} from '../../src/Logger';
import {
LoggerProxy,
} from 'n8n-workflow';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { getLogger } from '../../src/Logger';
import { Db, ICredentialsDecryptedDb } from '../../src';
export class ExportCredentialsCommand extends Command { export class ExportCredentialsCommand extends Command {
static description = 'Export credentials'; static description = 'Export credentials';
@ -45,7 +29,8 @@ export class ExportCredentialsCommand extends Command {
description: 'Export all credentials', description: 'Export all credentials',
}), }),
backup: flags.boolean({ 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({ id: flags.string({
description: 'The ID of the credential to export', description: 'The ID of the credential to export',
@ -58,17 +43,21 @@ export class ExportCredentialsCommand extends Command {
description: 'Format the output in an easier to read fashion', description: 'Format the output in an easier to read fashion',
}), }),
separate: flags.boolean({ 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({ 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() { async run() {
const logger = getLogger(); const logger = getLogger();
LoggerProxy.init(logger); LoggerProxy.init(logger);
// eslint-disable-next-line @typescript-eslint/no-shadow
const { flags } = this.parse(ExportCredentialsCommand); const { flags } = this.parse(ExportCredentialsCommand);
if (flags.backup) { if (flags.backup) {
@ -103,7 +92,9 @@ export class ExportCredentialsCommand extends Command {
fs.mkdirSync(flags.output, { recursive: true }); fs.mkdirSync(flags.output, { recursive: true });
} }
} catch (e) { } 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.error('\nFILESYSTEM ERROR');
logger.info('===================================='); logger.info('====================================');
logger.error(e.message); logger.error(e.message);
@ -127,6 +118,7 @@ export class ExportCredentialsCommand extends Command {
findQuery.id = flags.id; findQuery.id = flags.id;
} }
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const credentials = await Db.collections.Credentials!.find(findQuery); const credentials = await Db.collections.Credentials!.find(findQuery);
if (flags.decrypted) { if (flags.decrypted) {
@ -148,17 +140,22 @@ export class ExportCredentialsCommand extends Command {
} }
if (flags.separate) { if (flags.separate) {
let fileContents: string, i: number; let fileContents: string;
let i: number;
for (i = 0; i < credentials.length; i++) { for (i = 0; i < credentials.length; i++) {
fileContents = JSON.stringify(credentials[i], null, flags.pretty ? 2 : undefined); 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); fs.writeFileSync(filename, fileContents);
} }
console.info(`Successfully exported ${i} credentials.`); console.info(`Successfully exported ${i} credentials.`);
} else { } else {
const fileContents = JSON.stringify(credentials, null, flags.pretty ? 2 : undefined); const fileContents = JSON.stringify(credentials, null, flags.pretty ? 2 : undefined);
if (flags.output) { if (flags.output) {
fs.writeFileSync(flags.output!, fileContents); fs.writeFileSync(flags.output, fileContents);
console.info(`Successfully exported ${credentials.length} credentials.`); console.info(`Successfully exported ${credentials.length} credentials.`);
} else { } else {
console.info(fileContents); console.info(fileContents);

View file

@ -1,26 +1,13 @@
import { /* eslint-disable @typescript-eslint/no-unsafe-member-access */
Command, /* eslint-disable no-console */
flags, import { Command, flags } from '@oclif/command';
} from '@oclif/command';
import { import { IDataObject, LoggerProxy } from 'n8n-workflow';
IDataObject
} from 'n8n-workflow';
import {
Db,
} from '../../src';
import {
getLogger,
} from '../../src/Logger';
import {
LoggerProxy,
} from 'n8n-workflow';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { getLogger } from '../../src/Logger';
import { Db } from '../../src';
export class ExportWorkflowsCommand extends Command { export class ExportWorkflowsCommand extends Command {
static description = 'Export workflows'; static description = 'Export workflows';
@ -38,7 +25,8 @@ export class ExportWorkflowsCommand extends Command {
description: 'Export all workflows', description: 'Export all workflows',
}), }),
backup: flags.boolean({ 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({ id: flags.string({
description: 'The ID of the workflow to export', 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', description: 'Format the output in an easier to read fashion',
}), }),
separate: flags.boolean({ 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() { async run() {
const logger = getLogger(); const logger = getLogger();
LoggerProxy.init(logger); LoggerProxy.init(logger);
// eslint-disable-next-line @typescript-eslint/no-shadow
const { flags } = this.parse(ExportWorkflowsCommand); const { flags } = this.parse(ExportWorkflowsCommand);
if (flags.backup) { if (flags.backup) {
@ -93,7 +84,9 @@ export class ExportWorkflowsCommand extends Command {
fs.mkdirSync(flags.output, { recursive: true }); fs.mkdirSync(flags.output, { recursive: true });
} }
} catch (e) { } 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.error('\nFILESYSTEM ERROR');
logger.info('===================================='); logger.info('====================================');
logger.error(e.message); logger.error(e.message);
@ -117,6 +110,7 @@ export class ExportWorkflowsCommand extends Command {
findQuery.id = flags.id; findQuery.id = flags.id;
} }
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const workflows = await Db.collections.Workflow!.find(findQuery); const workflows = await Db.collections.Workflow!.find(findQuery);
if (workflows.length === 0) { if (workflows.length === 0) {
@ -124,18 +118,27 @@ export class ExportWorkflowsCommand extends Command {
} }
if (flags.separate) { if (flags.separate) {
let fileContents: string, i: number; let fileContents: string;
let i: number;
for (i = 0; i < workflows.length; i++) { for (i = 0; i < workflows.length; i++) {
fileContents = JSON.stringify(workflows[i], null, flags.pretty ? 2 : undefined); 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); fs.writeFileSync(filename, fileContents);
} }
console.info(`Successfully exported ${i} workflows.`); console.info(`Successfully exported ${i} workflows.`);
} else { } else {
const fileContents = JSON.stringify(workflows, null, flags.pretty ? 2 : undefined); const fileContents = JSON.stringify(workflows, null, flags.pretty ? 2 : undefined);
if (flags.output) { if (flags.output) {
fs.writeFileSync(flags.output!, fileContents); fs.writeFileSync(flags.output, fileContents);
console.info(`Successfully exported ${workflows.length} ${workflows.length === 1 ? 'workflow.' : 'workflows.'}`); console.info(
`Successfully exported ${workflows.length} ${
workflows.length === 1 ? 'workflow.' : 'workflows.'
}`,
);
} else { } else {
console.info(fileContents); console.info(fileContents);
} }

View file

@ -1,28 +1,16 @@
import { /* eslint-disable @typescript-eslint/no-unsafe-member-access */
Command, /* eslint-disable no-console */
flags, import { Command, flags } from '@oclif/command';
} from '@oclif/command';
import { import { Credentials, UserSettings } from 'n8n-core';
Credentials,
UserSettings,
} from 'n8n-core';
import { import { LoggerProxy } from 'n8n-workflow';
Db,
} from '../../src';
import {
getLogger,
} from '../../src/Logger';
import {
LoggerProxy,
} from 'n8n-workflow';
import * as fs from 'fs'; import * as fs from 'fs';
import * as glob from 'fast-glob'; import * as glob from 'fast-glob';
import * as path from 'path'; import * as path from 'path';
import { getLogger } from '../../src/Logger';
import { Db } from '../../src';
export class ImportCredentialsCommand extends Command { export class ImportCredentialsCommand extends Command {
static description = 'Import credentials'; 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() { async run() {
const logger = getLogger(); const logger = getLogger();
LoggerProxy.init(logger); LoggerProxy.init(logger);
// eslint-disable-next-line @typescript-eslint/no-shadow
const { flags } = this.parse(ImportCredentialsCommand); const { flags } = this.parse(ImportCredentialsCommand);
if (!flags.input) { if (!flags.input) {
@ -76,18 +66,25 @@ export class ImportCredentialsCommand extends Command {
} }
if (flags.separate) { 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++) { 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' })); 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') { if (typeof credential.data === 'object') {
// plain data / decrypted input. Should be encrypted first. // 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); 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); await Db.collections.Credentials!.save(credential);
} }
} else { } else {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const fileContents = JSON.parse(fs.readFileSync(flags.input, { encoding: 'utf8' })); const fileContents = JSON.parse(fs.readFileSync(flags.input, { encoding: 'utf8' }));
if (!Array.isArray(fileContents)) { if (!Array.isArray(fileContents)) {
@ -97,8 +94,13 @@ export class ImportCredentialsCommand extends Command {
for (i = 0; i < fileContents.length; i++) { for (i = 0; i < fileContents.length; i++) {
if (typeof fileContents[i].data === 'object') { if (typeof fileContents[i].data === 'object') {
// plain data / decrypted input. Should be encrypted first. // 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]); await Db.collections.Credentials!.save(fileContents[i]);
} }
} }

View file

@ -1,26 +1,15 @@
import { /* eslint-disable no-console */
Command, /* eslint-disable @typescript-eslint/no-unsafe-assignment */
flags, import { Command, flags } from '@oclif/command';
} from '@oclif/command';
import { import { LoggerProxy } from 'n8n-workflow';
Db,
} from '../../src';
import {
getLogger,
} from '../../src/Logger';
import {
LoggerProxy,
} from 'n8n-workflow';
import * as fs from 'fs'; import * as fs from 'fs';
import * as glob from 'fast-glob'; import * as glob from 'fast-glob';
import * as path from 'path'; import * as path from 'path';
import { import { UserSettings } from 'n8n-core';
UserSettings, import { getLogger } from '../../src/Logger';
} from 'n8n-core'; import { Db } from '../../src';
export class ImportWorkflowsCommand extends Command { export class ImportWorkflowsCommand extends Command {
static description = 'Import workflows'; 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() { async run() {
const logger = getLogger(); const logger = getLogger();
LoggerProxy.init(logger); LoggerProxy.init(logger);
// eslint-disable-next-line @typescript-eslint/no-shadow
const { flags } = this.parse(ImportWorkflowsCommand); const { flags } = this.parse(ImportWorkflowsCommand);
if (!flags.input) { if (!flags.input) {
@ -68,9 +59,12 @@ export class ImportWorkflowsCommand extends Command {
await UserSettings.prepareUserSettings(); await UserSettings.prepareUserSettings();
let i; let i;
if (flags.separate) { 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++) { for (i = 0; i < files.length; i++) {
const workflow = JSON.parse(fs.readFileSync(files[i], { encoding: 'utf8' })); 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); await Db.collections.Workflow!.save(workflow);
} }
} else { } else {
@ -81,6 +75,7 @@ export class ImportWorkflowsCommand extends Command {
} }
for (i = 0; i < fileContents.length; i++) { 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]); await Db.collections.Workflow!.save(fileContents[i]);
} }
} }
@ -89,6 +84,7 @@ export class ImportWorkflowsCommand extends Command {
process.exit(0); process.exit(0);
} catch (error) { } catch (error) {
console.error('An error occurred while exporting workflows. See log messages for details.'); 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); logger.error(error.message);
this.exit(1); this.exit(1);
} }

View file

@ -1,16 +1,10 @@
import { /* eslint-disable @typescript-eslint/no-unsafe-member-access */
Command, /* eslint-disable no-console */
flags, import { Command, flags } from '@oclif/command';
} from '@oclif/command';
import { import { IDataObject } from 'n8n-workflow';
IDataObject
} from 'n8n-workflow';
import {
Db,
} from "../../src";
import { Db } from '../../src';
export class ListWorkflowCommand extends Command { export class ListWorkflowCommand extends Command {
static description = '\nList workflows'; 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() { async run() {
// eslint-disable-next-line @typescript-eslint/no-shadow
const { flags } = this.parse(ListWorkflowCommand); const { flags } = this.parse(ListWorkflowCommand);
if (flags.active !== undefined && !['true', 'false'].includes(flags.active)) { if (flags.active !== undefined && !['true', 'false'].includes(flags.active)) {
@ -46,14 +42,13 @@ export class ListWorkflowCommand extends Command {
findQuery.active = flags.active === 'true'; findQuery.active = flags.active === 'true';
} }
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const workflows = await Db.collections.Workflow!.find(findQuery); const workflows = await Db.collections.Workflow!.find(findQuery);
if (flags.onlyId) { if (flags.onlyId) {
workflows.forEach(workflow => console.log(workflow.id)); workflows.forEach((workflow) => console.log(workflow.id));
} else { } else {
workflows.forEach(workflow => console.log(workflow.id + "|" + workflow.name)); workflows.forEach((workflow) => console.log(`${workflow.id}|${workflow.name}`));
} }
} catch (e) { } catch (e) {
console.error('\nGOT ERROR'); console.error('\nGOT ERROR');
console.log('===================================='); console.log('====================================');

View file

@ -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 * as localtunnel from 'localtunnel';
import { import { TUNNEL_SUBDOMAIN_ENV, UserSettings } from 'n8n-core';
TUNNEL_SUBDOMAIN_ENV,
UserSettings,
} from 'n8n-core';
import { Command, flags } from '@oclif/command'; import { Command, flags } from '@oclif/command';
const open = require('open'); // eslint-disable-next-line import/no-extraneous-dependencies
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import { IDataObject, LoggerProxy } from 'n8n-workflow';
import * as config from '../config'; import * as config from '../config';
import { import {
ActiveExecutions, ActiveExecutions,
@ -17,6 +22,7 @@ import {
Db, Db,
ExternalHooks, ExternalHooks,
GenericHelpers, GenericHelpers,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
IExecutionsCurrentSummary, IExecutionsCurrentSummary,
LoadNodesAndCredentials, LoadNodesAndCredentials,
NodeTypes, NodeTypes,
@ -24,15 +30,11 @@ import {
TestWebhooks, TestWebhooks,
WaitTracker, WaitTracker,
} from '../src'; } from '../src';
import { IDataObject } from 'n8n-workflow';
import { import { getLogger } from '../src/Logger';
getLogger,
} from '../src/Logger';
import { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires
LoggerProxy, const open = require('open');
} from 'n8n-workflow';
let activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner | undefined; let activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner | undefined;
let processExistCode = 0; let processExistCode = 0;
@ -54,29 +56,32 @@ export class Start extends Command {
description: 'opens the UI automatically in browser', description: 'opens the UI automatically in browser',
}), }),
tunnel: flags.boolean({ 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 * Opens the UI in browser
*/ */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
static openBrowser() { static openBrowser() {
const editorUrl = GenericHelpers.getBaseUrl(); const editorUrl = GenericHelpers.getBaseUrl();
open(editorUrl, { wait: true }) // eslint-disable-next-line @typescript-eslint/no-unused-vars
.catch((error: Error) => { 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`); 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. * Stoppes the n8n in a graceful way.
* Make for example sure that all the webhooks from third party services * Make for example sure that all the webhooks from third party services
* get removed. * get removed.
*/ */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
static async stopProcess() { static async stopProcess() {
getLogger().info('\nStopping n8n...'); getLogger().info('\nStopping n8n...');
@ -90,10 +95,12 @@ export class Start extends Command {
process.exit(processExistCode); process.exit(processExistCode);
}, 30000); }, 30000);
const skipWebhookDeregistration = config.get('endpoints.skipWebhoooksDeregistrationOnShutdown') as boolean; const skipWebhookDeregistration = config.get(
'endpoints.skipWebhoooksDeregistrationOnShutdown',
) as boolean;
const removePromises = []; const removePromises = [];
if (activeWorkflowRunner !== undefined && skipWebhookDeregistration !== true) { if (activeWorkflowRunner !== undefined && !skipWebhookDeregistration) {
removePromises.push(activeWorkflowRunner.removeAll()); removePromises.push(activeWorkflowRunner.removeAll());
} }
@ -105,22 +112,23 @@ export class Start extends Command {
// Wait for active workflow executions to finish // Wait for active workflow executions to finish
const activeExecutionsInstance = ActiveExecutions.getInstance(); const activeExecutionsInstance = ActiveExecutions.getInstance();
let executingWorkflows = activeExecutionsInstance.getActiveExecutions() as IExecutionsCurrentSummary[]; let executingWorkflows = activeExecutionsInstance.getActiveExecutions();
let count = 0; let count = 0;
while (executingWorkflows.length !== 0) { while (executingWorkflows.length !== 0) {
if (count++ % 4 === 0) { if (count++ % 4 === 0) {
console.log(`Waiting for ${executingWorkflows.length} active executions to finish...`); 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}`); console.log(` - Execution ID ${execution.id}, workflow ID: ${execution.workflowId}`);
}); });
} }
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => { await new Promise((resolve) => {
setTimeout(resolve, 500); setTimeout(resolve, 500);
}); });
executingWorkflows = activeExecutionsInstance.getActiveExecutions(); executingWorkflows = activeExecutionsInstance.getActiveExecutions();
} }
} catch (error) { } catch (error) {
console.error('There was an error shutting down n8n.', error); console.error('There was an error shutting down n8n.', error);
} }
@ -128,12 +136,12 @@ export class Start extends Command {
process.exit(processExistCode); process.exit(processExistCode);
} }
async run() { async run() {
// Make sure that n8n shuts down gracefully if possible // Make sure that n8n shuts down gracefully if possible
process.on('SIGTERM', Start.stopProcess); process.on('SIGTERM', Start.stopProcess);
process.on('SIGINT', Start.stopProcess); process.on('SIGINT', Start.stopProcess);
// eslint-disable-next-line @typescript-eslint/no-shadow
const { flags } = this.parse(Start); const { flags } = this.parse(Start);
// Wrap that the process does not close but we can still use async // 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'); logger.info('Initializing n8n process');
// todo remove a few versions after release // 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 // Start directly with the init of the database to improve startup time
const startDbInitPromise = Db.init().catch((error: Error) => { 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 redisPort = config.get('queue.bull.redis.port');
const redisDB = config.get('queue.bull.redis.db'); const redisDB = config.get('queue.bull.redis.db');
const redisConnectionTimeoutLimit = config.get('queue.bull.redis.timeoutThreshold'); const redisConnectionTimeoutLimit = config.get('queue.bull.redis.timeoutThreshold');
let lastTimer = 0, cumulativeTimeout = 0; let lastTimer = 0;
let cumulativeTimeout = 0;
const settings = { const settings = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
retryStrategy: (times: number): number | null => { retryStrategy: (times: number): number | null => {
const now = Date.now(); const now = Date.now();
if (now - lastTimer > 30000) { if (now - lastTimer > 30000) {
@ -199,7 +211,10 @@ export class Start extends Command {
cumulativeTimeout += now - lastTimer; cumulativeTimeout += now - lastTimer;
lastTimer = now; lastTimer = now;
if (cumulativeTimeout > redisConnectionTimeoutLimit) { 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); 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') { if (dbType === 'sqlite') {
const shouldRunVacuum = config.get('database.sqlite.executeVacuumOnStartup') as number; const shouldRunVacuum = config.get('database.sqlite.executeVacuumOnStartup') as number;
if (shouldRunVacuum) { if (shouldRunVacuum) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises, @typescript-eslint/no-non-null-assertion
await Db.collections.Execution!.query('VACUUM;'); await Db.collections.Execution!.query('VACUUM;');
} }
} }
if (flags.tunnel === true) { if (flags.tunnel) {
this.log('\nWaiting for tunnel ...'); this.log('\nWaiting for tunnel ...');
let tunnelSubdomain; 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]; tunnelSubdomain = process.env[TUNNEL_SUBDOMAIN_ENV];
} else if (userSettings.tunnelSubdomain !== undefined) { } else if (userSettings.tunnelSubdomain !== undefined) {
tunnelSubdomain = userSettings.tunnelSubdomain; tunnelSubdomain = userSettings.tunnelSubdomain;
@ -257,9 +276,13 @@ export class Start extends Command {
if (tunnelSubdomain === undefined) { if (tunnelSubdomain === undefined) {
// When no tunnel subdomain did exist yet create a new random one // When no tunnel subdomain did exist yet create a new random one
const availableCharacters = 'abcdefghijklmnopqrstuvwxyz0123456789'; const availableCharacters = 'abcdefghijklmnopqrstuvwxyz0123456789';
userSettings.tunnelSubdomain = Array.from({ length: 24 }).map(() => { userSettings.tunnelSubdomain = Array.from({ length: 24 })
return availableCharacters.charAt(Math.floor(Math.random() * availableCharacters.length)); .map(() => {
}).join(''); return availableCharacters.charAt(
Math.floor(Math.random() * availableCharacters.length),
);
})
.join('');
await UserSettings.writeUserSettings(userSettings); await UserSettings.writeUserSettings(userSettings);
} }
@ -269,14 +292,16 @@ export class Start extends Command {
subdomain: tunnelSubdomain, subdomain: tunnelSubdomain,
}; };
const port = config.get('port') as number; const port = config.get('port');
// @ts-ignore // @ts-ignore
const webhookTunnel = await localtunnel(port, tunnelSettings); 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(`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(); await Server.start();
@ -285,6 +310,7 @@ export class Start extends Command {
activeWorkflowRunner = ActiveWorkflowRunner.getInstance(); activeWorkflowRunner = ActiveWorkflowRunner.getInstance();
await activeWorkflowRunner.init(); await activeWorkflowRunner.init();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const waitTracker = WaitTracker(); const waitTracker = WaitTracker();
const editorUrl = GenericHelpers.getBaseUrl(); const editorUrl = GenericHelpers.getBaseUrl();
@ -297,7 +323,7 @@ export class Start extends Command {
process.stdin.setEncoding('utf8'); process.stdin.setEncoding('utf8');
let inputText = ''; let inputText = '';
if (flags.open === true) { if (flags.open) {
Start.openBrowser(); Start.openBrowser();
} }
this.log(`\nPress "o" to open in Browser.`); this.log(`\nPress "o" to open in Browser.`);
@ -307,15 +333,18 @@ export class Start extends Command {
inputText = ''; inputText = '';
} else if (key.charCodeAt(0) === 3) { } else if (key.charCodeAt(0) === 3) {
// Ctrl + c got pressed // Ctrl + c got pressed
// eslint-disable-next-line @typescript-eslint/no-floating-promises
Start.stopProcess(); Start.stopProcess();
} else { } else {
// When anything else got pressed, record it and send it on enter into the child process // 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) { if (key.charCodeAt(0) === 13) {
// send to child process and print in terminal // send to child process and print in terminal
process.stdout.write('\n'); process.stdout.write('\n');
inputText = ''; inputText = '';
} else { } else {
// record it and write into terminal // record it and write into terminal
// eslint-disable-next-line @typescript-eslint/no-unused-vars
inputText += key; inputText += key;
process.stdout.write(key); process.stdout.write(key);
} }
@ -323,6 +352,7 @@ export class Start extends Command {
}); });
} }
} catch (error) { } catch (error) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
this.error(`There was an error: ${error.message}`); this.error(`There was an error: ${error.message}`);
processExistCode = 1; processExistCode = 1;

View file

@ -1,26 +1,16 @@
import { /* eslint-disable @typescript-eslint/no-unsafe-member-access */
Command, flags, /* eslint-disable no-console */
} from '@oclif/command'; import { Command, flags } from '@oclif/command';
import { import { IDataObject, LoggerProxy } from 'n8n-workflow';
IDataObject
} from 'n8n-workflow';
import { // eslint-disable-next-line @typescript-eslint/no-unused-vars
Db, import { Db, GenericHelpers } from '../../src';
GenericHelpers,
} from '../../src';
import { import { getLogger } from '../../src/Logger';
getLogger,
} from '../../src/Logger';
import {
LoggerProxy,
} from 'n8n-workflow';
export class UpdateWorkflowCommand extends Command { export class UpdateWorkflowCommand extends Command {
static description = '\Update workflows'; static description = 'Update workflows';
static examples = [ static examples = [
`$ n8n update:workflow --all --active=false`, `$ 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() { async run() {
const logger = getLogger(); const logger = getLogger();
LoggerProxy.init(logger); LoggerProxy.init(logger);
// eslint-disable-next-line @typescript-eslint/no-shadow
const { flags } = this.parse(UpdateWorkflowCommand); const { flags } = this.parse(UpdateWorkflowCommand);
if (!flags.all && !flags.id) { if (!flags.all && !flags.id) {
@ -52,7 +44,9 @@ export class UpdateWorkflowCommand extends Command {
} }
if (flags.all && flags.id) { 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; return;
} }
@ -60,13 +54,12 @@ export class UpdateWorkflowCommand extends Command {
if (flags.active === undefined) { if (flags.active === undefined) {
console.info(`No update flag like "--active=true" has been set!`); console.info(`No update flag like "--active=true" has been set!`);
return; return;
} else { }
if (!['false', 'true'].includes(flags.active)) { if (!['false', 'true'].includes(flags.active)) {
console.info(`Valid values for flag "--active" are only "false" or "true"!`); console.info(`Valid values for flag "--active" are only "false" or "true"!`);
return; return;
} }
updateQuery.active = flags.active === 'true'; updateQuery.active = flags.active === 'true';
}
try { try {
await Db.init(); await Db.init();
@ -80,6 +73,7 @@ export class UpdateWorkflowCommand extends Command {
findQuery.active = true; findQuery.active = true;
} }
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await Db.collections.Workflow!.update(findQuery, updateQuery); await Db.collections.Workflow!.update(findQuery, updateQuery);
console.info('Done'); console.info('Done');
} catch (e) { } catch (e) {

View file

@ -1,9 +1,14 @@
import { /* eslint-disable no-console */
UserSettings, /* eslint-disable @typescript-eslint/no-unsafe-call */
} from 'n8n-core'; /* 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'; import { Command, flags } from '@oclif/command';
// eslint-disable-next-line import/no-extraneous-dependencies
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import { IDataObject, LoggerProxy } from 'n8n-workflow';
import * as config from '../config'; import * as config from '../config';
import { import {
ActiveExecutions, ActiveExecutions,
@ -15,29 +20,20 @@ import {
GenericHelpers, GenericHelpers,
LoadNodesAndCredentials, LoadNodesAndCredentials,
NodeTypes, NodeTypes,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
TestWebhooks, TestWebhooks,
WebhookServer, WebhookServer,
} from '../src'; } from '../src';
import { IDataObject } from 'n8n-workflow';
import { import { getLogger } from '../src/Logger';
getLogger,
} from '../src/Logger';
import {
LoggerProxy,
} from 'n8n-workflow';
let activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner | undefined; let activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner | undefined;
let processExistCode = 0; let processExistCode = 0;
export class Webhook extends Command { export class Webhook extends Command {
static description = 'Starts n8n webhook process. Intercepts only production URLs.'; static description = 'Starts n8n webhook process. Intercepts only production URLs.';
static examples = [ static examples = [`$ n8n webhook`];
`$ n8n webhook`,
];
static flags = { static flags = {
help: flags.help({ char: 'h' }), 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 * Make for example sure that all the webhooks from third party services
* get removed. * get removed.
*/ */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
static async stopProcess() { static async stopProcess() {
LoggerProxy.info(`\nStopping n8n...`); LoggerProxy.info(`\nStopping n8n...`);
@ -68,14 +65,16 @@ export class Webhook extends Command {
let count = 0; let count = 0;
while (executingWorkflows.length !== 0) { while (executingWorkflows.length !== 0) {
if (count++ % 4 === 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) => { await new Promise((resolve) => {
setTimeout(resolve, 500); setTimeout(resolve, 500);
}); });
executingWorkflows = activeExecutionsInstance.getActiveExecutions(); executingWorkflows = activeExecutionsInstance.getActiveExecutions();
} }
} catch (error) { } catch (error) {
LoggerProxy.error('There was an error shutting down n8n.', error); LoggerProxy.error('There was an error shutting down n8n.', error);
} }
@ -83,7 +82,7 @@ export class Webhook extends Command {
process.exit(processExistCode); process.exit(processExistCode);
} }
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async run() { async run() {
const logger = getLogger(); const logger = getLogger();
LoggerProxy.init(logger); LoggerProxy.init(logger);
@ -92,6 +91,7 @@ export class Webhook extends Command {
process.on('SIGTERM', Webhook.stopProcess); process.on('SIGTERM', Webhook.stopProcess);
process.on('SIGINT', 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); const { flags } = this.parse(Webhook);
// Wrap that the process does not close but we can still use async // Wrap that the process does not close but we can still use async
@ -114,7 +114,8 @@ export class Webhook extends Command {
try { try {
// Start directly with the init of the database to improve startup time // 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}"`); logger.error(`There was an error initializing DB: "${error.message}"`);
processExistCode = 1; processExistCode = 1;
@ -124,6 +125,7 @@ export class Webhook extends Command {
}); });
// Make sure the settings exist // Make sure the settings exist
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const userSettings = await UserSettings.prepareUserSettings(); const userSettings = await UserSettings.prepareUserSettings();
// Load all node and credential types // Load all node and credential types
@ -153,9 +155,11 @@ export class Webhook extends Command {
const redisPort = config.get('queue.bull.redis.port'); const redisPort = config.get('queue.bull.redis.port');
const redisDB = config.get('queue.bull.redis.db'); const redisDB = config.get('queue.bull.redis.db');
const redisConnectionTimeoutLimit = config.get('queue.bull.redis.timeoutThreshold'); const redisConnectionTimeoutLimit = config.get('queue.bull.redis.timeoutThreshold');
let lastTimer = 0, cumulativeTimeout = 0; let lastTimer = 0;
let cumulativeTimeout = 0;
const settings = { const settings = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
retryStrategy: (times: number): number | null => { retryStrategy: (times: number): number | null => {
const now = Date.now(); const now = Date.now();
if (now - lastTimer > 30000) { if (now - lastTimer > 30000) {
@ -166,7 +170,10 @@ export class Webhook extends Command {
cumulativeTimeout += now - lastTimer; cumulativeTimeout += now - lastTimer;
lastTimer = now; lastTimer = now;
if (cumulativeTimeout > redisConnectionTimeoutLimit) { 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); process.exit(1);
} }
} }
@ -208,11 +215,12 @@ export class Webhook extends Command {
activeWorkflowRunner = ActiveWorkflowRunner.getInstance(); activeWorkflowRunner = ActiveWorkflowRunner.getInstance();
await activeWorkflowRunner.initWebhooks(); await activeWorkflowRunner.initWebhooks();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const editorUrl = GenericHelpers.getBaseUrl(); const editorUrl = GenericHelpers.getBaseUrl();
console.info('Webhook listener waiting for requests.'); console.info('Webhook listener waiting for requests.');
} catch (error) { } catch (error) {
console.error('Exiting due to error. See log message for details.'); 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}"`); logger.error(`Webhook process cannot continue. "${error.message}"`);
processExistCode = 1; processExistCode = 1;

View file

@ -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 * as PCancelable from 'p-cancelable';
import { Command, flags } from '@oclif/command'; import { Command, flags } from '@oclif/command';
import { import { UserSettings, WorkflowExecute } from 'n8n-core';
UserSettings,
WorkflowExecute,
} from 'n8n-core';
import { import {
IDataObject, IDataObject,
@ -13,12 +19,12 @@ import {
IWorkflowExecuteHooks, IWorkflowExecuteHooks,
Workflow, Workflow,
WorkflowHooks, WorkflowHooks,
LoggerProxy,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import { FindOneOptions } from 'typeorm';
FindOneOptions,
} from 'typeorm';
import * as Bull from 'bull';
import { import {
ActiveExecutions, ActiveExecutions,
CredentialsOverwrites, CredentialsOverwrites,
@ -37,24 +43,15 @@ import {
WorkflowExecuteAdditionalData, WorkflowExecuteAdditionalData,
} from '../src'; } from '../src';
import { import { getLogger } from '../src/Logger';
getLogger,
} from '../src/Logger';
import {
LoggerProxy,
} from 'n8n-workflow';
import * as config from '../config'; import * as config from '../config';
import * as Bull from 'bull';
import * as Queue from '../src/Queue'; import * as Queue from '../src/Queue';
export class Worker extends Command { export class Worker extends Command {
static description = '\nStarts a n8n worker'; static description = '\nStarts a n8n worker';
static examples = [ static examples = [`$ n8n worker --concurrency=5`];
`$ n8n worker --concurrency=5`,
];
static flags = { static flags = {
help: flags.help({ char: 'h' }), help: flags.help({ char: 'h' }),
@ -82,6 +79,7 @@ export class Worker extends Command {
LoggerProxy.info(`Stopping n8n...`); LoggerProxy.info(`Stopping n8n...`);
// Stop accepting new jobs // Stop accepting new jobs
// eslint-disable-next-line @typescript-eslint/no-floating-promises
Worker.jobQueue.pause(true); Worker.jobQueue.pause(true);
try { try {
@ -103,13 +101,17 @@ export class Worker extends Command {
while (Object.keys(Worker.runningJobs).length !== 0) { while (Object.keys(Worker.runningJobs).length !== 0) {
if (count++ % 4 === 0) { if (count++ % 4 === 0) {
const waitLeft = Math.ceil((stopTime - new Date().getTime()) / 1000); 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) => { await new Promise((resolve) => {
setTimeout(resolve, 500); setTimeout(resolve, 500);
}); });
} }
} catch (error) { } catch (error) {
LoggerProxy.error('There was an error shutting down n8n.', 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<IBullJobResponse> { async runJob(job: Bull.Job, nodeTypes: INodeTypes): Promise<IBullJobResponse> {
const jobData = job.data as IBullJobData; const jobData = job.data as IBullJobData;
const executionDb = await Db.collections.Execution!.findOne(jobData.executionId) as IExecutionFlattedDb; const executionDb = (await Db.collections.Execution!.findOne(
const currentExecutionDb = ResponseHelper.unflattenExecutionData(executionDb) as IExecutionResponse; jobData.executionId,
LoggerProxy.info(`Start job: ${job.id} (Workflow ID: ${currentExecutionDb.workflowData.id} | Execution: ${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; let { staticData } = currentExecutionDb.workflowData;
if (jobData.loadStaticData === true) { if (jobData.loadStaticData) {
const findOptions = { const findOptions = {
select: ['id', 'staticData'], select: ['id', 'staticData'],
} as FindOneOptions; } 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) { 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; staticData = workflowData.staticData;
} }
let workflowTimeout = config.get('executions.timeout') as number; // initialize with default let workflowTimeout = config.get('executions.timeout') as number; // initialize with default
if (currentExecutionDb.workflowData.settings && currentExecutionDb.workflowData.settings.executionTimeout) { if (
workflowTimeout = currentExecutionDb.workflowData.settings!.executionTimeout as number; // preference on workflow setting // 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; let executionTimeoutTimestamp: number | undefined;
@ -146,16 +161,37 @@ export class Worker extends Command {
executionTimeoutTimestamp = Date.now() + workflowTimeout * 1000; 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); const additionalData = await WorkflowExecuteAdditionalData.getBase(
additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(currentExecutionDb.mode, job.data.executionId, currentExecutionDb.workflowData, { retryOf: currentExecutionDb.retryOf as string }); undefined,
executionTimeoutTimestamp,
);
additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(
currentExecutionDb.mode,
job.data.executionId,
currentExecutionDb.workflowData,
{ retryOf: currentExecutionDb.retryOf as string },
);
additionalData.executionId = jobData.executionId; additionalData.executionId = jobData.executionId;
let workflowExecute: WorkflowExecute; let workflowExecute: WorkflowExecute;
let workflowRun: PCancelable<IRun>; let workflowRun: PCancelable<IRun>;
if (currentExecutionDb.data !== undefined) { if (currentExecutionDb.data !== undefined) {
workflowExecute = new WorkflowExecute(additionalData, currentExecutionDb.mode, currentExecutionDb.data); workflowExecute = new WorkflowExecute(
additionalData,
currentExecutionDb.mode,
currentExecutionDb.data,
);
workflowRun = workflowExecute.processRunExecutionData(workflow); workflowRun = workflowExecute.processRunExecutionData(workflow);
} else { } else {
// Execute all nodes // Execute all nodes
@ -180,6 +216,7 @@ export class Worker extends Command {
const logger = getLogger(); const logger = getLogger();
LoggerProxy.init(logger); LoggerProxy.init(logger);
// eslint-disable-next-line no-console
console.info('Starting n8n worker...'); console.info('Starting n8n worker...');
// Make sure that n8n shuts down gracefully if possible // Make sure that n8n shuts down gracefully if possible
@ -192,7 +229,7 @@ export class Worker extends Command {
const { flags } = this.parse(Worker); const { flags } = this.parse(Worker);
// Start directly with the init of the database to improve startup time // 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}"`); logger.error(`There was an error initializing DB: "${error.message}"`);
Worker.processExistCode = 1; Worker.processExistCode = 1;
@ -225,10 +262,12 @@ export class Worker extends Command {
// Wait till the database is ready // Wait till the database is ready
await startDbInitPromise; await startDbInitPromise;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const redisConnectionTimeoutLimit = config.get('queue.bull.redis.timeoutThreshold'); const redisConnectionTimeoutLimit = config.get('queue.bull.redis.timeoutThreshold');
Worker.jobQueue = Queue.getInstance().getBullObjectInstance(); 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(); 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) => { Worker.jobQueue.on('error', (error: Error) => {
if (error.toString().includes('ECONNREFUSED') === true) { if (error.toString().includes('ECONNREFUSED')) {
const now = Date.now(); const now = Date.now();
if (now - lastTimer > 30000) { if (now - lastTimer > 30000) {
// Means we had no timeout at all or last timeout was temporary and we recovered // 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; cumulativeTimeout += now - lastTimer;
lastTimer = now; lastTimer = now;
if (cumulativeTimeout > redisConnectionTimeoutLimit) { 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); process.exit(1);
} }
} }
logger.warn('Redis unavailable - trying to reconnect...'); 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 // This is a non-recoverable error
// Happens when worker starts and Redis is unavailable // Happens when worker starts and Redis is unavailable
// Even if Redis comes back online, worker will be zombie // Even if Redis comes back online, worker will be zombie
@ -287,6 +329,5 @@ export class Worker extends Command {
process.exit(1); process.exit(1);
} }
})(); })();
} }
} }

View file

@ -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 convict from 'convict';
import * as dotenv from 'dotenv'; import * as dotenv from 'dotenv';
import * as path from 'path'; import * as path from 'path';
@ -6,7 +9,6 @@ import * as core from 'n8n-core';
dotenv.config(); dotenv.config();
const config = convict({ const config = convict({
database: { database: {
type: { type: {
doc: 'Type of database to use', doc: 'Type of database to use',
@ -84,7 +86,6 @@ const config = convict({
env: 'DB_POSTGRESDB_SSL_REJECT_UNAUTHORIZED', env: 'DB_POSTGRESDB_SSL_REJECT_UNAUTHORIZED',
}, },
}, },
}, },
mysqldb: { mysqldb: {
database: { database: {
@ -159,7 +160,6 @@ const config = convict({
}, },
executions: { executions: {
// By default workflows get always executed in their own process. // By default workflows get always executed in their own process.
// If this option gets set to "main" it will run them in the // If this option gets set to "main" it will run them in the
// main-process instead. // main-process instead.
@ -573,7 +573,6 @@ const config = convict({
throw new Error(); throw new Error();
} }
} }
} catch (error) { } catch (error) {
throw new TypeError(`The Nodes to exclude is not a valid Array of strings.`); 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', env: 'N8N_VERSION_NOTIFICATIONS_INFO_URL',
}, },
}, },
}); });
// Overwrite default configuration with settings which got defined in // Overwrite default configuration with settings which got defined in

View file

@ -4,88 +4,72 @@ import { entities } from '../src/databases/entities';
module.exports = [ module.exports = [
{ {
"name": "sqlite", name: 'sqlite',
"type": "sqlite", type: 'sqlite',
"logging": true, logging: true,
"entities": Object.values(entities), entities: Object.values(entities),
"database": path.join(UserSettings.getUserN8nFolderPath(), 'database.sqlite'), database: path.join(UserSettings.getUserN8nFolderPath(), 'database.sqlite'),
"migrations": [ migrations: ['./src/databases/sqlite/migrations/*.ts'],
"./src/databases/sqlite/migrations/*.ts" subscribers: ['./src/databases/sqlite/subscribers/*.ts'],
], cli: {
"subscribers": [ entitiesDir: './src/databases/entities',
"./src/databases/sqlite/subscribers/*.ts" migrationsDir: './src/databases/sqlite/migrations',
], subscribersDir: './src/databases/sqlite/subscribers',
"cli": { },
"entitiesDir": "./src/databases/entities",
"migrationsDir": "./src/databases/sqlite/migrations",
"subscribersDir": "./src/databases/sqlite/subscribers"
}
}, },
{ {
"name": "postgres", name: 'postgres',
"type": "postgres", type: 'postgres',
"logging": false, logging: false,
"host": "localhost", host: 'localhost',
"username": "postgres", username: 'postgres',
"password": "", password: '',
"port": 5432, port: 5432,
"database": "n8n", database: 'n8n',
"schema": "public", schema: 'public',
"entities": Object.values(entities), entities: Object.values(entities),
"migrations": [ migrations: ['./src/databases/postgresdb/migrations/*.ts'],
"./src/databases/postgresdb/migrations/*.ts" subscribers: ['src/subscriber/**/*.ts'],
], cli: {
"subscribers": [ entitiesDir: './src/databases/entities',
"src/subscriber/**/*.ts" migrationsDir: './src/databases/postgresdb/migrations',
], subscribersDir: './src/databases/postgresdb/subscribers',
"cli": { },
"entitiesDir": "./src/databases/entities",
"migrationsDir": "./src/databases/postgresdb/migrations",
"subscribersDir": "./src/databases/postgresdb/subscribers"
}
}, },
{ {
"name": "mysql", name: 'mysql',
"type": "mysql", type: 'mysql',
"database": "n8n", database: 'n8n',
"username": "root", username: 'root',
"password": "password", password: 'password',
"host": "localhost", host: 'localhost',
"port": "3306", port: '3306',
"logging": false, logging: false,
"entities": Object.values(entities), entities: Object.values(entities),
"migrations": [ migrations: ['./src/databases/mysqldb/migrations/*.ts'],
"./src/databases/mysqldb/migrations/*.ts" subscribers: ['src/subscriber/**/*.ts'],
], cli: {
"subscribers": [ entitiesDir: './src/databases/entities',
"src/subscriber/**/*.ts" migrationsDir: './src/databases/mysqldb/migrations',
], subscribersDir: './src/databases/mysqldb/Subscribers',
"cli": { },
"entitiesDir": "./src/databases/entities",
"migrationsDir": "./src/databases/mysqldb/migrations",
"subscribersDir": "./src/databases/mysqldb/Subscribers"
}
}, },
{ {
"name": "mariadb", name: 'mariadb',
"type": "mariadb", type: 'mariadb',
"database": "n8n", database: 'n8n',
"username": "root", username: 'root',
"password": "password", password: 'password',
"host": "localhost", host: 'localhost',
"port": "3306", port: '3306',
"logging": false, logging: false,
"entities": Object.values(entities), entities: Object.values(entities),
"migrations": [ migrations: ['./src/databases/mysqldb/migrations/*.ts'],
"./src/databases/mysqldb/migrations/*.ts" subscribers: ['src/subscriber/**/*.ts'],
], cli: {
"subscribers": [ entitiesDir: './src/databases/entities',
"src/subscriber/**/*.ts" migrationsDir: './src/databases/mysqldb/migrations',
], subscribersDir: './src/databases/mysqldb/Subscribers',
"cli": { },
"entitiesDir": "./src/databases/entities",
"migrationsDir": "./src/databases/mysqldb/migrations",
"subscribersDir": "./src/databases/mysqldb/Subscribers"
}
}, },
]; ];

View file

@ -21,14 +21,15 @@
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"dev": "concurrently -k -n \"TypeScript,Node\" -c \"yellow.bold,cyan.bold\" \"npm run watch\" \"nodemon\"", "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", "postpack": "rm -f oclif.manifest.json",
"prepack": "echo \"Building project...\" && rm -rf dist && tsc -b && oclif-dev manifest", "prepack": "echo \"Building project...\" && rm -rf dist && tsc -b && oclif-dev manifest",
"start": "run-script-os", "start": "run-script-os",
"start:default": "cd bin && ./n8n", "start:default": "cd bin && ./n8n",
"start:windows": "cd bin && n8n", "start:windows": "cd bin && n8n",
"test": "jest", "test": "jest",
"tslint": "tslint -p tsconfig.json -c tslint.json",
"tslintfix": "tslint --fix -p tsconfig.json -c tslint.json",
"watch": "tsc --watch", "watch": "tsc --watch",
"typeorm": "ts-node ./node_modules/typeorm/cli.js" "typeorm": "ts-node ./node_modules/typeorm/cli.js"
}, },
@ -77,7 +78,7 @@
"ts-jest": "^26.3.0", "ts-jest": "^26.3.0",
"ts-node": "^8.9.1", "ts-node": "^8.9.1",
"tslint": "^6.1.2", "tslint": "^6.1.2",
"typescript": "~3.9.7" "typescript": "~4.3.5"
}, },
"dependencies": { "dependencies": {
"@oclif/command": "^1.5.18", "@oclif/command": "^1.5.18",

View file

@ -1,11 +1,18 @@
import { /* eslint-disable prefer-template */
IRun, /* eslint-disable @typescript-eslint/restrict-plus-operands */
} from 'n8n-workflow'; /* 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 { import { createDeferredPromise } from 'n8n-core';
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 { import {
Db, Db,
IExecutingWorkflowData, IExecutingWorkflowData,
@ -17,16 +24,11 @@ import {
WorkflowHelpers, WorkflowHelpers,
} from '.'; } from '.';
import { ChildProcess } from 'child_process';
import * as PCancelable from 'p-cancelable';
export class ActiveExecutions { export class ActiveExecutions {
private activeExecutions: { private activeExecutions: {
[index: string]: IExecutingWorkflowData; [index: string]: IExecutingWorkflowData;
} = {}; } = {};
/** /**
* Add a new active execution * Add a new active execution
* *
@ -35,8 +37,11 @@ export class ActiveExecutions {
* @returns {string} * @returns {string}
* @memberof ActiveExecutions * @memberof ActiveExecutions
*/ */
async add(executionData: IWorkflowExecutionDataProcess, process?: ChildProcess, executionId?: string): Promise<string> { async add(
executionData: IWorkflowExecutionDataProcess,
process?: ChildProcess,
executionId?: string,
): Promise<string> {
if (executionId === undefined) { if (executionId === undefined) {
// Is a new execution so save in DB // Is a new execution so save in DB
@ -52,14 +57,23 @@ export class ActiveExecutions {
fullExecutionData.retryOf = executionData.retryOf.toString(); 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(); fullExecutionData.workflowId = executionData.workflowData.id.toString();
} }
const execution = ResponseHelper.flattenExecutionData(fullExecutionData); const execution = ResponseHelper.flattenExecutionData(fullExecutionData);
const executionResult = await Db.collections.Execution!.save(execution as IExecutionFlattedDb); const executionResult = await Db.collections.Execution!.save(
executionId = typeof executionResult.id === "object" ? executionResult.id!.toString() : executionResult.id + ""; execution as IExecutionFlattedDb,
);
executionId =
typeof executionResult.id === 'object'
? // @ts-ignore
executionResult.id!.toString()
: executionResult.id + '';
} else { } else {
// Is an existing execution we want to finish so update in DB // 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); await Db.collections.Execution!.update(executionId, execution);
} }
// @ts-ignore
this.activeExecutions[executionId] = { this.activeExecutions[executionId] = {
executionData, executionData,
process, process,
@ -79,10 +94,10 @@ export class ActiveExecutions {
postExecutePromises: [], postExecutePromises: [],
}; };
// @ts-ignore
return executionId; return executionId;
} }
/** /**
* Attaches an execution * Attaches an execution
* *
@ -90,15 +105,17 @@ export class ActiveExecutions {
* @param {PCancelable<IRun>} workflowExecution * @param {PCancelable<IRun>} workflowExecution
* @memberof ActiveExecutions * @memberof ActiveExecutions
*/ */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
attachWorkflowExecution(executionId: string, workflowExecution: PCancelable<IRun>) { attachWorkflowExecution(executionId: string, workflowExecution: PCancelable<IRun>) {
if (this.activeExecutions[executionId] === undefined) { 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; this.activeExecutions[executionId].workflowExecution = workflowExecution;
} }
/** /**
* Remove an active execution * Remove an active execution
* *
@ -113,6 +130,7 @@ export class ActiveExecutions {
} }
// Resolve all the waiting promises // Resolve all the waiting promises
// eslint-disable-next-line no-restricted-syntax
for (const promise of this.activeExecutions[executionId].postExecutePromises) { for (const promise of this.activeExecutions[executionId].postExecutePromises) {
promise.resolve(fullRunData); promise.resolve(fullRunData);
} }
@ -121,7 +139,6 @@ export class ActiveExecutions {
delete this.activeExecutions[executionId]; delete this.activeExecutions[executionId];
} }
/** /**
* Forces an execution to stop * Forces an execution to stop
* *
@ -144,7 +161,8 @@ export class ActiveExecutions {
setTimeout(() => { setTimeout(() => {
// execute on next event loop tick; // execute on next event loop tick;
this.activeExecutions[executionId].process!.send({ this.activeExecutions[executionId].process!.send({
type: timeout ? timeout : 'stopExecution', // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
type: timeout || 'stopExecution',
}); });
}, 1); }, 1);
} }
@ -153,10 +171,10 @@ export class ActiveExecutions {
this.activeExecutions[executionId].workflowExecution!.cancel(); this.activeExecutions[executionId].workflowExecution!.cancel();
} }
// eslint-disable-next-line consistent-return
return this.getPostExecutePromise(executionId); return this.getPostExecutePromise(executionId);
} }
/** /**
* Returns a promise which will resolve with the data of the execution * Returns a promise which will resolve with the data of the execution
* with the given id * with the given id
@ -178,7 +196,6 @@ export class ActiveExecutions {
return waitPromise.promise(); return waitPromise.promise();
} }
/** /**
* Returns all the currently active executions * Returns all the currently active executions
* *
@ -189,25 +206,22 @@ export class ActiveExecutions {
const returnData: IExecutionsCurrentSummary[] = []; const returnData: IExecutionsCurrentSummary[] = [];
let data; let data;
// eslint-disable-next-line no-restricted-syntax
for (const id of Object.keys(this.activeExecutions)) { for (const id of Object.keys(this.activeExecutions)) {
data = this.activeExecutions[id]; data = this.activeExecutions[id];
returnData.push( returnData.push({
{
id, id,
retryOf: data.executionData.retryOf as string | undefined, retryOf: data.executionData.retryOf as string | undefined,
startedAt: data.startedAt, startedAt: data.startedAt,
mode: data.executionData.executionMode, mode: data.executionData.executionMode,
workflowId: data.executionData.workflowData.id! as string, workflowId: data.executionData.workflowData.id! as string,
} });
);
} }
return returnData; return returnData;
} }
} }
let activeExecutionsInstance: ActiveExecutions | undefined; let activeExecutionsInstance: ActiveExecutions | undefined;
export function getInstance(): ActiveExecutions { export function getInstance(): ActiveExecutions {

View file

@ -1,23 +1,15 @@
import { /* eslint-disable prefer-spread */
Db, /* eslint-disable @typescript-eslint/no-non-null-assertion */
IActivationError, /* eslint-disable no-param-reassign */
IResponseCallbackData, /* eslint-disable no-console */
IWebhookDb, /* eslint-disable no-await-in-loop */
IWorkflowDb, /* eslint-disable no-restricted-syntax */
IWorkflowExecutionDataProcess, /* eslint-disable @typescript-eslint/no-floating-promises */
NodeTypes, /* eslint-disable @typescript-eslint/no-shadow */
ResponseHelper, /* eslint-disable @typescript-eslint/no-unsafe-call */
WebhookHelpers, /* eslint-disable @typescript-eslint/no-unsafe-member-access */
WorkflowCredentials, /* eslint-disable @typescript-eslint/no-unsafe-assignment */
WorkflowExecuteAdditionalData, import { ActiveWorkflows, NodeExecuteFunctions } from 'n8n-core';
WorkflowHelpers,
WorkflowRunner,
} from './';
import {
ActiveWorkflows,
NodeExecuteFunctions,
} from 'n8n-core';
import { import {
IExecuteData, IExecuteData,
@ -32,12 +24,28 @@ import {
Workflow, Workflow,
WorkflowActivateMode, WorkflowActivateMode,
WorkflowExecuteMode, WorkflowExecuteMode,
LoggerProxy as Logger,
} from 'n8n-workflow'; } from 'n8n-workflow';
import * as express from 'express'; import * as express from 'express';
// eslint-disable-next-line import/no-cycle
import { import {
LoggerProxy as Logger, Db,
} from 'n8n-workflow'; 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)`; 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; [key: string]: IActivationError;
} = {}; } = {};
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async init() { async init() {
// Get the active workflows from database // Get the active workflows from database
// NOTE // NOTE
// Here I guess we can have a flag on the workflow table like hasTrigger // 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 // 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 // Clear up active workflow table
await Db.collections.Webhook?.clear(); await Db.collections.Webhook?.clear();
@ -69,21 +79,32 @@ export class ActiveWorkflowRunner {
for (const workflowData of workflowsData) { for (const workflowData of workflowsData) {
console.log(` - ${workflowData.name}`); 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 { try {
await this.add(workflowData.id.toString(), 'init', workflowData); 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`); console.log(` => Started`);
} catch (error) { } catch (error) {
console.log(` => ERROR: Workflow could not be activated:`); console.log(` => ERROR: Workflow could not be activated:`);
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
console.log(` ${error.message}`); 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)'); Logger.verbose('Finished initializing active workflows (startup)');
} }
} }
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async initWebhooks() { async initWebhooks() {
this.activeWorkflows = new ActiveWorkflows(); this.activeWorkflows = new ActiveWorkflows();
} }
@ -104,7 +125,10 @@ export class ActiveWorkflowRunner {
} }
const activeWorkflows = await this.getActiveWorkflows(); 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 = []; const removePromises = [];
for (const workflowId of activeWorkflowId) { for (const workflowId of activeWorkflowId) {
@ -112,7 +136,6 @@ export class ActiveWorkflowRunner {
} }
await Promise.all(removePromises); await Promise.all(removePromises);
return;
} }
/** /**
@ -125,10 +148,19 @@ export class ActiveWorkflowRunner {
* @returns {Promise<object>} * @returns {Promise<object>}
* @memberof ActiveWorkflowRunner * @memberof ActiveWorkflowRunner
*/ */
async executeWebhook(httpMethod: WebhookHttpMethod, path: string, req: express.Request, res: express.Response): Promise<IResponseCallbackData> { async executeWebhook(
httpMethod: WebhookHttpMethod,
path: string,
req: express.Request,
res: express.Response,
): Promise<IResponseCallbackData> {
Logger.debug(`Received webhoook "${httpMethod}" for path "${path}"`); Logger.debug(`Received webhoook "${httpMethod}" for path "${path}"`);
if (this.activeWorkflows === null) { 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 // Reset request parameters
@ -139,7 +171,10 @@ export class ActiveWorkflowRunner {
path = path.slice(0, -1); 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; let webhookId: string | undefined;
// check if path is dynamic // check if path is dynamic
@ -147,19 +182,30 @@ export class ActiveWorkflowRunner {
// check if a dynamic webhook path exists // check if a dynamic webhook path exists
const pathElements = path.split('/'); const pathElements = path.split('/');
webhookId = pathElements.shift(); 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) { if (dynamicWebhooks === undefined || dynamicWebhooks.length === 0) {
// The requested webhook is not registered // 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; let maxMatches = 0;
const pathElementsSet = new Set(pathElements); const pathElementsSet = new Set(pathElements);
// check if static elements match in path // check if static elements match in path
// if more results have been returned choose the one with the most static-route matches // if more results have been returned choose the one with the most static-route matches
dynamicWebhooks.forEach(dynamicWebhook => { dynamicWebhooks.forEach((dynamicWebhook) => {
const staticElements = dynamicWebhook.webhookPath.split('/').filter(ele => !ele.startsWith(':')); const staticElements = dynamicWebhook.webhookPath
const allStaticExist = staticElements.every(staticEle => pathElementsSet.has(staticEle)); .split('/')
.filter((ele) => !ele.startsWith(':'));
const allStaticExist = staticElements.every((staticEle) => pathElementsSet.has(staticEle));
if (allStaticExist && staticElements.length > maxMatches) { if (allStaticExist && staticElements.length > maxMatches) {
maxMatches = staticElements.length; maxMatches = staticElements.length;
@ -171,12 +217,20 @@ export class ActiveWorkflowRunner {
} }
}); });
if (webhook === undefined) { 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 // extracting params from path
webhook!.webhookPath.split('/').forEach((ele, index) => { // @ts-ignore
webhook.webhookPath.split('/').forEach((ele, index) => {
if (ele.startsWith(':')) { if (ele.startsWith(':')) {
// write params to req.params // write params to req.params
req.params[ele.slice(1)] = pathElements[index]; req.params[ele.slice(1)] = pathElements[index];
@ -186,16 +240,33 @@ export class ActiveWorkflowRunner {
const workflowData = await Db.collections.Workflow!.findOne(webhook.workflowId); const workflowData = await Db.collections.Workflow!.findOne(webhook.workflowId);
if (workflowData === undefined) { 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 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 additionalData = await WorkflowExecuteAdditionalData.getBase();
const webhookData = NodeHelpers.getNodeWebhooks(workflow, workflow.getNode(webhook.node as string) as INode, additionalData).filter((webhook) => { const webhookData = NodeHelpers.getNodeWebhooks(
return (webhook.httpMethod === httpMethod && webhook.path === path); workflow,
workflow.getNode(webhook.node) as INode,
additionalData,
).filter((webhook) => {
return webhook.httpMethod === httpMethod && webhook.path === path;
})[0]; })[0];
// Get the node which has the webhook defined to know where to start from and to // 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) => { return new Promise((resolve, reject) => {
const executionMode = 'webhook'; const executionMode = 'webhook';
//@ts-ignore // @ts-ignore
WebhookHelpers.executeWebhook(workflow, webhookData, workflowData, workflowStartNode, executionMode, undefined, undefined, undefined, req, res, (error: Error | null, data: object) => { 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) { if (error !== null) {
return reject(error); return reject(error);
} }
resolve(data); resolve(data);
}); },
);
}); });
} }
@ -226,10 +310,10 @@ export class ActiveWorkflowRunner {
* @memberof ActiveWorkflowRunner * @memberof ActiveWorkflowRunner
*/ */
async getWebhookMethods(path: string): Promise<string[]> { async getWebhookMethods(path: string): Promise<string[]> {
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 // 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; return webhookMethods;
} }
@ -240,11 +324,15 @@ export class ActiveWorkflowRunner {
* @memberof ActiveWorkflowRunner * @memberof ActiveWorkflowRunner
*/ */
async getActiveWorkflows(): Promise<IWorkflowDb[]> { async getActiveWorkflows(): Promise<IWorkflowDb[]> {
const activeWorkflows = await Db.collections.Workflow?.find({ where: { active: true }, select: ['id'] }) as IWorkflowDb[]; const activeWorkflows = (await Db.collections.Workflow?.find({
return activeWorkflows.filter(workflow => this.activationErrors[workflow.id.toString()] === undefined); where: { active: true },
select: ['id'],
})) as IWorkflowDb[];
return activeWorkflows.filter(
(workflow) => this.activationErrors[workflow.id.toString()] === undefined,
);
} }
/** /**
* Returns if the workflow is active * Returns if the workflow is active
* *
@ -253,8 +341,8 @@ export class ActiveWorkflowRunner {
* @memberof ActiveWorkflowRunner * @memberof ActiveWorkflowRunner
*/ */
async isActive(id: string): Promise<boolean> { async isActive(id: string): Promise<boolean> {
const workflow = await Db.collections.Workflow?.findOne({ id: Number(id) }) as IWorkflowDb; const workflow = (await Db.collections.Workflow?.findOne({ id: Number(id) })) as IWorkflowDb;
return workflow?.active as boolean; return workflow?.active;
} }
/** /**
@ -281,12 +369,16 @@ export class ActiveWorkflowRunner {
* @returns {Promise<void>} * @returns {Promise<void>}
* @memberof ActiveWorkflowRunner * @memberof ActiveWorkflowRunner
*/ */
async addWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): Promise<void> { async addWorkflowWebhooks(
workflow: Workflow,
additionalData: IWorkflowExecuteAdditionalDataWorkflow,
mode: WorkflowExecuteMode,
activation: WorkflowActivateMode,
): Promise<void> {
const webhooks = WebhookHelpers.getWorkflowWebhooks(workflow, additionalData, undefined, true); const webhooks = WebhookHelpers.getWorkflowWebhooks(workflow, additionalData, undefined, true);
let path = '' as string | undefined; let path = '' as string | undefined;
for (const webhookData of webhooks) { for (const webhookData of webhooks) {
const node = workflow.getNode(webhookData.node) as INode; const node = workflow.getNode(webhookData.node) as INode;
node.name = webhookData.node; node.name = webhookData.node;
@ -312,18 +404,35 @@ export class ActiveWorkflowRunner {
} }
try { try {
// eslint-disable-next-line no-await-in-loop
await Db.collections.Webhook?.insert(webhook); 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 (webhookExists !== true) {
// If webhook does not exist yet create it // 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) { } catch (error) {
try { try {
await this.removeWorkflowWebhooks(workflow.id as string); await this.removeWorkflowWebhooks(workflow.id as string);
} catch (error) { } 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 = ''; let errorMessage = '';
@ -337,6 +446,7 @@ export class ActiveWorkflowRunner {
// it's a error runnig the webhook methods (checkExists, create) // it's a error runnig the webhook methods (checkExists, create)
errorMessage = error.detail; errorMessage = error.detail;
} else { } else {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
errorMessage = error.message; errorMessage = error.message;
} }
@ -347,7 +457,6 @@ export class ActiveWorkflowRunner {
await WorkflowHelpers.saveStaticData(workflow); await WorkflowHelpers.saveStaticData(workflow);
} }
/** /**
* Remove all the webhooks of the workflow * Remove all the webhooks of the workflow
* *
@ -362,7 +471,16 @@ export class ActiveWorkflowRunner {
} }
const nodeTypes = NodeTypes(); 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'; const mode = 'internal';
@ -371,7 +489,14 @@ export class ActiveWorkflowRunner {
const webhooks = WebhookHelpers.getWorkflowWebhooks(workflow, additionalData, undefined, true); const webhooks = WebhookHelpers.getWorkflowWebhooks(workflow, additionalData, undefined, true);
for (const webhookData of webhooks) { 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); await WorkflowHelpers.saveStaticData(workflow);
@ -394,7 +519,14 @@ export class ActiveWorkflowRunner {
* @returns * @returns
* @memberof ActiveWorkflowRunner * @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[] = [ const nodeExecutionStack: IExecuteData[] = [
{ {
node, node,
@ -427,7 +559,6 @@ export class ActiveWorkflowRunner {
return workflowRunner.run(runData, true); return workflowRunner.run(runData, true);
} }
/** /**
* Return poll function which gets the global functions from n8n-core * Return poll function which gets the global functions from n8n-core
* and overwrites the __emit to be able to start it in subprocess * and overwrites the __emit to be able to start it in subprocess
@ -438,18 +569,30 @@ export class ActiveWorkflowRunner {
* @returns {IGetExecutePollFunctions} * @returns {IGetExecutePollFunctions}
* @memberof ActiveWorkflowRunner * @memberof ActiveWorkflowRunner
*/ */
getExecutePollFunctions(workflowData: IWorkflowDb, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): IGetExecutePollFunctions { getExecutePollFunctions(
return ((workflow: Workflow, node: INode) => { workflowData: IWorkflowDb,
const returnFunctions = NodeExecuteFunctions.getExecutePollFunctions(workflow, node, additionalData, mode, activation); 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 => { 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}"`); Logger.debug(`Received event to trigger execution for workflow "${workflow.name}"`);
this.runWorkflow(workflowData, node, data, additionalData, mode); this.runWorkflow(workflowData, node, data, additionalData, mode);
}; };
return returnFunctions; return returnFunctions;
}); };
} }
/** /**
* Return trigger function which gets the global functions from n8n-core * Return trigger function which gets the global functions from n8n-core
* and overwrites the emit to be able to start it in subprocess * and overwrites the emit to be able to start it in subprocess
@ -460,16 +603,31 @@ export class ActiveWorkflowRunner {
* @returns {IGetExecuteTriggerFunctions} * @returns {IGetExecuteTriggerFunctions}
* @memberof ActiveWorkflowRunner * @memberof ActiveWorkflowRunner
*/ */
getExecuteTriggerFunctions(workflowData: IWorkflowDb, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): IGetExecuteTriggerFunctions { getExecuteTriggerFunctions(
return ((workflow: Workflow, node: INode) => { workflowData: IWorkflowDb,
const returnFunctions = NodeExecuteFunctions.getExecuteTriggerFunctions(workflow, node, additionalData, mode, activation); 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 => { returnFunctions.emit = (data: INodeExecutionData[][]): void => {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
Logger.debug(`Received trigger for workflow "${workflow.name}"`); Logger.debug(`Received trigger for workflow "${workflow.name}"`);
WorkflowHelpers.saveStaticData(workflow); 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; return returnFunctions;
}); };
} }
/** /**
@ -480,7 +638,11 @@ export class ActiveWorkflowRunner {
* @returns {Promise<void>} * @returns {Promise<void>}
* @memberof ActiveWorkflowRunner * @memberof ActiveWorkflowRunner
*/ */
async add(workflowId: string, activation: WorkflowActivateMode, workflowData?: IWorkflowDb): Promise<void> { async add(
workflowId: string,
activation: WorkflowActivateMode,
workflowData?: IWorkflowDb,
): Promise<void> {
if (this.activeWorkflows === null) { if (this.activeWorkflows === null) {
throw new Error(`The "activeWorkflows" instance did not get initialized yet.`); throw new Error(`The "activeWorkflows" instance did not get initialized yet.`);
} }
@ -488,33 +650,69 @@ export class ActiveWorkflowRunner {
let workflowInstance: Workflow; let workflowInstance: Workflow;
try { try {
if (workflowData === undefined) { if (workflowData === undefined) {
workflowData = await Db.collections.Workflow!.findOne(workflowId) as IWorkflowDb; workflowData = (await Db.collections.Workflow!.findOne(workflowId)) as IWorkflowDb;
} }
if (!workflowData) { if (!workflowData) {
throw new Error(`Could not find workflow with id "${workflowId}".`); throw new Error(`Could not find workflow with id "${workflowId}".`);
} }
const nodeTypes = NodeTypes(); 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']); const canBeActivated = workflowInstance.checkIfWorkflowCanBeActivated([
if (canBeActivated === false) { 'n8n-nodes-base.start',
]);
if (!canBeActivated) {
Logger.error(`Unable to activate workflow "${workflowData.name}"`); 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 mode = 'trigger';
const additionalData = await WorkflowExecuteAdditionalData.getBase(); const additionalData = await WorkflowExecuteAdditionalData.getBase();
const getTriggerFunctions = this.getExecuteTriggerFunctions(workflowData, additionalData, mode, activation); const getTriggerFunctions = this.getExecuteTriggerFunctions(
const getPollFunctions = this.getExecutePollFunctions(workflowData, additionalData, mode, activation); workflowData,
additionalData,
mode,
activation,
);
const getPollFunctions = this.getExecutePollFunctions(
workflowData,
additionalData,
mode,
activation,
);
// Add the workflows which have webhooks defined // Add the workflows which have webhooks defined
await this.addWorkflowWebhooks(workflowInstance, additionalData, mode, activation); await this.addWorkflowWebhooks(workflowInstance, additionalData, mode, activation);
if (workflowInstance.getTriggerNodes().length !== 0 if (
|| workflowInstance.getPollNodes().length !== 0) { workflowInstance.getTriggerNodes().length !== 0 ||
await this.activeWorkflows.add(workflowId, workflowInstance, additionalData, mode, activation, getTriggerFunctions, getPollFunctions); workflowInstance.getPollNodes().length !== 0
Logger.verbose(`Successfully activated workflow "${workflowData.name}"`, { workflowId, workflowName: workflowData.name }); ) {
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) { if (this.activationErrors[workflowId] !== undefined) {
@ -548,13 +746,15 @@ export class ActiveWorkflowRunner {
* @memberof ActiveWorkflowRunner * @memberof ActiveWorkflowRunner
*/ */
async remove(workflowId: string): Promise<void> { async remove(workflowId: string): Promise<void> {
if (this.activeWorkflows !== null) { if (this.activeWorkflows !== null) {
// Remove all the webhooks of the workflow // Remove all the webhooks of the workflow
try { try {
await this.removeWorkflowWebhooks(workflowId); await this.removeWorkflowWebhooks(workflowId);
} catch (error) { } 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) { if (this.activationErrors[workflowId] !== undefined) {
@ -576,8 +776,6 @@ export class ActiveWorkflowRunner {
} }
} }
let workflowRunnerInstance: ActiveWorkflowRunner | undefined; let workflowRunnerInstance: ActiveWorkflowRunner | undefined;
export function getInstance(): ActiveWorkflowRunner { export function getInstance(): ActiveWorkflowRunner {

View file

@ -1,32 +1,30 @@
import { import { ICredentialType, ICredentialTypes as ICredentialTypesInterface } from 'n8n-workflow';
ICredentialType,
ICredentialTypes as ICredentialTypesInterface,
} from 'n8n-workflow';
import { // eslint-disable-next-line import/no-cycle
CredentialsOverwrites, import { CredentialsOverwrites, ICredentialsTypeData } from '.';
ICredentialsTypeData,
} from './';
class CredentialTypesClass implements ICredentialTypesInterface { class CredentialTypesClass implements ICredentialTypesInterface {
credentialTypes: ICredentialsTypeData = {}; credentialTypes: ICredentialsTypeData = {};
async init(credentialTypes: ICredentialsTypeData): Promise<void> { async init(credentialTypes: ICredentialsTypeData): Promise<void> {
this.credentialTypes = credentialTypes; this.credentialTypes = credentialTypes;
// Load the credentials overwrites if any exist // Load the credentials overwrites if any exist
const credentialsOverwrites = CredentialsOverwrites().getAll(); const credentialsOverwrites = CredentialsOverwrites().getAll();
// eslint-disable-next-line no-restricted-syntax
for (const credentialType of Object.keys(credentialsOverwrites)) { for (const credentialType of Object.keys(credentialsOverwrites)) {
if (credentialTypes[credentialType] === undefined) { if (credentialTypes[credentialType] === undefined) {
// eslint-disable-next-line no-continue
continue; continue;
} }
// Add which properties got overwritten that the Editor-UI knows // Add which properties got overwritten that the Editor-UI knows
// which properties it should hide // 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; let credentialTypesInstance: CredentialTypesClass | undefined;
// eslint-disable-next-line @typescript-eslint/naming-convention
export function CredentialTypes(): CredentialTypesClass { export function CredentialTypes(): CredentialTypesClass {
if (credentialTypesInstance === undefined) { if (credentialTypesInstance === undefined) {
credentialTypesInstance = new CredentialTypesClass(); credentialTypesInstance = new CredentialTypesClass();

View file

@ -1,6 +1,4 @@
import { import { Credentials } from 'n8n-core';
Credentials,
} from 'n8n-core';
import { import {
ICredentialDataDecryptedObject, ICredentialDataDecryptedObject,
@ -17,29 +15,24 @@ import {
WorkflowExecuteMode, WorkflowExecuteMode,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { // eslint-disable-next-line import/no-cycle
CredentialsOverwrites, import { CredentialsOverwrites, CredentialTypes, Db, ICredentialsDb } from '.';
CredentialTypes,
Db,
ICredentialsDb,
} from './';
const mockNodeTypes: INodeTypes = { const mockNodeTypes: INodeTypes = {
nodeTypes: {}, nodeTypes: {},
init: async (nodeTypes?: INodeTypeData): Promise<void> => { }, // eslint-disable-next-line @typescript-eslint/no-unused-vars
init: async (nodeTypes?: INodeTypeData): Promise<void> => {},
getAll: (): INodeType[] => { getAll: (): INodeType[] => {
// Does not get used in Workflow so no need to return it // Does not get used in Workflow so no need to return it
return []; return [];
}, },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getByName: (nodeType: string): INodeType | undefined => { getByName: (nodeType: string): INodeType | undefined => {
return undefined; return undefined;
}, },
}; };
export class CredentialsHelper extends ICredentialsHelper { export class CredentialsHelper extends ICredentialsHelper {
/** /**
* Returns the credentials instance * Returns the credentials instance
* *
@ -49,23 +42,27 @@ export class CredentialsHelper extends ICredentialsHelper {
* @memberof CredentialsHelper * @memberof CredentialsHelper
*/ */
async getCredentials(name: string, type: string): Promise<Credentials> { async getCredentials(name: string, type: string): Promise<Credentials> {
const credentialsDb = await Db.collections.Credentials?.find({ type });
const credentialsDb = await Db.collections.Credentials?.find({type});
if (credentialsDb === undefined || credentialsDb.length === 0) { if (credentialsDb === undefined || credentialsDb.length === 0) {
throw new Error(`No credentials of type "${type}" exist.`); 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) { if (credential === undefined) {
throw new Error(`No credentials with name "${name}" exist for type "${type}".`); 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 * Returns all the properties of the credentials with the given name
* *
@ -86,6 +83,7 @@ export class CredentialsHelper extends ICredentialsHelper {
} }
const combineProperties = [] as INodeProperties[]; const combineProperties = [] as INodeProperties[];
// eslint-disable-next-line no-restricted-syntax
for (const credentialsTypeName of credentialTypeData.extends) { for (const credentialsTypeName of credentialTypeData.extends) {
const mergeCredentialProperties = this.getCredentialsProperties(credentialsTypeName); const mergeCredentialProperties = this.getCredentialsProperties(credentialsTypeName);
NodeHelpers.mergeNodeProperties(combineProperties, mergeCredentialProperties); NodeHelpers.mergeNodeProperties(combineProperties, mergeCredentialProperties);
@ -97,7 +95,6 @@ export class CredentialsHelper extends ICredentialsHelper {
return combineProperties; return combineProperties;
} }
/** /**
* Returns the decrypted credential data with applied overwrites * Returns the decrypted credential data with applied overwrites
* *
@ -107,7 +104,13 @@ export class CredentialsHelper extends ICredentialsHelper {
* @returns {ICredentialDataDecryptedObject} * @returns {ICredentialDataDecryptedObject}
* @memberof CredentialsHelper * @memberof CredentialsHelper
*/ */
async getDecrypted(name: string, type: string, mode: WorkflowExecuteMode, raw?: boolean, expressionResolveValues?: ICredentialsExpressionResolveValues): Promise<ICredentialDataDecryptedObject> { async getDecrypted(
name: string,
type: string,
mode: WorkflowExecuteMode,
raw?: boolean,
expressionResolveValues?: ICredentialsExpressionResolveValues,
): Promise<ICredentialDataDecryptedObject> {
const credentials = await this.getCredentials(name, type); const credentials = await this.getCredentials(name, type);
const decryptedDataOriginal = credentials.getData(this.encryptionKey); const decryptedDataOriginal = credentials.getData(this.encryptionKey);
@ -116,10 +119,14 @@ export class CredentialsHelper extends ICredentialsHelper {
return decryptedDataOriginal; return decryptedDataOriginal;
} }
return this.applyDefaultsAndOverwrites(decryptedDataOriginal, type, mode, expressionResolveValues); return this.applyDefaultsAndOverwrites(
decryptedDataOriginal,
type,
mode,
expressionResolveValues,
);
} }
/** /**
* Applies credential default data and overwrites * Applies credential default data and overwrites
* *
@ -128,11 +135,21 @@ export class CredentialsHelper extends ICredentialsHelper {
* @returns {ICredentialDataDecryptedObject} * @returns {ICredentialDataDecryptedObject}
* @memberof CredentialsHelper * @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); const credentialsProperties = this.getCredentialsProperties(type);
// Add the default credential values // 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) { if (decryptedDataOriginal.oauthTokenData !== undefined) {
// The OAuth data gets removed as it is not defined specifically as a parameter // 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) { if (expressionResolveValues) {
try { try {
const workflow = new Workflow({ nodes: Object.values(expressionResolveValues.workflow.nodes), connections: expressionResolveValues.workflow.connectionsBySourceNode, active: false, nodeTypes: expressionResolveValues.workflow.nodeTypes }); const workflow = new Workflow({
decryptedData = workflow.expression.getParameterValue(decryptedData as INodeParameters, expressionResolveValues.runExecutionData, expressionResolveValues.runIndex, expressionResolveValues.itemIndex, expressionResolveValues.node.name, expressionResolveValues.connectionInputData, mode, {}, false, decryptedData) as ICredentialDataDecryptedObject; 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) { } catch (e) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
e.message += ' [Error resolving credentials]'; e.message += ' [Error resolving credentials]';
throw e; throw e;
} }
@ -157,18 +191,30 @@ export class CredentialsHelper extends ICredentialsHelper {
parameters: {} as INodeParameters, parameters: {} as INodeParameters,
} as INode; } 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 // 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 // Load and apply the credentials overwrites if any exist
const credentialsOverwrites = CredentialsOverwrites(); const credentialsOverwrites = CredentialsOverwrites();
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return credentialsOverwrites.applyOverwrite(type, decryptedData); return credentialsOverwrites.applyOverwrite(type, decryptedData);
} }
/** /**
* Updates credentials in the database * Updates credentials in the database
* *
@ -178,10 +224,15 @@ export class CredentialsHelper extends ICredentialsHelper {
* @returns {Promise<void>} * @returns {Promise<void>}
* @memberof CredentialsHelper * @memberof CredentialsHelper
*/ */
async updateCredentials(name: string, type: string, data: ICredentialDataDecryptedObject): Promise<void> { async updateCredentials(
name: string,
type: string,
data: ICredentialDataDecryptedObject,
): Promise<void> {
// eslint-disable-next-line @typescript-eslint/await-thenable
const credentials = await this.getCredentials(name, type); 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 // The first time executeWorkflow gets called the Database has
// to get initialized first // to get initialized first
await Db.init(); await Db.init();
@ -201,7 +252,7 @@ export class CredentialsHelper extends ICredentialsHelper {
type, type,
}; };
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await Db.collections.Credentials!.update(findQuery, newCredentialsData); await Db.collections.Credentials!.update(findQuery, newCredentialsData);
} }
} }

View file

@ -1,20 +1,15 @@
import { /* eslint-disable no-underscore-dangle */
ICredentialDataDecryptedObject, import { ICredentialDataDecryptedObject } from 'n8n-workflow';
} from 'n8n-workflow';
import {
CredentialTypes,
GenericHelpers,
ICredentialsOverwrite,
} from './';
// eslint-disable-next-line import/no-cycle
import { CredentialTypes, GenericHelpers, ICredentialsOverwrite } from '.';
class CredentialsOverwritesClass { class CredentialsOverwritesClass {
private credentialTypes = CredentialTypes(); private credentialTypes = CredentialTypes();
private overwriteData: ICredentialsOverwrite = {};
private resolvedTypes: string[] = [];
private overwriteData: ICredentialsOverwrite = {};
private resolvedTypes: string[] = [];
async init(overwriteData?: ICredentialsOverwrite) { async init(overwriteData?: ICredentialsOverwrite) {
if (overwriteData !== undefined) { if (overwriteData !== undefined) {
@ -24,9 +19,10 @@ class CredentialsOverwritesClass {
return; return;
} }
const data = await GenericHelpers.getConfigValue('credentials.overwrite.data') as string; const data = (await GenericHelpers.getConfigValue('credentials.overwrite.data')) as string;
try { try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-shadow
const overwriteData = JSON.parse(data); const overwriteData = JSON.parse(data);
this.__setData(overwriteData); this.__setData(overwriteData);
} catch (error) { } catch (error) {
@ -34,10 +30,10 @@ class CredentialsOverwritesClass {
} }
} }
__setData(overwriteData: ICredentialsOverwrite) { __setData(overwriteData: ICredentialsOverwrite) {
this.overwriteData = overwriteData; this.overwriteData = overwriteData;
// eslint-disable-next-line no-restricted-syntax
for (const credentialTypeData of this.credentialTypes.getAll()) { for (const credentialTypeData of this.credentialTypes.getAll()) {
const type = credentialTypeData.name; const type = credentialTypeData.name;
@ -49,29 +45,30 @@ class CredentialsOverwritesClass {
} }
} }
applyOverwrite(type: string, data: ICredentialDataDecryptedObject) { applyOverwrite(type: string, data: ICredentialDataDecryptedObject) {
const overwrites = this.get(type); const overwrites = this.get(type);
if (overwrites === undefined) { if (overwrites === undefined) {
return data; return data;
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const returnData = JSON.parse(JSON.stringify(data)); const returnData = JSON.parse(JSON.stringify(data));
// Overwrite only if there is currently no data set // Overwrite only if there is currently no data set
// eslint-disable-next-line no-restricted-syntax
for (const key of Object.keys(overwrites)) { for (const key of Object.keys(overwrites)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if ([null, undefined, ''].includes(returnData[key])) { if ([null, undefined, ''].includes(returnData[key])) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
returnData[key] = overwrites[key]; returnData[key] = overwrites[key];
} }
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return returnData; return returnData;
} }
__getExtended(type: string): ICredentialDataDecryptedObject | undefined { __getExtended(type: string): ICredentialDataDecryptedObject | undefined {
if (this.resolvedTypes.includes(type)) { if (this.resolvedTypes.includes(type)) {
// Type got already resolved and can so returned directly // Type got already resolved and can so returned directly
return this.overwriteData[type]; return this.overwriteData[type];
@ -89,6 +86,7 @@ class CredentialsOverwritesClass {
} }
const overwrites: ICredentialDataDecryptedObject = {}; const overwrites: ICredentialDataDecryptedObject = {};
// eslint-disable-next-line no-restricted-syntax
for (const credentialsTypeName of credentialTypeData.extends) { for (const credentialsTypeName of credentialTypeData.extends) {
Object.assign(overwrites, this.__getExtended(credentialsTypeName)); Object.assign(overwrites, this.__getExtended(credentialsTypeName));
} }
@ -102,20 +100,18 @@ class CredentialsOverwritesClass {
return overwrites; return overwrites;
} }
get(type: string): ICredentialDataDecryptedObject | undefined { get(type: string): ICredentialDataDecryptedObject | undefined {
return this.overwriteData[type]; return this.overwriteData[type];
} }
getAll(): ICredentialsOverwrite { getAll(): ICredentialsOverwrite {
return this.overwriteData; return this.overwriteData;
} }
} }
let credentialsOverwritesInstance: CredentialsOverwritesClass | undefined; let credentialsOverwritesInstance: CredentialsOverwritesClass | undefined;
// eslint-disable-next-line @typescript-eslint/naming-convention
export function CredentialsOverwrites(): CredentialsOverwritesClass { export function CredentialsOverwrites(): CredentialsOverwritesClass {
if (credentialsOverwritesInstance === undefined) { if (credentialsOverwritesInstance === undefined) {
credentialsOverwritesInstance = new CredentialsOverwritesClass(); credentialsOverwritesInstance = new CredentialsOverwritesClass();

View file

@ -1,26 +1,24 @@
import { /* eslint-disable @typescript-eslint/no-unsafe-assignment */
DatabaseType, /* eslint-disable @typescript-eslint/restrict-template-expressions */
GenericHelpers, /* eslint-disable no-case-declarations */
IDatabaseCollections, /* eslint-disable @typescript-eslint/naming-convention */
} from './'; import { UserSettings } from 'n8n-core';
import { ConnectionOptions, createConnection, getRepository } from 'typeorm';
import {
UserSettings,
} from 'n8n-core';
import {
ConnectionOptions,
createConnection,
getRepository,
} from 'typeorm';
import { TlsOptions } from 'tls'; 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'; import * as config from '../config';
// eslint-disable-next-line import/no-cycle
import { entities } from './databases/entities'; 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, Credentials: null,
Execution: null, Execution: null,
Workflow: null, Workflow: null,
@ -28,14 +26,8 @@ export let collections: IDatabaseCollections = {
Tag: null, 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<IDatabaseCollections> { export async function init(): Promise<IDatabaseCollections> {
const dbType = await GenericHelpers.getConfigValue('database.type') as DatabaseType; const dbType = (await GenericHelpers.getConfigValue('database.type')) as DatabaseType;
const n8nFolder = UserSettings.getUserN8nFolderPath(); const n8nFolder = UserSettings.getUserN8nFolderPath();
let connectionOptions: ConnectionOptions; let connectionOptions: ConnectionOptions;
@ -44,13 +36,17 @@ export async function init(): Promise<IDatabaseCollections> {
switch (dbType) { switch (dbType) {
case 'postgresdb': case 'postgresdb':
const sslCa = await GenericHelpers.getConfigValue('database.postgresdb.ssl.ca') as string; const sslCa = (await GenericHelpers.getConfigValue('database.postgresdb.ssl.ca')) as string;
const sslCert = await GenericHelpers.getConfigValue('database.postgresdb.ssl.cert') as string; const sslCert = (await GenericHelpers.getConfigValue(
const sslKey = await GenericHelpers.getConfigValue('database.postgresdb.ssl.key') as string; 'database.postgresdb.ssl.cert',
const sslRejectUnauthorized = await GenericHelpers.getConfigValue('database.postgresdb.ssl.rejectUnauthorized') as boolean; )) 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; let ssl: TlsOptions | undefined;
if (sslCa !== '' || sslCert !== '' || sslKey !== '' || sslRejectUnauthorized !== true) { if (sslCa !== '' || sslCert !== '' || sslKey !== '' || !sslRejectUnauthorized) {
ssl = { ssl = {
ca: sslCa || undefined, ca: sslCa || undefined,
cert: sslCert || undefined, cert: sslCert || undefined,
@ -62,11 +58,11 @@ export async function init(): Promise<IDatabaseCollections> {
connectionOptions = { connectionOptions = {
type: 'postgres', type: 'postgres',
entityPrefix, entityPrefix,
database: await GenericHelpers.getConfigValue('database.postgresdb.database') as string, database: (await GenericHelpers.getConfigValue('database.postgresdb.database')) as string,
host: await GenericHelpers.getConfigValue('database.postgresdb.host') as string, host: (await GenericHelpers.getConfigValue('database.postgresdb.host')) as string,
password: await GenericHelpers.getConfigValue('database.postgresdb.password') as string, password: (await GenericHelpers.getConfigValue('database.postgresdb.password')) as string,
port: await GenericHelpers.getConfigValue('database.postgresdb.port') as number, port: (await GenericHelpers.getConfigValue('database.postgresdb.port')) as number,
username: await GenericHelpers.getConfigValue('database.postgresdb.user') as string, username: (await GenericHelpers.getConfigValue('database.postgresdb.user')) as string,
schema: config.get('database.postgresdb.schema'), schema: config.get('database.postgresdb.schema'),
migrations: postgresMigrations, migrations: postgresMigrations,
migrationsRun: true, migrationsRun: true,
@ -80,12 +76,12 @@ export async function init(): Promise<IDatabaseCollections> {
case 'mysqldb': case 'mysqldb':
connectionOptions = { connectionOptions = {
type: dbType === 'mysqldb' ? 'mysql' : 'mariadb', type: dbType === 'mysqldb' ? 'mysql' : 'mariadb',
database: await GenericHelpers.getConfigValue('database.mysqldb.database') as string, database: (await GenericHelpers.getConfigValue('database.mysqldb.database')) as string,
entityPrefix, entityPrefix,
host: await GenericHelpers.getConfigValue('database.mysqldb.host') as string, host: (await GenericHelpers.getConfigValue('database.mysqldb.host')) as string,
password: await GenericHelpers.getConfigValue('database.mysqldb.password') as string, password: (await GenericHelpers.getConfigValue('database.mysqldb.password')) as string,
port: await GenericHelpers.getConfigValue('database.mysqldb.port') as number, port: (await GenericHelpers.getConfigValue('database.mysqldb.port')) as number,
username: await GenericHelpers.getConfigValue('database.mysqldb.user') as string, username: (await GenericHelpers.getConfigValue('database.mysqldb.user')) as string,
migrations: mysqlMigrations, migrations: mysqlMigrations,
migrationsRun: true, migrationsRun: true,
migrationsTableName: `${entityPrefix}migrations`, migrationsTableName: `${entityPrefix}migrations`,
@ -122,8 +118,10 @@ export async function init(): Promise<IDatabaseCollections> {
// n8n knows it has changed. Happens only on sqlite. // n8n knows it has changed. Happens only on sqlite.
let migrations = []; let migrations = [];
try { try {
migrations = await connection.query(`SELECT id FROM ${entityPrefix}migrations where name = "MakeStoppedAtNullable1607431743769"`); migrations = await connection.query(
} catch(error) { `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. // 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<IDatabaseCollections> {
transaction: 'none', transaction: 'none',
}); });
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (migrations.length === 0) { if (migrations.length === 0) {
await connection.close(); await connection.close();
connection = await createConnection(connectionOptions); connection = await createConnection(connectionOptions);

View file

@ -1,23 +1,20 @@
import { /* eslint-disable @typescript-eslint/no-var-requires */
Db, /* eslint-disable import/no-dynamic-require */
IExternalHooksClass, /* eslint-disable no-restricted-syntax */
IExternalHooksFileData, // eslint-disable-next-line import/no-cycle
IExternalHooksFunctions, import { Db, IExternalHooksClass, IExternalHooksFileData, IExternalHooksFunctions } from '.';
} from './';
import * as config from '../config'; import * as config from '../config';
class ExternalHooksClass implements IExternalHooksClass { class ExternalHooksClass implements IExternalHooksClass {
externalHooks: { externalHooks: {
[key: string]: Array<() => {}> [key: string]: Array<() => {}>;
} = {}; } = {};
initDidRun = false; initDidRun = false;
async init(): Promise<void> { async init(): Promise<void> {
if (this.initDidRun === true) { if (this.initDidRun) {
return; return;
} }
@ -26,7 +23,6 @@ class ExternalHooksClass implements IExternalHooksClass {
this.initDidRun = true; this.initDidRun = true;
} }
async reload(externalHooks?: IExternalHooksFileData) { async reload(externalHooks?: IExternalHooksFileData) {
this.externalHooks = {}; this.externalHooks = {};
@ -37,7 +33,6 @@ class ExternalHooksClass implements IExternalHooksClass {
} }
} }
async loadHooksFiles(reload = false) { async loadHooksFiles(reload = false) {
const externalHookFiles = config.get('externalHookFiles').split(':'); const externalHookFiles = config.get('externalHookFiles').split(':');
@ -46,21 +41,22 @@ class ExternalHooksClass implements IExternalHooksClass {
hookFilePath = hookFilePath.trim(); hookFilePath = hookFilePath.trim();
if (hookFilePath !== '') { if (hookFilePath !== '') {
try { try {
if (reload) {
if (reload === true) {
delete require.cache[require.resolve(hookFilePath)]; 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; const hookFile = require(hookFilePath) as IExternalHooksFileData;
this.loadHooks(hookFile); this.loadHooks(hookFile);
} catch (error) { } 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}`); throw new Error(`Problem loading external hook file "${hookFilePath}": ${error.message}`);
} }
} }
} }
} }
loadHooks(hookFileData: IExternalHooksFileData) { loadHooks(hookFileData: IExternalHooksFileData) {
for (const resource of Object.keys(hookFileData)) { for (const resource of Object.keys(hookFileData)) {
for (const operation of Object.keys(hookFileData[resource])) { for (const operation of Object.keys(hookFileData[resource])) {
@ -71,13 +67,17 @@ class ExternalHooksClass implements IExternalHooksClass {
this.externalHooks[hookString] = []; 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],
);
} }
} }
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async run(hookName: string, hookParameters?: any[]): Promise<void> { // tslint:disable-line:no-any async run(hookName: string, hookParameters?: any[]): Promise<void> {
const externalHookFunctions: IExternalHooksFunctions = { const externalHookFunctions: IExternalHooksFunctions = {
dbCollections: Db.collections, dbCollections: Db.collections,
}; };
@ -86,22 +86,20 @@ class ExternalHooksClass implements IExternalHooksClass {
return; 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); await externalHookFunction.apply(externalHookFunctions, hookParameters);
} }
} }
exists(hookName: string): boolean { exists(hookName: string): boolean {
return !!this.externalHooks[hookName]; return !!this.externalHooks[hookName];
} }
} }
let externalHooksInstance: ExternalHooksClass | undefined; let externalHooksInstance: ExternalHooksClass | undefined;
// eslint-disable-next-line @typescript-eslint/naming-convention
export function ExternalHooks(): ExternalHooksClass { export function ExternalHooks(): ExternalHooksClass {
if (externalHooksInstance === undefined) { if (externalHooksInstance === undefined) {
externalHooksInstance = new ExternalHooksClass(); externalHooksInstance = new ExternalHooksClass();

View file

@ -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 * as express from 'express';
import { join as pathJoin } from 'path'; import { join as pathJoin } from 'path';
import { readFile as fsReadFile } from 'fs/promises'; import { readFile as fsReadFile } from 'fs/promises';
import { readFileSync as fsReadFileSync } from 'fs'; import { readFileSync as fsReadFileSync } from 'fs';
import { IDataObject } from 'n8n-workflow'; 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; let versionCache: IPackageVersions | undefined;
@ -16,18 +22,17 @@ let versionCache: IPackageVersions | undefined;
* @returns {string} * @returns {string}
*/ */
export function getBaseUrl(): string { export function getBaseUrl(): string {
const protocol = config.get('protocol') as string; const protocol = config.get('protocol');
const host = config.get('host') as string; const host = config.get('host');
const port = config.get('port') as number; const port = config.get('port');
const path = config.get('path') as string; 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}${path}`;
} }
return `${protocol}://${host}:${port}${path}`; return `${protocol}://${host}:${port}${path}`;
} }
/** /**
* Returns the session id if one is set * 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; return req.headers.sessionid as string | undefined;
} }
/** /**
* Returns information which version of the packages are installed * Returns information which version of the packages are installed
* *
@ -51,10 +55,12 @@ export async function getVersions(): Promise<IPackageVersions> {
return versionCache; 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); const packageData = JSON.parse(packageFile);
versionCache = { versionCache = {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
cli: packageData.version, cli: packageData.version,
}; };
@ -71,9 +77,11 @@ export async function getVersions(): Promise<IPackageVersions> {
function extractSchemaForKey(configKey: string, configSchema: IDataObject): IDataObject { function extractSchemaForKey(configKey: string, configSchema: IDataObject): IDataObject {
const configKeyParts = configKey.split('.'); const configKeyParts = configKey.split('.');
// eslint-disable-next-line no-restricted-syntax
for (const key of configKeyParts) { for (const key of configKeyParts) {
if (configSchema[key] === undefined) { if (configSchema[key] === undefined) {
throw new Error(`Key "${key}" of ConfigKey "${configKey}" does not exist`); 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) { } else if ((configSchema[key]! as IDataObject)._cvtProperties === undefined) {
configSchema = configSchema[key] as IDataObject; configSchema = configSchema[key] as IDataObject;
} else { } else {
@ -90,7 +98,9 @@ function extractSchemaForKey(configKey: string, configSchema: IDataObject): IDat
* @param {string} configKey The key of the config data to get * @param {string} configKey The key of the config data to get
* @returns {(Promise<string | boolean | number | undefined>)} * @returns {(Promise<string | boolean | number | undefined>)}
*/ */
export async function getConfigValue(configKey: string): Promise<string | boolean | number | undefined> { export async function getConfigValue(
configKey: string,
): Promise<string | boolean | number | undefined> {
// Get the environment variable // Get the environment variable
const configSchema = config.getSchema(); const configSchema = config.getSchema();
// @ts-ignore // @ts-ignore
@ -102,7 +112,7 @@ export async function getConfigValue(configKey: string): Promise<string | boolea
} }
// Check if special file enviroment variable exists // Check if special file enviroment variable exists
const fileEnvironmentVariable = process.env[currentSchema.env + '_FILE']; const fileEnvironmentVariable = process.env[`${currentSchema.env}_FILE`];
if (fileEnvironmentVariable === undefined) { if (fileEnvironmentVariable === undefined) {
// Does not exist, so return value from config // Does not exist, so return value from config
return config.get(configKey); return config.get(configKey);
@ -110,7 +120,7 @@ export async function getConfigValue(configKey: string): Promise<string | boolea
let data; let data;
try { try {
data = await fsReadFile(fileEnvironmentVariable, 'utf8') as string; data = await fsReadFile(fileEnvironmentVariable, 'utf8');
} catch (error) { } catch (error) {
if (error.code === 'ENOENT') { if (error.code === 'ENOENT') {
throw new Error(`The file "${fileEnvironmentVariable}" could not be found.`); throw new Error(`The file "${fileEnvironmentVariable}" could not be found.`);
@ -141,7 +151,7 @@ export function getConfigValueSync(configKey: string): string | boolean | number
} }
// Check if special file enviroment variable exists // Check if special file enviroment variable exists
const fileEnvironmentVariable = process.env[currentSchema.env + '_FILE']; const fileEnvironmentVariable = process.env[`${currentSchema.env}_FILE`];
if (fileEnvironmentVariable === undefined) { if (fileEnvironmentVariable === undefined) {
// Does not exist, so return value from config // Does not exist, so return value from config
return config.get(configKey); return config.get(configKey);
@ -149,7 +159,7 @@ export function getConfigValueSync(configKey: string): string | boolean | number
let data; let data;
try { try {
data = fsReadFileSync(fileEnvironmentVariable, 'utf8') as string; data = fsReadFileSync(fileEnvironmentVariable, 'utf8');
} catch (error) { } catch (error) {
if (error.code === 'ENOENT') { if (error.code === 'ENOENT') {
throw new Error(`The file "${fileEnvironmentVariable}" could not be found.`); throw new Error(`The file "${fileEnvironmentVariable}" could not be found.`);

View file

@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable import/no-cycle */
import { import {
ExecutionError, ExecutionError,
ICredentialDataDecryptedObject, ICredentialDataDecryptedObject,
@ -10,15 +12,15 @@ import {
IRunExecutionData, IRunExecutionData,
ITaskData, ITaskData,
IWorkflowBase as IWorkflowBaseWorkflow, IWorkflowBase as IWorkflowBaseWorkflow,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
IWorkflowCredentials, IWorkflowCredentials,
Workflow, Workflow,
WorkflowExecuteMode, WorkflowExecuteMode,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import { IDeferredPromise, WorkflowExecute } from 'n8n-core';
IDeferredPromise, WorkflowExecute,
} from 'n8n-core';
// eslint-disable-next-line import/no-extraneous-dependencies
import * as PCancelable from 'p-cancelable'; import * as PCancelable from 'p-cancelable';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
@ -85,7 +87,7 @@ export interface ITagDb {
} }
export type UsageCount = { export type UsageCount = {
usageCount: number usageCount: number;
}; };
export type ITagWithCountDb = ITagDb & UsageCount; export type ITagWithCountDb = ITagDb & UsageCount;
@ -214,7 +216,6 @@ export interface IExecutionsSummary {
workflowName?: string; workflowName?: string;
} }
export interface IExecutionsCurrentSummary { export interface IExecutionsCurrentSummary {
id: string; id: string;
retryOf?: string; retryOf?: string;
@ -223,7 +224,6 @@ export interface IExecutionsCurrentSummary {
workflowId: string; workflowId: string;
} }
export interface IExecutionDeleteFilter { export interface IExecutionDeleteFilter {
deleteBefore?: Date; deleteBefore?: Date;
filters?: IDataObject; filters?: IDataObject;
@ -240,22 +240,33 @@ export interface IExecutingWorkflowData {
export interface IExternalHooks { export interface IExternalHooks {
credentials?: { credentials?: {
create?: Array<{ (this: IExternalHooksFunctions, credentialsData: ICredentialsEncrypted): Promise<void>; }> create?: Array<{
delete?: Array<{ (this: IExternalHooksFunctions, credentialId: string): Promise<void>; }> (this: IExternalHooksFunctions, credentialsData: ICredentialsEncrypted): Promise<void>;
update?: Array<{ (this: IExternalHooksFunctions, credentialsData: ICredentialsDb): Promise<void>; }> }>;
delete?: Array<{ (this: IExternalHooksFunctions, credentialId: string): Promise<void> }>;
update?: Array<{
(this: IExternalHooksFunctions, credentialsData: ICredentialsDb): Promise<void>;
}>;
}; };
workflow?: { workflow?: {
activate?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb): Promise<void>; }> activate?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb): Promise<void> }>;
create?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowBase): Promise<void>; }> create?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowBase): Promise<void> }>;
delete?: Array<{ (this: IExternalHooksFunctions, workflowId: string): Promise<void>; }> delete?: Array<{ (this: IExternalHooksFunctions, workflowId: string): Promise<void> }>;
execute?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb, mode: WorkflowExecuteMode): Promise<void>; }> execute?: Array<{
update?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb): Promise<void>; }> (
this: IExternalHooksFunctions,
workflowData: IWorkflowDb,
mode: WorkflowExecuteMode,
): Promise<void>;
}>;
update?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb): Promise<void> }>;
}; };
} }
export interface IExternalHooksFileData { export interface IExternalHooksFileData {
[key: string]: { [key: string]: {
[key: string]: Array<(...args: any[]) => Promise<void>>; //tslint:disable-line:no-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: Array<(...args: any[]) => Promise<void>>;
}; };
} }
@ -265,7 +276,8 @@ export interface IExternalHooksFunctions {
export interface IExternalHooksClass { export interface IExternalHooksClass {
init(): Promise<void>; init(): Promise<void>;
run(hookName: string, hookParameters?: any[]): Promise<void>; // tslint:disable-line:no-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
run(hookName: string, hookParameters?: any[]): Promise<void>;
} }
export interface IN8nConfig { export interface IN8nConfig {
@ -295,12 +307,14 @@ export interface IN8nConfigEndpoints {
webhookTest: string; webhookTest: string;
} }
// eslint-disable-next-line import/export
export interface IN8nConfigExecutions { export interface IN8nConfigExecutions {
saveDataOnError: SaveExecutionDataType; saveDataOnError: SaveExecutionDataType;
saveDataOnSuccess: SaveExecutionDataType; saveDataOnSuccess: SaveExecutionDataType;
saveDataManualExecutions: boolean; saveDataManualExecutions: boolean;
} }
// eslint-disable-next-line import/export
export interface IN8nConfigExecutions { export interface IN8nConfigExecutions {
saveDataOnError: SaveExecutionDataType; saveDataOnError: SaveExecutionDataType;
saveDataOnSuccess: SaveExecutionDataType; saveDataOnSuccess: SaveExecutionDataType;
@ -409,13 +423,11 @@ export interface IPushDataNodeExecuteAfter {
nodeName: string; nodeName: string;
} }
export interface IPushDataNodeExecuteBefore { export interface IPushDataNodeExecuteBefore {
executionId: string; executionId: string;
nodeName: string; nodeName: string;
} }
export interface IPushDataTestWebhook { export interface IPushDataTestWebhook {
executionId: string; executionId: string;
workflowId: string; workflowId: string;
@ -432,7 +444,6 @@ export interface IResponseCallbackData {
responseCode?: number; responseCode?: number;
} }
export interface ITransferNodeTypes { export interface ITransferNodeTypes {
[key: string]: { [key: string]: {
className: string; className: string;
@ -440,7 +451,6 @@ export interface ITransferNodeTypes {
}; };
} }
export interface IWorkflowErrorData { export interface IWorkflowErrorData {
[key: string]: IDataObject | string | number | ExecutionError; [key: string]: IDataObject | string | number | ExecutionError;
execution: { execution: {
@ -457,7 +467,8 @@ export interface IWorkflowErrorData {
export interface IProcessMessageDataHook { export interface IProcessMessageDataHook {
hook: string; hook: string;
parameters: any[]; // tslint:disable-line:no-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
parameters: any[];
} }
export interface IWorkflowExecutionDataProcess { export interface IWorkflowExecutionDataProcess {
@ -471,7 +482,6 @@ export interface IWorkflowExecutionDataProcess {
workflowData: IWorkflowBase; workflowData: IWorkflowBase;
} }
export interface IWorkflowExecutionDataProcessWithExecution extends IWorkflowExecutionDataProcess { export interface IWorkflowExecutionDataProcessWithExecution extends IWorkflowExecutionDataProcess {
credentialsOverwrite: ICredentialsOverwrite; credentialsOverwrite: ICredentialsOverwrite;
credentialsTypeData: ICredentialsTypeData; credentialsTypeData: ICredentialsTypeData;

View file

@ -1,7 +1,14 @@
import { /* eslint-disable @typescript-eslint/naming-convention */
CUSTOM_EXTENSION_ENV, /* eslint-disable no-prototype-builtins */
UserSettings, /* eslint-disable no-param-reassign */
} from 'n8n-core'; /* 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 { import {
CodexData, CodexData,
ICredentialType, ICredentialType,
@ -11,12 +18,6 @@ import {
LoggerProxy, LoggerProxy,
} from 'n8n-workflow'; } from 'n8n-workflow';
import * as config from '../config';
import {
getLogger,
} from '../src/Logger';
import { import {
access as fsAccess, access as fsAccess,
readdir as fsReaddir, readdir as fsReaddir,
@ -25,18 +26,20 @@ import {
} from 'fs/promises'; } from 'fs/promises';
import * as glob from 'fast-glob'; import * as glob from 'fast-glob';
import * as path from 'path'; import * as path from 'path';
import { getLogger } from './Logger';
import * as config from '../config';
const CUSTOM_NODES_CATEGORY = 'Custom Nodes'; const CUSTOM_NODES_CATEGORY = 'Custom Nodes';
class LoadNodesAndCredentialsClass { class LoadNodesAndCredentialsClass {
nodeTypes: INodeTypeData = {}; nodeTypes: INodeTypeData = {};
credentialTypes: { credentialTypes: {
[key: string]: ICredentialType [key: string]: ICredentialType;
} = {}; } = {};
excludeNodes: string[] | undefined = undefined; excludeNodes: string[] | undefined = undefined;
includeNodes: string[] | undefined = undefined; includeNodes: string[] | undefined = undefined;
nodeModulesPath = ''; nodeModulesPath = '';
@ -64,6 +67,7 @@ class LoadNodesAndCredentialsClass {
break; break;
} catch (error) { } catch (error) {
// Folder does not exist so get next one // Folder does not exist so get next one
// eslint-disable-next-line no-continue
continue; continue;
} }
} }
@ -90,7 +94,9 @@ class LoadNodesAndCredentialsClass {
// Add folders from special environment variable // Add folders from special environment variable
if (process.env[CUSTOM_EXTENSION_ENV] !== undefined) { 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(';'); const customExtensionFolders = process.env[CUSTOM_EXTENSION_ENV]!.split(';');
// eslint-disable-next-line prefer-spread
customDirectories.push.apply(customDirectories, customExtensionFolders); customDirectories.push.apply(customDirectories, customExtensionFolders);
} }
@ -99,7 +105,6 @@ class LoadNodesAndCredentialsClass {
} }
} }
/** /**
* Returns all the names of the packages which could * Returns all the names of the packages which could
* contain n8n nodes * contain n8n nodes
@ -120,9 +125,11 @@ class LoadNodesAndCredentialsClass {
if (!(await fsStat(nodeModulesPath)).isDirectory()) { if (!(await fsStat(nodeModulesPath)).isDirectory()) {
continue; continue;
} }
if (isN8nNodesPackage) { results.push(`${relativePath}${file}`); } if (isN8nNodesPackage) {
results.push(`${relativePath}${file}`);
}
if (isNpmScopedPackage) { if (isNpmScopedPackage) {
results.push(...await getN8nNodePackagesRecursive(`${relativePath}${file}/`)); results.push(...(await getN8nNodePackagesRecursive(`${relativePath}${file}/`)));
} }
} }
return results; return results;
@ -138,6 +145,7 @@ class LoadNodesAndCredentialsClass {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async loadCredentialsFromFile(credentialName: string, filePath: string): Promise<void> { async loadCredentialsFromFile(credentialName: string, filePath: string): Promise<void> {
// eslint-disable-next-line import/no-dynamic-require, global-require, @typescript-eslint/no-var-requires
const tempModule = require(filePath); const tempModule = require(filePath);
let tempCredential: ICredentialType; let tempCredential: ICredentialType;
@ -145,7 +153,9 @@ class LoadNodesAndCredentialsClass {
tempCredential = new tempModule[credentialName]() as ICredentialType; tempCredential = new tempModule[credentialName]() as ICredentialType;
} catch (e) { } catch (e) {
if (e instanceof TypeError) { 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 { } else {
throw e; throw e;
} }
@ -154,7 +164,6 @@ class LoadNodesAndCredentialsClass {
this.credentialTypes[tempCredential.name] = tempCredential; this.credentialTypes[tempCredential.name] = tempCredential;
} }
/** /**
* Loads a node from a file * Loads a node from a file
* *
@ -167,26 +176,34 @@ class LoadNodesAndCredentialsClass {
let tempNode: INodeType; let tempNode: INodeType;
let fullNodeName: string; let fullNodeName: string;
// eslint-disable-next-line import/no-dynamic-require, global-require, @typescript-eslint/no-var-requires
const tempModule = require(filePath); const tempModule = require(filePath);
try { try {
tempNode = new tempModule[nodeName]() as INodeType; tempNode = new tempModule[nodeName]() as INodeType;
this.addCodex({ node: tempNode, filePath, isCustom: packageName === 'CUSTOM' }); this.addCodex({ node: tempNode, filePath, isCustom: packageName === 'CUSTOM' });
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console
console.error(`Error loading node "${nodeName}" from: "${filePath}"`); console.error(`Error loading node "${nodeName}" from: "${filePath}"`);
throw error; throw error;
} }
fullNodeName = packageName + '.' + tempNode.description.name; // eslint-disable-next-line prefer-const
fullNodeName = `${packageName}.${tempNode.description.name}`;
tempNode.description.name = fullNodeName; tempNode.description.name = fullNodeName;
if (tempNode.description.icon !== undefined && if (tempNode.description.icon !== undefined && tempNode.description.icon.startsWith('file:')) {
tempNode.description.icon.startsWith('file:')) {
// If a file icon gets used add the full path // 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) { 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)) { if (this.includeNodes !== undefined && !this.includeNodes.includes(fullNodeName)) {
@ -212,7 +229,9 @@ class LoadNodesAndCredentialsClass {
* @returns {CodexData} * @returns {CodexData}
*/ */
getCodex(filePath: string): 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 const { categories, subcategories, alias } = require(`${filePath}on`); // .js to .json
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return { return {
...(categories && { categories }), ...(categories && { categories }),
...(subcategories && { subcategories }), ...(subcategories && { subcategories }),
@ -230,11 +249,7 @@ class LoadNodesAndCredentialsClass {
* @param obj.isCustom Whether the node is custom * @param obj.isCustom Whether the node is custom
* @returns {void} * @returns {void}
*/ */
addCodex({ node, filePath, isCustom }: { addCodex({ node, filePath, isCustom }: { node: INodeType; filePath: string; isCustom: boolean }) {
node: INodeType;
filePath: string;
isCustom: boolean;
}) {
try { try {
const codex = this.getCodex(filePath); const codex = this.getCodex(filePath);
@ -246,6 +261,7 @@ class LoadNodesAndCredentialsClass {
node.description.codex = codex; node.description.codex = codex;
} catch (_) { } catch (_) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
this.logger.debug(`No codex available for: ${filePath.split('/').pop()}`); this.logger.debug(`No codex available for: ${filePath.split('/').pop()}`);
if (isCustom) { if (isCustom) {
@ -264,7 +280,7 @@ class LoadNodesAndCredentialsClass {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async loadDataFromDirectory(setPackageName: string, directory: string): Promise<void> { async loadDataFromDirectory(setPackageName: string, directory: string): Promise<void> {
const files = await glob(path.join(directory, '**/*\.@(node|credentials)\.js')); const files = await glob(path.join(directory, '**/*.@(node|credentials).js'));
let fileName: string; let fileName: string;
let type: string; let type: string;
@ -283,7 +299,6 @@ class LoadNodesAndCredentialsClass {
await Promise.all(loadPromises); await Promise.all(loadPromises);
} }
/** /**
* Loads nodes and credentials from the package with the given name * Loads nodes and credentials from the package with the given name
* *
@ -301,10 +316,12 @@ class LoadNodesAndCredentialsClass {
return; return;
} }
let tempPath: string, filePath: string; let tempPath: string;
let filePath: string;
// Read all node types // 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)) { if (packageFile.n8n.hasOwnProperty('nodes') && Array.isArray(packageFile.n8n.nodes)) {
for (filePath of packageFile.n8n.nodes) { for (filePath of packageFile.n8n.nodes) {
tempPath = path.join(packagePath, filePath); tempPath = path.join(packagePath, filePath);
@ -314,18 +331,21 @@ class LoadNodesAndCredentialsClass {
} }
// Read all credential types // 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) { for (filePath of packageFile.n8n.credentials) {
tempPath = path.join(packagePath, filePath); tempPath = path.join(packagePath, filePath);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
[fileName, type] = path.parse(filePath).name.split('.'); [fileName, type] = path.parse(filePath).name.split('.');
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.loadCredentialsFromFile(fileName, tempPath); this.loadCredentialsFromFile(fileName, tempPath);
} }
} }
} }
} }
let packagesInformationInstance: LoadNodesAndCredentialsClass | undefined; let packagesInformationInstance: LoadNodesAndCredentialsClass | undefined;
export function LoadNodesAndCredentials(): LoadNodesAndCredentialsClass { export function LoadNodesAndCredentials(): LoadNodesAndCredentialsClass {

View file

@ -1,23 +1,23 @@
import config = require('../config'); /* eslint-disable @typescript-eslint/no-unsafe-assignment */
import * as winston from 'winston'; import * as winston from 'winston';
import { import { IDataObject, ILogger, LogTypes } from 'n8n-workflow';
IDataObject,
ILogger,
LogTypes,
} from 'n8n-workflow';
import * as callsites from 'callsites'; import * as callsites from 'callsites';
import { basename } from 'path'; import { basename } from 'path';
import config = require('../config');
class Logger implements ILogger { class Logger implements ILogger {
private logger: winston.Logger; private logger: winston.Logger;
constructor() { constructor() {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const level = config.get('logs.level'); 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({ this.logger = winston.createLogger({
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
level, level,
}); });
@ -28,18 +28,22 @@ class Logger implements ILogger {
winston.format.metadata(), winston.format.metadata(),
winston.format.timestamp(), winston.format.timestamp(),
winston.format.colorize({ all: true }), winston.format.colorize({ all: true }),
// eslint-disable-next-line @typescript-eslint/no-shadow
winston.format.printf(({ level, message, timestamp, metadata }) => { winston.format.printf(({ level, message, timestamp, metadata }) => {
return `${timestamp} | ${level.padEnd(18)} | ${message}` + (Object.keys(metadata).length ? ` ${JSON.stringify(metadata)}` : ''); // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
}) as winston.Logform.Format return `${timestamp} | ${level.padEnd(18)} | ${message}${
Object.keys(metadata).length ? ` ${JSON.stringify(metadata)}` : ''
}`;
}),
); );
} else { } else {
format = winston.format.printf(({ message }) => message) as winston.Logform.Format; format = winston.format.printf(({ message }) => message);
} }
this.logger.add( this.logger.add(
new winston.transports.Console({ new winston.transports.Console({
format, format,
}) }),
); );
} }
@ -47,15 +51,15 @@ class Logger implements ILogger {
const fileLogFormat = winston.format.combine( const fileLogFormat = winston.format.combine(
winston.format.timestamp(), winston.format.timestamp(),
winston.format.metadata(), winston.format.metadata(),
winston.format.json() winston.format.json(),
); );
this.logger.add( this.logger.add(
new winston.transports.File({ new winston.transports.File({
filename: config.get('logs.file.location'), filename: config.get('logs.file.location'),
format: fileLogFormat, 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'), 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 // We are in runtime, so it means we are looking at compiled js files
const logDetails = {} as IDataObject; const logDetails = {} as IDataObject;
if (callsite[2] !== undefined) { if (callsite[2] !== undefined) {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
logDetails.file = basename(callsite[2].getFileName() || ''); logDetails.file = basename(callsite[2].getFileName() || '');
const functionName = callsite[2].getFunctionName(); const functionName = callsite[2].getFunctionName();
if (functionName) { if (functionName) {
logDetails.function = functionName; logDetails.function = functionName;
} }
} }
this.logger.log(type, message, {...meta, ...logDetails}); this.logger.log(type, message, { ...meta, ...logDetails });
} }
// Convenience methods below // Convenience methods below
@ -100,11 +105,11 @@ class Logger implements ILogger {
warn(message: string, meta: object = {}) { warn(message: string, meta: object = {}) {
this.log('warn', message, meta); this.log('warn', message, meta);
} }
} }
let activeLoggerInstance: Logger | undefined; let activeLoggerInstance: Logger | undefined;
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function getLogger() { export function getLogger() {
if (activeLoggerInstance === undefined) { if (activeLoggerInstance === undefined) {
activeLoggerInstance = new Logger(); activeLoggerInstance = new Logger();

View file

@ -1,24 +1,21 @@
import { import { INodeType, INodeTypeData, INodeTypes, NodeHelpers } from 'n8n-workflow';
INodeType,
INodeTypeData,
INodeTypes,
NodeHelpers,
} from 'n8n-workflow';
class NodeTypesClass implements INodeTypes { class NodeTypesClass implements INodeTypes {
nodeTypes: INodeTypeData = {}; nodeTypes: INodeTypeData = {};
async init(nodeTypes: INodeTypeData): Promise<void> { async init(nodeTypes: INodeTypeData): Promise<void> {
// Some nodeTypes need to get special parameters applied like the // Some nodeTypes need to get special parameters applied like the
// polling nodes the polling times // polling nodes the polling times
// eslint-disable-next-line no-restricted-syntax
for (const nodeTypeData of Object.values(nodeTypes)) { for (const nodeTypeData of Object.values(nodeTypes)) {
const applyParameters = NodeHelpers.getSpecialNodeParameters(nodeTypeData.type); const applyParameters = NodeHelpers.getSpecialNodeParameters(nodeTypeData.type);
if (applyParameters.length) { 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; this.nodeTypes = nodeTypes;
@ -36,10 +33,9 @@ class NodeTypesClass implements INodeTypes {
} }
} }
let nodeTypesInstance: NodeTypesClass | undefined; let nodeTypesInstance: NodeTypesClass | undefined;
// eslint-disable-next-line @typescript-eslint/naming-convention
export function NodeTypes(): NodeTypesClass { export function NodeTypes(): NodeTypesClass {
if (nodeTypesInstance === undefined) { if (nodeTypesInstance === undefined) {
nodeTypesInstance = new NodeTypesClass(); nodeTypesInstance = new NodeTypesClass();

View file

@ -1,24 +1,22 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
// @ts-ignore // @ts-ignore
import * as sseChannel from 'sse-channel'; import * as sseChannel from 'sse-channel';
import * as express from 'express'; import * as express from 'express';
import { import { LoggerProxy as Logger } from 'n8n-workflow';
IPushData, // eslint-disable-next-line import/no-cycle
IPushDataType, import { IPushData, IPushDataType } from '.';
} from '.';
import {
LoggerProxy as Logger,
} from 'n8n-workflow';
export class Push { export class Push {
private channel: sseChannel; private channel: sseChannel;
private connections: { private connections: {
[key: string]: express.Response; [key: string]: express.Response;
} = {}; } = {};
constructor() { constructor() {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, new-cap
this.channel = new sseChannel({ this.channel = new sseChannel({
cors: { cors: {
// Allow access also from frontend when developing // 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) => { this.channel.on('disconnect', (channel: string, res: express.Response) => {
if (res.req !== undefined) { if (res.req !== undefined) {
Logger.debug(`Remove editor-UI session`, { sessionId: res.req.query.sessionId }); Logger.debug(`Remove editor-UI session`, { sessionId: res.req.query.sessionId });
@ -34,7 +33,6 @@ export class Push {
}); });
} }
/** /**
* Adds a new push connection * Adds a new push connection
* *
@ -43,6 +41,7 @@ export class Push {
* @param {express.Response} res The response * @param {express.Response} res The response
* @memberof Push * @memberof Push
*/ */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
add(sessionId: string, req: express.Request, res: express.Response) { add(sessionId: string, req: express.Request, res: express.Response) {
Logger.debug(`Add editor-UI session`, { sessionId }); Logger.debug(`Add editor-UI session`, { sessionId });
@ -57,7 +56,6 @@ export class Push {
this.channel.addClient(req, res); this.channel.addClient(req, res);
} }
/** /**
* Sends data to the client which is connected via a specific session * Sends data to the client which is connected via a specific session
* *
@ -67,9 +65,8 @@ export class Push {
* @memberof Push * @memberof Push
*/ */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
send(type: IPushDataType, data: any, sessionId?: string) {
send(type: IPushDataType, data: any, sessionId?: string) { // tslint:disable-line:no-any
if (sessionId !== undefined && this.connections[sessionId] === undefined) { if (sessionId !== undefined && this.connections[sessionId] === undefined) {
Logger.error(`The session "${sessionId}" is not registred.`, { sessionId }); Logger.error(`The session "${sessionId}" is not registred.`, { sessionId });
return; return;
@ -79,6 +76,7 @@ export class Push {
const sendData: IPushData = { const sendData: IPushData = {
type, type,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
data, data,
}; };
@ -89,7 +87,6 @@ export class Push {
// Send only to a specific client // Send only to a specific client
this.channel.send(JSON.stringify(sendData), [this.connections[sessionId]]); this.channel.send(JSON.stringify(sendData), [this.connections[sessionId]]);
} }
} }
} }

View file

@ -1,5 +1,6 @@
import * as Bull from 'bull'; import * as Bull from 'bull';
import * as config from '../config'; import * as config from '../config';
// eslint-disable-next-line import/no-cycle
import { IBullJobData } from './Interfaces'; import { IBullJobData } from './Interfaces';
export class Queue { export class Queue {
@ -18,15 +19,15 @@ export class Queue {
} }
async add(jobData: IBullJobData, jobOptions: object): Promise<Bull.Job> { async add(jobData: IBullJobData, jobOptions: object): Promise<Bull.Job> {
return await this.jobQueue.add(jobData,jobOptions); return this.jobQueue.add(jobData, jobOptions);
} }
async getJob(jobId: Bull.JobId): Promise<Bull.Job | null> { async getJob(jobId: Bull.JobId): Promise<Bull.Job | null> {
return await this.jobQueue.getJob(jobId); return this.jobQueue.getJob(jobId);
} }
async getJobs(jobTypes: Bull.JobStatus[]): Promise<Bull.Job[]> { async getJobs(jobTypes: Bull.JobStatus[]): Promise<Bull.Job[]> {
return await this.jobQueue.getJobs(jobTypes); return this.jobQueue.getJobs(jobTypes);
} }
getBullObjectInstance(): Bull.Queue { getBullObjectInstance(): Bull.Queue {
@ -43,7 +44,7 @@ export class Queue {
// Job is already running so tell it to stop // Job is already running so tell it to stop
await job.progress(-1); await job.progress(-1);
return true; return true;
} else { }
// Job did not get started yet so remove from queue // Job did not get started yet so remove from queue
try { try {
await job.remove(); await job.remove();
@ -51,7 +52,7 @@ export class Queue {
} catch (e) { } catch (e) {
await job.progress(-1); await job.progress(-1);
} }
}
return false; return false;
} }
} }

View file

@ -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 { Request, Response } from 'express';
import { parse, stringify } from 'flatted'; import { parse, stringify } from 'flatted';
// eslint-disable-next-line import/no-cycle
import { import {
IExecutionDb, IExecutionDb,
IExecutionFlatted, IExecutionFlatted,
IExecutionFlattedDb, IExecutionFlattedDb,
IExecutionResponse, IExecutionResponse,
IWorkflowDb, IWorkflowDb,
} from './'; } from '.';
/** /**
* Special Error which allows to return also an error code and http status code * Special Error which allows to return also an error code and http status code
@ -17,7 +23,6 @@ import {
* @extends {Error} * @extends {Error}
*/ */
export class ResponseError extends Error { export class ResponseError extends Error {
// The HTTP status code of response // The HTTP status code of response
httpStatusCode?: number; httpStatusCode?: number;
@ -35,7 +40,7 @@ export class ResponseError extends Error {
* @param {string} [hint] The error hint to provide a context (webhook related) * @param {string} [hint] The error hint to provide a context (webhook related)
* @memberof ResponseError * @memberof ResponseError
*/ */
constructor(message: string, errorCode?: number, httpStatusCode?: number, hint?:string) { constructor(message: string, errorCode?: number, httpStatusCode?: number, hint?: string) {
super(message); super(message);
this.name = 'ResponseError'; this.name = 'ResponseError';
@ -51,21 +56,23 @@ export class ResponseError extends Error {
} }
} }
export function basicAuthAuthorizationError(resp: Response, realm: string, message?: string) { export function basicAuthAuthorizationError(resp: Response, realm: string, message?: string) {
resp.statusCode = 401; resp.statusCode = 401;
resp.setHeader('WWW-Authenticate', `Basic realm="${realm}"`); 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) { export function jwtAuthAuthorizationError(resp: Response, message?: string) {
resp.statusCode = 403; resp.statusCode = 403;
resp.json({code: resp.statusCode, message}); resp.json({ code: resp.statusCode, message });
} }
export function sendSuccessResponse(
export function sendSuccessResponse(res: Response, data: any, raw?: boolean, responseCode?: number) { // tslint:disable-line:no-any res: Response,
data: any,
raw?: boolean,
responseCode?: number,
) {
if (responseCode !== undefined) { if (responseCode !== undefined) {
res.status(responseCode); res.status(responseCode);
} }
@ -83,7 +90,6 @@ export function sendSuccessResponse(res: Response, data: any, raw?: boolean, res
} }
} }
export function sendErrorResponse(res: Response, error: ResponseError) { export function sendErrorResponse(res: Response, error: ResponseError) {
let httpStatusCode = 500; let httpStatusCode = 500;
if (error.httpStatusCode) { if (error.httpStatusCode) {
@ -122,7 +128,6 @@ export function sendErrorResponse(res: Response, error: ResponseError) {
res.status(httpStatusCode).json(response); res.status(httpStatusCode).json(response);
} }
/** /**
* A helper function which does not just allow to return Promises it also makes sure that * A helper function which does not just allow to return Promises it also makes sure that
* all the responses have the same format * all the responses have the same format
@ -133,8 +138,7 @@ export function sendErrorResponse(res: Response, error: ResponseError) {
* @returns * @returns
*/ */
export function send(processFunction: (req: Request, res: Response) => Promise<any>) { // tslint:disable-line:no-any export function send(processFunction: (req: Request, res: Response) => Promise<any>) {
return async (req: Request, res: Response) => { return async (req: Request, res: Response) => {
try { try {
const data = await processFunction(req, res); const data = await processFunction(req, res);
@ -148,7 +152,6 @@ export function send(processFunction: (req: Request, res: Response) => Promise<a
}; };
} }
/** /**
* Flattens the Execution data. * Flattens the Execution data.
* As it contains a lot of references which normally would be saved as duplicate data * As it contains a lot of references which normally would be saved as duplicate data
@ -160,33 +163,34 @@ export function send(processFunction: (req: Request, res: Response) => Promise<a
*/ */
export function flattenExecutionData(fullExecutionData: IExecutionDb): IExecutionFlatted { export function flattenExecutionData(fullExecutionData: IExecutionDb): IExecutionFlatted {
// Flatten the data // Flatten the data
const returnData: IExecutionFlatted = Object.assign({}, { const returnData: IExecutionFlatted = {
data: stringify(fullExecutionData.data), data: stringify(fullExecutionData.data),
mode: fullExecutionData.mode, mode: fullExecutionData.mode,
// @ts-ignore
waitTill: fullExecutionData.waitTill, waitTill: fullExecutionData.waitTill,
startedAt: fullExecutionData.startedAt, startedAt: fullExecutionData.startedAt,
stoppedAt: fullExecutionData.stoppedAt, stoppedAt: fullExecutionData.stoppedAt,
finished: fullExecutionData.finished ? fullExecutionData.finished : false, finished: fullExecutionData.finished ? fullExecutionData.finished : false,
workflowId: fullExecutionData.workflowId, workflowId: fullExecutionData.workflowId,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
workflowData: fullExecutionData.workflowData!, workflowData: fullExecutionData.workflowData!,
}); };
if (fullExecutionData.id !== undefined) { if (fullExecutionData.id !== undefined) {
returnData.id = fullExecutionData.id!.toString(); returnData.id = fullExecutionData.id.toString();
} }
if (fullExecutionData.retryOf !== undefined) { if (fullExecutionData.retryOf !== undefined) {
returnData.retryOf = fullExecutionData.retryOf!.toString(); returnData.retryOf = fullExecutionData.retryOf.toString();
} }
if (fullExecutionData.retrySuccessId !== undefined) { if (fullExecutionData.retrySuccessId !== undefined) {
returnData.retrySuccessId = fullExecutionData.retrySuccessId!.toString(); returnData.retrySuccessId = fullExecutionData.retrySuccessId.toString();
} }
return returnData; return returnData;
} }
/** /**
* Unflattens the Execution data. * Unflattens the Execution data.
* *
@ -195,8 +199,7 @@ export function flattenExecutionData(fullExecutionData: IExecutionDb): IExecutio
* @returns {IExecutionResponse} * @returns {IExecutionResponse}
*/ */
export function unflattenExecutionData(fullExecutionData: IExecutionFlattedDb): IExecutionResponse { export function unflattenExecutionData(fullExecutionData: IExecutionFlattedDb): IExecutionResponse {
const returnData: IExecutionResponse = {
const returnData: IExecutionResponse = Object.assign({}, {
id: fullExecutionData.id.toString(), id: fullExecutionData.id.toString(),
workflowData: fullExecutionData.workflowData as IWorkflowDb, workflowData: fullExecutionData.workflowData as IWorkflowDb,
data: parse(fullExecutionData.data), data: parse(fullExecutionData.data),
@ -206,7 +209,7 @@ export function unflattenExecutionData(fullExecutionData: IExecutionFlattedDb):
stoppedAt: fullExecutionData.stoppedAt, stoppedAt: fullExecutionData.stoppedAt,
finished: fullExecutionData.finished ? fullExecutionData.finished : false, finished: fullExecutionData.finished ? fullExecutionData.finished : false,
workflowId: fullExecutionData.workflowId, workflowId: fullExecutionData.workflowId,
}); };
return returnData; return returnData;
} }

File diff suppressed because it is too large Load diff

View file

@ -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 { validate } from 'class-validator';
import { import { ResponseHelper } from '.';
ResponseHelper,
} from ".";
import { import { TagEntity } from './databases/entities/TagEntity';
TagEntity,
} from "./databases/entities/TagEntity";
import {
ITagWithCountDb,
} from "./Interfaces";
import { ITagWithCountDb } from './Interfaces';
// ---------------------------------- // ----------------------------------
// utils // utils
@ -29,7 +25,7 @@ export function sortByRequestOrder(tagsDb: TagEntity[], tagIds: string[]) {
return acc; return acc;
}, {} as { [key: string]: TagEntity }); }, {} 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); const errors = await validate(newTag);
if (errors.length) { if (errors.length) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const validationErrorMessage = Object.values(errors[0].constraints!)[0]; const validationErrorMessage = Object.values(errors[0].constraints!)[0];
throw new ResponseHelper.ResponseError(validationErrorMessage, undefined, 400); throw new ResponseHelper.ResponseError(validationErrorMessage, undefined, 400);
} }
@ -64,21 +61,28 @@ export function throwDuplicateEntryError(error: Error) {
/** /**
* Retrieve all tags and the number of workflows each tag is related to. * Retrieve all tags and the number of workflows each tag is related to.
*/ */
export function getTagsWithCountDb(tablePrefix: string): Promise<ITagWithCountDb[]> { export async function getTagsWithCountDb(tablePrefix: string): Promise<ITagWithCountDb[]> {
return getConnection() return getConnection()
.createQueryBuilder() .createQueryBuilder()
.select(`${tablePrefix}tag_entity.id`, 'id') .select(`${tablePrefix}tag_entity.id`, 'id')
.addSelect(`${tablePrefix}tag_entity.name`, 'name') .addSelect(`${tablePrefix}tag_entity.name`, 'name')
.addSelect(`COUNT(${tablePrefix}workflows_tags.workflowId)`, 'usageCount') .addSelect(`COUNT(${tablePrefix}workflows_tags.workflowId)`, 'usageCount')
.from(`${tablePrefix}tag_entity`, 'tag_entity') .from(`${tablePrefix}tag_entity`, 'tag_entity')
.leftJoin(`${tablePrefix}workflows_tags`, 'workflows_tags', `${tablePrefix}workflows_tags.tagId = tag_entity.id`) .leftJoin(
`${tablePrefix}workflows_tags`,
'workflows_tags',
`${tablePrefix}workflows_tags.tagId = tag_entity.id`,
)
.groupBy(`${tablePrefix}tag_entity.id`) .groupBy(`${tablePrefix}tag_entity.id`)
.getRawMany() .getRawMany()
.then(tagsWithCount => { .then((tagsWithCount) => {
tagsWithCount.forEach(tag => { 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(); tag.id = tag.id.toString();
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
tag.usageCount = Number(tag.usageCount); 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<ITagWithCountDb
/** /**
* Relate a workflow to one or more tags. * Relate a workflow to one or more tags.
*/ */
export function createRelations(workflowId: string, tagIds: string[], tablePrefix: string) { export async function createRelations(workflowId: string, tagIds: string[], tablePrefix: string) {
return getConnection() return getConnection()
.createQueryBuilder() .createQueryBuilder()
.insert() .insert()
.into(`${tablePrefix}workflows_tags`) .into(`${tablePrefix}workflows_tags`)
.values(tagIds.map(tagId => ({ workflowId, tagId }))) .values(tagIds.map((tagId) => ({ workflowId, tagId })))
.execute(); .execute();
} }
/** /**
* Remove all tags for a workflow during a tag update operation. * 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() return getConnection()
.createQueryBuilder() .createQueryBuilder()
.delete() .delete()

View file

@ -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 * as express from 'express';
import { import { ActiveWebhooks } from 'n8n-core';
IResponseCallbackData,
IWorkflowDb,
Push,
ResponseHelper,
WebhookHelpers,
} from './';
import {
ActiveWebhooks,
} from 'n8n-core';
import { import {
IWebhookData, IWebhookData,
@ -20,28 +13,28 @@ import {
WorkflowActivateMode, WorkflowActivateMode,
WorkflowExecuteMode, WorkflowExecuteMode,
} from 'n8n-workflow'; } 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)`; 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 { export class TestWebhooks {
private testWebhookData: { private testWebhookData: {
[key: string]: { [key: string]: {
sessionId?: string; sessionId?: string;
timeout: NodeJS.Timeout, timeout: NodeJS.Timeout;
workflowData: IWorkflowDb; workflowData: IWorkflowDb;
workflow: Workflow; workflow: Workflow;
}; };
} = {}; } = {};
private activeWebhooks: ActiveWebhooks | null = null;
private activeWebhooks: ActiveWebhooks | null = null;
constructor() { constructor() {
this.activeWebhooks = new ActiveWebhooks(); this.activeWebhooks = new ActiveWebhooks();
this.activeWebhooks.testWebhooks = true; this.activeWebhooks.testWebhooks = true;
} }
/** /**
* Executes a test-webhook and returns the data. It also makes sure that the * 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 * data gets additionally send to the UI. After the request got handled it
@ -54,7 +47,12 @@ export class TestWebhooks {
* @returns {Promise<object>} * @returns {Promise<object>}
* @memberof TestWebhooks * @memberof TestWebhooks
*/ */
async callTestWebhook(httpMethod: WebhookHttpMethod, path: string, request: express.Request, response: express.Response): Promise<IResponseCallbackData> { async callTestWebhook(
httpMethod: WebhookHttpMethod,
path: string,
request: express.Request,
response: express.Response,
): Promise<IResponseCallbackData> {
// Reset request parameters // Reset request parameters
request.params = {}; request.params = {};
@ -69,10 +67,16 @@ export class TestWebhooks {
if (webhookData === undefined) { if (webhookData === undefined) {
const pathElements = path.split('/'); const pathElements = path.split('/');
const webhookId = pathElements.shift(); const webhookId = pathElements.shift();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
webhookData = this.activeWebhooks!.get(httpMethod, pathElements.join('/'), webhookId); webhookData = this.activeWebhooks!.get(httpMethod, pathElements.join('/'), webhookId);
if (webhookData === undefined) { if (webhookData === undefined) {
// The requested webhook is not registered // 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; 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 // TODO: Clean that duplication up one day and improve code generally
if (this.testWebhookData[webhookKey] === undefined) { if (this.testWebhookData[webhookKey] === undefined) {
// The requested webhook is not registered // 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 the node which has the webhook defined to know where to start from and to
// get additional data // get additional data
@ -102,15 +115,28 @@ export class TestWebhooks {
throw new ResponseHelper.ResponseError('Could not find node to process webhook.', 404, 404); 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) => { return new Promise(async (resolve, reject) => {
try { try {
const executionMode = 'manual'; 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) => { 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) { if (error !== null) {
return reject(error); return reject(error);
} }
resolve(data); resolve(data);
}); },
);
if (executionId === undefined) { if (executionId === undefined) {
// The workflow did not run as the request was probably setup related // 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 // Inform editor-ui that webhook got received
if (this.testWebhookData[webhookKey].sessionId !== undefined) { if (this.testWebhookData[webhookKey].sessionId !== undefined) {
const pushInstance = Push.getInstance(); 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) { } catch (error) {
// Delete webhook also if an error is thrown // Delete webhook also if an error is thrown
} }
@ -132,6 +161,7 @@ export class TestWebhooks {
// Remove the webhook // Remove the webhook
clearTimeout(this.testWebhookData[webhookKey].timeout); clearTimeout(this.testWebhookData[webhookKey].timeout);
delete this.testWebhookData[webhookKey]; delete this.testWebhookData[webhookKey];
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.activeWebhooks!.removeWorkflow(workflow); this.activeWebhooks!.removeWorkflow(workflow);
}); });
} }
@ -140,18 +170,22 @@ export class TestWebhooks {
* Gets all request methods associated with a single test webhook * Gets all request methods associated with a single test webhook
* @param path webhook path * @param path webhook path
*/ */
async getWebhookMethods(path : string) : Promise<string[]> { async getWebhookMethods(path: string): Promise<string[]> {
const webhookMethods: string[] = this.activeWebhooks!.getWebhookMethods(path); const webhookMethods: string[] = this.activeWebhooks!.getWebhookMethods(path);
if (webhookMethods === undefined) { if (webhookMethods === undefined) {
// The requested webhook is not registered // 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; return webhookMethods;
} }
/** /**
* Checks if it has to wait for webhook data to execute the workflow. If yes it waits * 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 * for it and resolves with the result of the workflow if not it simply resolves
@ -162,9 +196,22 @@ export class TestWebhooks {
* @returns {(Promise<IExecutionDb | undefined>)} * @returns {(Promise<IExecutionDb | undefined>)}
* @memberof TestWebhooks * @memberof TestWebhooks
*/ */
async needsWebhookData(workflowData: IWorkflowDb, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode, sessionId?: string, destinationNode?: string): Promise<boolean> { async needsWebhookData(
const webhooks = WebhookHelpers.getWorkflowWebhooks(workflow, additionalData, destinationNode, true); workflowData: IWorkflowDb,
if (!webhooks.find(webhook => webhook.webhookDescription.restartWebhook !== true)) { workflow: Workflow,
additionalData: IWorkflowExecuteAdditionalData,
mode: WorkflowExecuteMode,
activation: WorkflowActivateMode,
sessionId?: string,
destinationNode?: string,
): Promise<boolean> {
const webhooks = WebhookHelpers.getWorkflowWebhooks(
workflow,
additionalData,
destinationNode,
true,
);
if (!webhooks.find((webhook) => webhook.webhookDescription.restartWebhook !== true)) {
// No webhooks found to start a workflow // No webhooks found to start a workflow
return false; return false;
} }
@ -180,8 +227,13 @@ export class TestWebhooks {
let key: string; let key: string;
const activatedKey: string[] = []; const activatedKey: string[] = [];
// eslint-disable-next-line no-restricted-syntax
for (const webhookData of webhooks) { 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); activatedKey.push(key);
@ -193,9 +245,11 @@ export class TestWebhooks {
}; };
try { try {
// eslint-disable-next-line no-await-in-loop
await this.activeWebhooks!.add(workflow, webhookData, mode, activation); await this.activeWebhooks!.add(workflow, webhookData, mode, activation);
} catch (error) { } 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); await this.activeWebhooks!.removeWorkflow(workflow);
throw error; throw error;
} }
@ -204,7 +258,6 @@ export class TestWebhooks {
return true; return true;
} }
/** /**
* Removes a test webhook of the workflow with the given id * Removes a test webhook of the workflow with the given id
* *
@ -214,10 +267,12 @@ export class TestWebhooks {
*/ */
cancelTestWebhook(workflowId: string): boolean { cancelTestWebhook(workflowId: string): boolean {
let foundWebhook = false; let foundWebhook = false;
// eslint-disable-next-line no-restricted-syntax
for (const webhookKey of Object.keys(this.testWebhookData)) { for (const webhookKey of Object.keys(this.testWebhookData)) {
const webhookData = this.testWebhookData[webhookKey]; const webhookData = this.testWebhookData[webhookKey];
if (webhookData.workflowData.id.toString() !== workflowId) { if (webhookData.workflowData.id.toString() !== workflowId) {
// eslint-disable-next-line no-continue
continue; continue;
} }
@ -227,19 +282,24 @@ export class TestWebhooks {
if (this.testWebhookData[webhookKey].sessionId !== undefined) { if (this.testWebhookData[webhookKey].sessionId !== undefined) {
try { try {
const pushInstance = Push.getInstance(); const pushInstance = Push.getInstance();
pushInstance.send('testWebhookDeleted', { workflowId }, this.testWebhookData[webhookKey].sessionId!); pushInstance.send(
'testWebhookDeleted',
{ workflowId },
this.testWebhookData[webhookKey].sessionId,
);
} catch (error) { } catch (error) {
// Could not inform editor, probably is not connected anymore. So sipmly go on. // 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 // Remove the webhook
delete this.testWebhookData[webhookKey]; delete this.testWebhookData[webhookKey];
if (foundWebhook === false) { if (!foundWebhook) {
// As it removes all webhooks of the workflow execute only once // As it removes all webhooks of the workflow execute only once
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.activeWebhooks!.removeWorkflow(workflow); this.activeWebhooks!.removeWorkflow(workflow);
} }
@ -249,7 +309,6 @@ export class TestWebhooks {
return foundWebhook; return foundWebhook;
} }
/** /**
* Removes all the currently active test webhooks * Removes all the currently active test webhooks
*/ */
@ -260,6 +319,7 @@ export class TestWebhooks {
let workflow: Workflow; let workflow: Workflow;
const workflows: Workflow[] = []; const workflows: Workflow[] = [];
// eslint-disable-next-line no-restricted-syntax
for (const webhookKey of Object.keys(this.testWebhookData)) { for (const webhookKey of Object.keys(this.testWebhookData)) {
workflow = this.testWebhookData[webhookKey].workflow; workflow = this.testWebhookData[webhookKey].workflow;
workflows.push(workflow); workflows.push(workflow);

View file

@ -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 { import {
ActiveExecutions, ActiveExecutions,
DatabaseType, DatabaseType,
@ -7,38 +20,23 @@ import {
IExecutionsStopData, IExecutionsStopData,
IWorkflowExecutionDataProcess, IWorkflowExecutionDataProcess,
ResponseHelper, ResponseHelper,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
WorkflowCredentials, WorkflowCredentials,
WorkflowRunner, WorkflowRunner,
} from '.'; } 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 { export class WaitTrackerClass {
activeExecutionsInstance: ActiveExecutions.ActiveExecutions; activeExecutionsInstance: ActiveExecutions.ActiveExecutions;
private waitingExecutions: { private waitingExecutions: {
[key: string]: { [key: string]: {
executionId: string, executionId: string;
timer: NodeJS.Timeout, timer: NodeJS.Timeout;
}; };
} = {}; } = {};
mainTimer: NodeJS.Timeout; mainTimer: NodeJS.Timeout;
constructor() { constructor() {
this.activeExecutionsInstance = ActiveExecutions.getInstance(); this.activeExecutionsInstance = ActiveExecutions.getInstance();
@ -50,7 +48,7 @@ export class WaitTrackerClass {
this.getwaitingExecutions(); this.getwaitingExecutions();
} }
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async getwaitingExecutions() { async getwaitingExecutions() {
Logger.debug('Wait tracker querying database for waiting executions'); Logger.debug('Wait tracker querying database for waiting executions');
// Find all the executions which should be triggered in the next 70 seconds // Find all the executions which should be triggered in the next 70 seconds
@ -63,11 +61,13 @@ export class WaitTrackerClass {
waitTill: 'ASC', waitTill: 'ASC',
}, },
}; };
const dbType = await GenericHelpers.getConfigValue('database.type') as DatabaseType; const dbType = (await GenericHelpers.getConfigValue('database.type')) as DatabaseType;
if (dbType === 'sqlite') { if (dbType === 'sqlite') {
// This is needed because of issue in TypeORM <> SQLite: // This is needed because of issue in TypeORM <> SQLite:
// https://github.com/typeorm/typeorm/issues/2286 // 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); const executions = await Db.collections.Execution!.find(findQuery);
@ -76,10 +76,13 @@ export class WaitTrackerClass {
return; return;
} }
const executionIds = executions.map(execution => execution.id.toString()).join(', '); const executionIds = executions.map((execution) => execution.id.toString()).join(', ');
Logger.debug(`Wait tracker found ${executions.length} executions. Setting timer for IDs: ${executionIds}`); 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 // 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) { for (const execution of executions) {
const executionId = execution.id.toString(); const executionId = execution.id.toString();
if (this.waitingExecutions[executionId] === undefined) { if (this.waitingExecutions[executionId] === undefined) {
@ -94,7 +97,6 @@ export class WaitTrackerClass {
} }
} }
async stopExecution(executionId: string): Promise<IExecutionsStopData> { async stopExecution(executionId: string): Promise<IExecutionsStopData> {
if (this.waitingExecutions[executionId] !== undefined) { if (this.waitingExecutions[executionId] !== undefined) {
// The waiting execution was already sheduled to execute. // The waiting execution was already sheduled to execute.
@ -124,7 +126,10 @@ export class WaitTrackerClass {
fullExecutionData.stoppedAt = new Date(); fullExecutionData.stoppedAt = new Date();
fullExecutionData.waitTill = undefined; fullExecutionData.waitTill = undefined;
await Db.collections.Execution!.update(executionId, ResponseHelper.flattenExecutionData(fullExecutionData)); await Db.collections.Execution!.update(
executionId,
ResponseHelper.flattenExecutionData(fullExecutionData),
);
return { return {
mode: fullExecutionData.mode, mode: fullExecutionData.mode,
@ -134,9 +139,8 @@ export class WaitTrackerClass {
}; };
} }
startExecution(executionId: string) { startExecution(executionId: string) {
Logger.debug(`Wait tracker resuming execution ${executionId}`, {executionId}); Logger.debug(`Wait tracker resuming execution ${executionId}`, { executionId });
delete this.waitingExecutions[executionId]; delete this.waitingExecutions[executionId];
(async () => { (async () => {
@ -149,7 +153,7 @@ export class WaitTrackerClass {
const fullExecutionData = ResponseHelper.unflattenExecutionData(fullExecutionDataFlatted); 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.'); 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(); const workflowRunner = new WorkflowRunner();
await workflowRunner.run(data, false, false, executionId); await workflowRunner.run(data, false, false, executionId);
})().catch((error) => { })().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; let waitTrackerInstance: WaitTrackerClass | undefined;
export function WaitTracker(): WaitTrackerClass { export function WaitTracker(): WaitTrackerClass {

View file

@ -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 { import {
Db, Db,
IExecutionResponse, IExecutionResponse,
@ -6,26 +22,18 @@ import {
NodeTypes, NodeTypes,
ResponseHelper, ResponseHelper,
WebhookHelpers, WebhookHelpers,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
WorkflowCredentials, WorkflowCredentials,
WorkflowExecuteAdditionalData, WorkflowExecuteAdditionalData,
} from '.'; } 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 { export class WaitingWebhooks {
async executeWebhook(
async executeWebhook(httpMethod: WebhookHttpMethod, fullPath: string, req: express.Request, res: express.Response): Promise<IResponseCallbackData> { httpMethod: WebhookHttpMethod,
fullPath: string,
req: express.Request,
res: express.Response,
): Promise<IResponseCallbackData> {
Logger.debug(`Received waiting-webhoook "${httpMethod}" for path "${fullPath}"`); Logger.debug(`Received waiting-webhoook "${httpMethod}" for path "${fullPath}"`);
// Reset request parameters // Reset request parameters
@ -44,47 +52,77 @@ export class WaitingWebhooks {
const execution = await Db.collections.Execution?.findOne(executionId); const execution = await Db.collections.Execution?.findOne(executionId);
if (execution === undefined) { 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); const fullExecutionData = ResponseHelper.unflattenExecutionData(execution);
if (fullExecutionData.finished === true || fullExecutionData.data.resultData.error) { if (fullExecutionData.finished || fullExecutionData.data.resultData.error) {
throw new ResponseHelper.ResponseError(`The execution "${executionId} has finished already.`, 409, 409); throw new ResponseHelper.ResponseError(
`The execution "${executionId} has finished already.`,
409,
409,
);
} }
return this.startExecution(httpMethod, path, fullExecutionData, req, res); return this.startExecution(httpMethod, path, fullExecutionData, req, res);
} }
async startExecution(
async startExecution(httpMethod: WebhookHttpMethod, path: string, fullExecutionData: IExecutionResponse, req: express.Request, res: express.Response): Promise<IResponseCallbackData> { httpMethod: WebhookHttpMethod,
path: string,
fullExecutionData: IExecutionResponse,
req: express.Request,
res: express.Response,
): Promise<IResponseCallbackData> {
const executionId = fullExecutionData.id; 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.'); 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 // 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 // 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 // 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 // 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 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 additionalData = await WorkflowExecuteAdditionalData.getBase();
const webhookData = NodeHelpers.getNodeWebhooks(workflow, workflow.getNode(lastNodeExecuted) as INode, additionalData).filter((webhook) => { const webhookData = NodeHelpers.getNodeWebhooks(
return (webhook.httpMethod === httpMethod && webhook.path === path && webhook.webhookDescription.restartWebhook === true); workflow,
workflow.getNode(lastNodeExecuted) as INode,
additionalData,
).filter((webhook) => {
return (
webhook.httpMethod === httpMethod &&
webhook.path === path &&
webhook.webhookDescription.restartWebhook === true
);
})[0]; })[0];
if (webhookData === undefined) { if (webhookData === undefined) {
@ -100,18 +138,30 @@ export class WaitingWebhooks {
throw new ResponseHelper.ResponseError('Could not find node to process webhook.', 404, 404); 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) => { return new Promise((resolve, reject) => {
const executionMode = 'webhook'; const executionMode = 'webhook';
WebhookHelpers.executeWebhook(workflow, webhookData, workflowData as IWorkflowDb, workflowStartNode, executionMode, undefined, runExecutionData, fullExecutionData.id, req, res, (error: Error | null, data: object) => { // 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) { if (error !== null) {
return reject(error); return reject(error);
} }
resolve(data); resolve(data);
},
);
}); });
});
} }
} }

View file

@ -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'; import * as express from 'express';
// eslint-disable-next-line import/no-extraneous-dependencies
import { get } from 'lodash'; import { get } from 'lodash';
import { import { BINARY_ENCODING, NodeExecuteFunctions } from 'n8n-core';
ActiveExecutions,
GenericHelpers,
IExecutionDb,
IResponseCallbackData,
IWorkflowDb,
IWorkflowExecutionDataProcess,
ResponseHelper,
WorkflowCredentials,
WorkflowExecuteAdditionalData,
WorkflowHelpers,
WorkflowRunner,
} from './';
import {
BINARY_ENCODING,
NodeExecuteFunctions,
} from 'n8n-core';
import { import {
IBinaryKeyData, IBinaryKeyData,
@ -35,7 +32,21 @@ import {
Workflow, Workflow,
WorkflowExecuteMode, WorkflowExecuteMode,
} from 'n8n-workflow'; } 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(); const activeExecutions = ActiveExecutions.getInstance();
@ -47,7 +58,12 @@ const activeExecutions = ActiveExecutions.getInstance();
* @param {Workflow} workflow * @param {Workflow} workflow
* @returns {IWebhookData[]} * @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 // Check all the nodes in the workflow if they have webhooks
const returnData: IWebhookData[] = []; const returnData: IWebhookData[] = [];
@ -63,9 +79,13 @@ export function getWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflo
if (parentNodes !== undefined && !parentNodes.includes(node.name)) { if (parentNodes !== undefined && !parentNodes.includes(node.name)) {
// If parentNodes are given check only them if they have webhooks // If parentNodes are given check only them if they have webhooks
// and no other ones // and no other ones
// eslint-disable-next-line no-continue
continue; continue;
} }
returnData.push.apply(returnData, NodeHelpers.getNodeWebhooks(workflow, node, additionalData, ignoreRestartWehbooks)); returnData.push.apply(
returnData,
NodeHelpers.getNodeWebhooks(workflow, node, additionalData, ignoreRestartWehbooks),
);
} }
return returnData; return returnData;
@ -91,8 +111,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
return returnData; return returnData;
} }
/**
/**
* Executes a webhook * Executes a webhook
* *
* @export * @export
@ -106,7 +125,19 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
* @param {((error: Error | null, data: IResponseCallbackData) => void)} responseCallback * @param {((error: Error | null, data: IResponseCallbackData) => void)} responseCallback
* @returns {(Promise<string | undefined>)} * @returns {(Promise<string | undefined>)}
*/ */
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<string | undefined> { 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<string | undefined> {
// Get the nodeType to know which responseMode is set // Get the nodeType to know which responseMode is set
const nodeType = workflow.nodeTypes.getByName(workflowStartNode.type); const nodeType = workflow.nodeTypes.getByName(workflowStartNode.type);
if (nodeType === undefined) { if (nodeType === undefined) {
@ -120,8 +151,20 @@ export async function executeWebhook(workflow: Workflow, webhookData: IWebhookDa
}; };
// Get the responseMode // Get the responseMode
const responseMode = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseMode'], executionMode, additionalKeys, 'onReceived'); const responseMode = workflow.expression.getSimpleParameterValue(
const responseCode = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseCode'], executionMode, additionalKeys, 200) as number; 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 (!['onReceived', 'lastNode'].includes(responseMode as string)) {
// If the mode is not known we error. Is probably best like that instead of using // 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; let webhookResultData: IWebhookResponseData;
try { try {
webhookResultData = await workflow.runWebhook(webhookData, workflowStartNode, additionalData, NodeExecuteFunctions, executionMode); webhookResultData = await workflow.runWebhook(
webhookData,
workflowStartNode,
additionalData,
NodeExecuteFunctions,
executionMode,
);
} catch (err) { } catch (err) {
// Send error response to webhook caller // Send error response to webhook caller
const errorMessage = 'Workflow Webhook Error: Workflow could not be started!'; const errorMessage = 'Workflow Webhook Error: Workflow could not be started!';
@ -171,7 +220,7 @@ export async function executeWebhook(workflow: Workflow, webhookData: IWebhookDa
noWebhookResponse: true, noWebhookResponse: true,
// Add empty data that it at least tries to "execute" the webhook // Add empty data that it at least tries to "execute" the webhook
// which then so gets the chance to throw the error. // 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, $executionId: executionId,
}; };
if (webhookData.webhookDescription['responseHeaders'] !== undefined) { if (webhookData.webhookDescription.responseHeaders !== undefined) {
const responseHeaders = workflow.expression.getComplexParameterValue(workflowStartNode, webhookData.webhookDescription['responseHeaders'], executionMode, additionalKeys, undefined) as { const responseHeaders = workflow.expression.getComplexParameterValue(
entries?: Array<{ workflowStartNode,
webhookData.webhookDescription.responseHeaders,
executionMode,
additionalKeys,
undefined,
) as {
entries?:
| Array<{
name: string; name: string;
value: string; value: string;
}> | undefined; }>
| undefined;
}; };
if (responseHeaders !== undefined && responseHeaders['entries'] !== undefined) { if (responseHeaders !== undefined && responseHeaders.entries !== undefined) {
for (const item of responseHeaders['entries']) { for (const item of responseHeaders.entries) {
res.setHeader(item['name'], item['value']); res.setHeader(item.name, item.value);
} }
} }
} }
if (webhookResultData.noWebhookResponse === true && didSendResponse === false) { if (webhookResultData.noWebhookResponse === true && !didSendResponse) {
// The response got already send // The response got already send
responseCallback(null, { responseCallback(null, {
noWebhookResponse: true, noWebhookResponse: true,
@ -209,7 +266,7 @@ export async function executeWebhook(workflow: Workflow, webhookData: IWebhookDa
// Workflow should not run // Workflow should not run
if (webhookResultData.webhookResponse !== undefined) { if (webhookResultData.webhookResponse !== undefined) {
// Data to respond with is given // Data to respond with is given
if (didSendResponse === false) { if (!didSendResponse) {
responseCallback(null, { responseCallback(null, {
data: webhookResultData.webhookResponse, data: webhookResultData.webhookResponse,
responseCode, responseCode,
@ -218,7 +275,8 @@ export async function executeWebhook(workflow: Workflow, webhookData: IWebhookDa
} }
} else { } else {
// Send default response // Send default response
if (didSendResponse === false) { // eslint-disable-next-line no-lonely-if
if (!didSendResponse) {
responseCallback(null, { responseCallback(null, {
data: { data: {
message: 'Webhook call got received.', 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 // 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 // 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 // Return response directly and do not wait for the workflow to finish
if (webhookResultData.webhookResponse !== undefined) { if (webhookResultData.webhookResponse !== undefined) {
// Data to respond with is given // Data to respond with is given
@ -255,18 +313,17 @@ export async function executeWebhook(workflow: Workflow, webhookData: IWebhookDa
// Initialize the data of the webhook node // Initialize the data of the webhook node
const nodeExecutionStack: IExecuteData[] = []; const nodeExecutionStack: IExecuteData[] = [];
nodeExecutionStack.push( nodeExecutionStack.push({
{
node: workflowStartNode, node: workflowStartNode,
data: { data: {
main: webhookResultData.workflowData, main: webhookResultData.workflowData,
}, },
} });
);
runExecutionData = runExecutionData || { runExecutionData =
startData: { runExecutionData ||
}, ({
startData: {},
resultData: { resultData: {
runData: {}, runData: {},
}, },
@ -275,12 +332,13 @@ export async function executeWebhook(workflow: Workflow, webhookData: IWebhookDa
nodeExecutionStack, nodeExecutionStack,
waitingExecution: {}, waitingExecution: {},
}, },
} as IRunExecutionData; } as IRunExecutionData);
if (executionId !== undefined) { if (executionId !== undefined) {
// Set the data the webhook node did return on the waiting node if executionId // 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. // 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) { if (Object.keys(runExecutionDataMerge).length !== 0) {
@ -299,13 +357,19 @@ export async function executeWebhook(workflow: Workflow, webhookData: IWebhookDa
const workflowRunner = new WorkflowRunner(); const workflowRunner = new WorkflowRunner();
executionId = await workflowRunner.run(runData, true, !didSendResponse, executionId); 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 // Get a promise which resolves when the workflow did execute and send then response
const executePromise = activeExecutions.getPostExecutePromise(executionId) as Promise<IExecutionDb | undefined>; const executePromise = activeExecutions.getPostExecutePromise(executionId) as Promise<
executePromise.then((data) => { IExecutionDb | undefined
>;
executePromise
.then((data) => {
if (data === undefined) { if (data === undefined) {
if (didSendResponse === false) { if (!didSendResponse) {
responseCallback(null, { responseCallback(null, {
data: { data: {
message: 'Workflow did execute sucessfully but no data got returned.', message: 'Workflow did execute sucessfully but no data got returned.',
@ -318,8 +382,8 @@ export async function executeWebhook(workflow: Workflow, webhookData: IWebhookDa
} }
const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data); const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data);
if(data.data.resultData.error || returnData?.error !== undefined) { if (data.data.resultData.error || returnData?.error !== undefined) {
if (didSendResponse === false) { if (!didSendResponse) {
responseCallback(null, { responseCallback(null, {
data: { data: {
message: 'Workflow did error.', message: 'Workflow did error.',
@ -332,10 +396,11 @@ export async function executeWebhook(workflow: Workflow, webhookData: IWebhookDa
} }
if (returnData === undefined) { if (returnData === undefined) {
if (didSendResponse === false) { if (!didSendResponse) {
responseCallback(null, { responseCallback(null, {
data: { data: {
message: 'Workflow did execute sucessfully but the last node did not return any data.', message:
'Workflow did execute sucessfully but the last node did not return any data.',
}, },
responseCode, responseCode,
}); });
@ -348,9 +413,15 @@ export async function executeWebhook(workflow: Workflow, webhookData: IWebhookDa
$executionId: executionId, $executionId: executionId,
}; };
const responseData = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseData'], executionMode, additionalKeys, 'firstEntryJson'); const responseData = workflow.expression.getSimpleParameterValue(
workflowStartNode,
webhookData.webhookDescription.responseData,
executionMode,
additionalKeys,
'firstEntryJson',
);
if (didSendResponse === false) { if (!didSendResponse) {
let data: IDataObject | IDataObject[]; let data: IDataObject | IDataObject[];
if (responseData === 'firstEntryJson') { if (responseData === 'firstEntryJson') {
@ -363,20 +434,36 @@ export async function executeWebhook(workflow: Workflow, webhookData: IWebhookDa
data = returnData.data!.main[0]![0].json; data = returnData.data!.main[0]![0].json;
const responsePropertyName = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responsePropertyName'], executionMode, additionalKeys, undefined); const responsePropertyName = workflow.expression.getSimpleParameterValue(
workflowStartNode,
webhookData.webhookDescription.responsePropertyName,
executionMode,
additionalKeys,
undefined,
);
if (responsePropertyName !== undefined) { if (responsePropertyName !== undefined) {
data = get(data, responsePropertyName as string) as IDataObject; data = get(data, responsePropertyName as string) as IDataObject;
} }
const responseContentType = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseContentType'], executionMode, additionalKeys, undefined); const responseContentType = workflow.expression.getSimpleParameterValue(
workflowStartNode,
webhookData.webhookDescription.responseContentType,
executionMode,
additionalKeys,
undefined,
);
if (responseContentType !== undefined) { if (responseContentType !== undefined) {
// Send the webhook response manually to be able to set the content-type // Send the webhook response manually to be able to set the content-type
res.setHeader('Content-Type', responseContentType as string); res.setHeader('Content-Type', responseContentType as string);
// Returning an object, boolean, number, ... causes problems so make sure to stringify if needed // 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)) { if (
data !== null &&
data !== undefined &&
['Buffer', 'String'].includes(data.constructor.name)
) {
res.end(data); res.end(data);
} else { } else {
res.end(JSON.stringify(data)); res.end(JSON.stringify(data));
@ -387,7 +474,6 @@ export async function executeWebhook(workflow: Workflow, webhookData: IWebhookDa
}); });
didSendResponse = true; didSendResponse = true;
} }
} else if (responseData === 'firstEntryBinary') { } else if (responseData === 'firstEntryBinary') {
// Return the binary data of the first entry // Return the binary data of the first entry
data = returnData.data!.main[0]![0]; data = returnData.data!.main[0]![0];
@ -402,20 +488,33 @@ export async function executeWebhook(workflow: Workflow, webhookData: IWebhookDa
didSendResponse = true; didSendResponse = true;
} }
const responseBinaryPropertyName = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseBinaryPropertyName'], executionMode, additionalKeys, 'data'); const responseBinaryPropertyName = workflow.expression.getSimpleParameterValue(
workflowStartNode,
webhookData.webhookDescription.responseBinaryPropertyName,
executionMode,
additionalKeys,
'data',
);
if (responseBinaryPropertyName === undefined && didSendResponse === false) { if (responseBinaryPropertyName === undefined && !didSendResponse) {
responseCallback(new Error('No "responseBinaryPropertyName" is set.'), {}); responseCallback(new Error('No "responseBinaryPropertyName" is set.'), {});
didSendResponse = true; didSendResponse = true;
} }
const binaryData = (data.binary as IBinaryKeyData)[responseBinaryPropertyName as string]; const binaryData = (data.binary as IBinaryKeyData)[
if (binaryData === undefined && didSendResponse === false) { responseBinaryPropertyName as string
responseCallback(new Error(`The binary property "${responseBinaryPropertyName}" which should be returned does not exist.`), {}); ];
if (binaryData === undefined && !didSendResponse) {
responseCallback(
new Error(
`The binary property "${responseBinaryPropertyName}" which should be returned does not exist.`,
),
{},
);
didSendResponse = true; didSendResponse = true;
} }
if (didSendResponse === false) { if (!didSendResponse) {
// Send the webhook response manually // Send the webhook response manually
res.setHeader('Content-Type', binaryData.mimeType); res.setHeader('Content-Type', binaryData.mimeType);
res.end(Buffer.from(binaryData.data, BINARY_ENCODING)); res.end(Buffer.from(binaryData.data, BINARY_ENCODING));
@ -424,7 +523,6 @@ export async function executeWebhook(workflow: Workflow, webhookData: IWebhookDa
noWebhookResponse: true, noWebhookResponse: true,
}); });
} }
} else { } else {
// Return the JSON data of all the entries // Return the JSON data of all the entries
data = []; data = [];
@ -433,7 +531,7 @@ export async function executeWebhook(workflow: Workflow, webhookData: IWebhookDa
} }
} }
if (didSendResponse === false) { if (!didSendResponse) {
responseCallback(null, { responseCallback(null, {
data, data,
responseCode, responseCode,
@ -445,17 +543,17 @@ export async function executeWebhook(workflow: Workflow, webhookData: IWebhookDa
return data; return data;
}) })
.catch((e) => { .catch((e) => {
if (didSendResponse === false) { if (!didSendResponse) {
responseCallback(new Error('There was a problem executing the workflow.'), {}); 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; return executionId;
} catch (e) { } catch (e) {
if (didSendResponse === false) { if (!didSendResponse) {
responseCallback(new Error('There was a problem executing the workflow.'), {}); 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 * Returns the base URL of the webhooks
* *

View file

@ -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 * as express from 'express';
import { import { readFileSync } from 'fs';
readFileSync, import { getConnectionManager } from 'typeorm';
} from 'fs';
import {
getConnectionManager,
} from 'typeorm';
import * as bodyParser from 'body-parser'; 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 _ 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 { import {
ActiveExecutions, ActiveExecutions,
ActiveWorkflowRunner, ActiveWorkflowRunner,
@ -19,25 +27,31 @@ import {
IExternalHooksClass, IExternalHooksClass,
IPackageVersions, IPackageVersions,
ResponseHelper, ResponseHelper,
} from './'; } from '.';
import * as compression from 'compression';
import * as config from '../config'; 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() { export function registerProductionWebhooks() {
// ---------------------------------------- // ----------------------------------------
// Regular Webhooks // Regular Webhooks
// ---------------------------------------- // ----------------------------------------
// HEAD webhook requests // HEAD webhook requests
this.app.head(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => { this.app.head(
`/${this.endpointWebhook}/*`,
async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook/" to get the registred part of the url // Cut away the "/webhook/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhook.length + 2); const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
this.endpointWebhook.length + 2,
);
let response; let response;
try { try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
response = await this.activeWorkflowRunner.executeWebhook('HEAD', requestUrl, req, res); response = await this.activeWorkflowRunner.executeWebhook('HEAD', requestUrl, req, res);
} catch (error) { } catch (error) {
ResponseHelper.sendErrorResponse(res, error); ResponseHelper.sendErrorResponse(res, error);
@ -50,12 +64,17 @@ export function registerProductionWebhooks() {
} }
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
}); },
);
// OPTIONS webhook requests // OPTIONS webhook requests
this.app.options(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => { this.app.options(
`/${this.endpointWebhook}/*`,
async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook/" to get the registred part of the url // Cut away the "/webhook/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhook.length + 2); const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
this.endpointWebhook.length + 2,
);
let allowedMethods: string[]; let allowedMethods: string[];
try { try {
@ -70,12 +89,17 @@ export function registerProductionWebhooks() {
} }
ResponseHelper.sendSuccessResponse(res, {}, true, 204); ResponseHelper.sendSuccessResponse(res, {}, true, 204);
}); },
);
// GET webhook requests // GET webhook requests
this.app.get(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => { this.app.get(
`/${this.endpointWebhook}/*`,
async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook/" to get the registred part of the url // Cut away the "/webhook/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhook.length + 2); const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
this.endpointWebhook.length + 2,
);
let response; let response;
try { try {
@ -91,12 +115,17 @@ export function registerProductionWebhooks() {
} }
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
}); },
);
// POST webhook requests // POST webhook requests
this.app.post(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => { this.app.post(
`/${this.endpointWebhook}/*`,
async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook/" to get the registred part of the url // Cut away the "/webhook/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhook.length + 2); const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
this.endpointWebhook.length + 2,
);
let response; let response;
try { try {
@ -112,27 +141,43 @@ export function registerProductionWebhooks() {
} }
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
}); },
);
} }
class App { class App {
app: express.Application; app: express.Application;
activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner; activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner;
endpointWebhook: string; endpointWebhook: string;
endpointPresetCredentials: string; endpointPresetCredentials: string;
externalHooks: IExternalHooksClass; externalHooks: IExternalHooksClass;
saveDataErrorExecution: string; saveDataErrorExecution: string;
saveDataSuccessExecution: string; saveDataSuccessExecution: string;
saveManualExecutions: boolean; saveManualExecutions: boolean;
executionTimeout: number; executionTimeout: number;
maxExecutionTimeout: number; maxExecutionTimeout: number;
timezone: string; timezone: string;
activeExecutionsInstance: ActiveExecutions.ActiveExecutions; activeExecutionsInstance: ActiveExecutions.ActiveExecutions;
versions: IPackageVersions | undefined; versions: IPackageVersions | undefined;
restEndpoint: string; restEndpoint: string;
protocol: string; protocol: string;
sslKey: string; sslKey: string;
sslCert: string; sslCert: string;
presetCredentialsLoaded: boolean; presetCredentialsLoaded: boolean;
@ -163,7 +208,6 @@ class App {
this.endpointPresetCredentials = config.get('credentials.overwrite.endpoint') as string; this.endpointPresetCredentials = config.get('credentials.overwrite.endpoint') as string;
} }
/** /**
* Returns the current epoch time * Returns the current epoch time
* *
@ -174,9 +218,7 @@ class App {
return new Date(); return new Date();
} }
async config(): Promise<void> { async config(): Promise<void> {
this.versions = await GenericHelpers.getVersions(); this.versions = await GenericHelpers.getVersions();
// Compress the response data // Compress the response data
@ -191,49 +233,63 @@ class App {
}); });
// Support application/json type post data // Support application/json type post data
this.app.use(bodyParser.json({ this.app.use(
limit: '16mb', verify: (req, res, buf) => { bodyParser.json({
// @ts-ignore limit: '16mb',
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(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) => { verify: (req, res, buf) => {
// @ts-ignore // @ts-ignore
req.rawBody = buf; req.rawBody = buf;
}, },
})); }),
);
if (process.env['NODE_ENV'] !== 'production') { // Support application/xml type post data
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;
},
}),
);
// 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') {
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
// Allow access also from frontend when developing // Allow access also from frontend when developing
res.header('Access-Control-Allow-Origin', 'http://localhost:8080'); 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-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(); next();
}); });
} }
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
if (Db.collections.Workflow === null) { if (Db.collections.Workflow === null) {
const error = new ResponseHelper.ResponseError('Database is not ready!', undefined, 503); const error = new ResponseHelper.ResponseError('Database is not ready!', undefined, 503);
@ -243,25 +299,22 @@ class App {
next(); next();
}); });
// ---------------------------------------- // ----------------------------------------
// Healthcheck // Healthcheck
// ---------------------------------------- // ----------------------------------------
// Does very basic health check // Does very basic health check
this.app.get('/healthz', async (req: express.Request, res: express.Response) => { this.app.get('/healthz', async (req: express.Request, res: express.Response) => {
const connection = getConnectionManager().get(); const connection = getConnectionManager().get();
try { try {
if (connection.isConnected === false) { if (!connection.isConnected) {
// Connection is not active // Connection is not active
throw new Error('No active database connection!'); throw new Error('No active database connection!');
} }
// DB ping // DB ping
await connection.query('SELECT 1'); await connection.query('SELECT 1');
// eslint-disable-next-line id-denylist
} catch (err) { } catch (err) {
const error = new ResponseHelper.ResponseError('No Database connection!', undefined, 503); const error = new ResponseHelper.ResponseError('No Database connection!', undefined, 503);
return ResponseHelper.sendErrorResponse(res, error); return ResponseHelper.sendErrorResponse(res, error);
@ -276,9 +329,7 @@ class App {
}); });
registerProductionWebhooks.apply(this); registerProductionWebhooks.apply(this);
} }
} }
export async function start(): Promise<void> { export async function start(): Promise<void> {
@ -292,12 +343,14 @@ export async function start(): Promise<void> {
let server; let server;
if (app.protocol === 'https' && app.sslKey && app.sslCert) { if (app.protocol === 'https' && app.sslKey && app.sslCert) {
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
const https = require('https'); const https = require('https');
const privateKey = readFileSync(app.sslKey, 'utf8'); const privateKey = readFileSync(app.sslKey, 'utf8');
const cert = readFileSync(app.sslCert, 'utf8'); const cert = readFileSync(app.sslCert, 'utf8');
const credentials = { key: privateKey, cert }; const credentials = { key: privateKey, cert };
server = https.createServer(credentials, app.app); server = https.createServer(credentials, app.app);
} else { } else {
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
const http = require('http'); const http = require('http');
server = http.createServer(app.app); server = http.createServer(app.app);
} }

View file

@ -1,22 +1,25 @@
import { /* eslint-disable no-prototype-builtins */
Db, import { INode, IWorkflowCredentials } from 'n8n-workflow';
} from './'; // eslint-disable-next-line import/no-cycle
import { import { Db } from '.';
INode,
IWorkflowCredentials
} from 'n8n-workflow';
// eslint-disable-next-line @typescript-eslint/naming-convention
export async function WorkflowCredentials(nodes: INode[]): Promise<IWorkflowCredentials> { export async function WorkflowCredentials(nodes: INode[]): Promise<IWorkflowCredentials> {
// Go through all nodes to find which credentials are needed to execute the workflow // Go through all nodes to find which credentials are needed to execute the workflow
const returnCredentials: IWorkflowCredentials = {}; 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) { for (node of nodes) {
if (node.disabled === true || !node.credentials) { if (node.disabled === true || !node.credentials) {
// eslint-disable-next-line no-continue
continue; continue;
} }
// eslint-disable-next-line no-restricted-syntax
for (type of Object.keys(node.credentials)) { for (type of Object.keys(node.credentials)) {
if (!returnCredentials.hasOwnProperty(type)) { if (!returnCredentials.hasOwnProperty(type)) {
returnCredentials[type] = {}; returnCredentials[type] = {};
@ -24,14 +27,15 @@ export async function WorkflowCredentials(nodes: INode[]): Promise<IWorkflowCred
name = node.credentials[type]; name = node.credentials[type];
if (!returnCredentials[type].hasOwnProperty(name)) { if (!returnCredentials[type].hasOwnProperty(name)) {
// eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-non-null-assertion
foundCredentials = await Db.collections.Credentials!.find({ name, type }); foundCredentials = await Db.collections.Credentials!.find({ name, type });
if (!foundCredentials.length) { if (!foundCredentials.length) {
throw new Error(`Could not find credentials for type "${type}" with name "${name}".`); throw new Error(`Could not find credentials for type "${type}" with name "${name}".`);
} }
// eslint-disable-next-line prefer-destructuring
returnCredentials[type][name] = foundCredentials[0]; returnCredentials[type][name] = foundCredentials[0];
} }
} }
} }
return returnCredentials; return returnCredentials;

View file

@ -1,27 +1,20 @@
import { /* eslint-disable no-restricted-syntax */
ActiveExecutions, /* eslint-disable @typescript-eslint/restrict-plus-operands */
CredentialsHelper, /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
Db, /* eslint-disable @typescript-eslint/await-thenable */
ExternalHooks, /* eslint-disable @typescript-eslint/no-non-null-assertion */
IExecutionDb, /* eslint-disable @typescript-eslint/no-use-before-define */
IExecutionFlattedDb, /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
IExecutionResponse, /* eslint-disable no-param-reassign */
IPushDataExecutionFinished, /* eslint-disable @typescript-eslint/no-unsafe-call */
IWorkflowBase, /* eslint-disable @typescript-eslint/prefer-optional-chain */
IWorkflowExecuteProcess, /* eslint-disable id-denylist */
IWorkflowExecutionDataProcess, /* eslint-disable @typescript-eslint/restrict-template-expressions */
NodeTypes, /* eslint-disable @typescript-eslint/no-unsafe-member-access */
Push, /* eslint-disable @typescript-eslint/no-unused-vars */
ResponseHelper, /* eslint-disable func-names */
WebhookHelpers, /* eslint-disable @typescript-eslint/no-unsafe-assignment */
WorkflowCredentials, import { UserSettings, WorkflowExecute } from 'n8n-core';
WorkflowHelpers,
} from './';
import {
UserSettings,
WorkflowExecute,
} from 'n8n-core';
import { import {
IDataObject, IDataObject,
@ -43,10 +36,29 @@ import {
WorkflowHooks, WorkflowHooks,
} from 'n8n-workflow'; } from 'n8n-workflow';
import * as config from '../config';
import { LessThanOrEqual } from 'typeorm'; import { LessThanOrEqual } from 'typeorm';
import { DateUtils } from 'typeorm/util/DateUtils'; import { DateUtils } from 'typeorm/util/DateUtils';
import * as config from '../config';
// eslint-disable-next-line import/no-cycle
import {
ActiveExecutions,
CredentialsHelper,
Db,
ExternalHooks,
IExecutionDb,
IExecutionFlattedDb,
IExecutionResponse,
IPushDataExecutionFinished,
IWorkflowBase,
IWorkflowExecuteProcess,
IWorkflowExecutionDataProcess,
NodeTypes,
Push,
ResponseHelper,
WebhookHelpers,
WorkflowCredentials,
WorkflowHelpers,
} from '.';
const ERROR_TRIGGER_TYPE = config.get('nodes.errorTriggerType') as string; const ERROR_TRIGGER_TYPE = config.get('nodes.errorTriggerType') as string;
@ -59,10 +71,16 @@ const ERROR_TRIGGER_TYPE = config.get('nodes.errorTriggerType') as string;
* @param {WorkflowExecuteMode} mode The mode in which the workflow got started in * @param {WorkflowExecuteMode} mode The mode in which the workflow got started in
* @param {string} [executionId] The id the execution got saved as * @param {string} [executionId] The id the execution got saved as
*/ */
function executeErrorWorkflow(workflowData: IWorkflowBase, fullRunData: IRun, mode: WorkflowExecuteMode, executionId?: string, retryOf?: string): void { function executeErrorWorkflow(
workflowData: IWorkflowBase,
fullRunData: IRun,
mode: WorkflowExecuteMode,
executionId?: string,
retryOf?: string,
): void {
// Check if there was an error and if so if an errorWorkflow or a trigger is set // Check if there was an error and if so if an errorWorkflow or a trigger is set
let pastExecutionUrl: string | undefined = undefined; let pastExecutionUrl: string | undefined;
if (executionId !== undefined) { if (executionId !== undefined) {
pastExecutionUrl = `${WebhookHelpers.getWebhookBaseUrl()}execution/${executionId}`; pastExecutionUrl = `${WebhookHelpers.getWebhookBaseUrl()}execution/${executionId}`;
} }
@ -78,20 +96,42 @@ function executeErrorWorkflow(workflowData: IWorkflowBase, fullRunData: IRun, mo
retryOf, retryOf,
}, },
workflow: { workflow: {
id: workflowData.id !== undefined ? workflowData.id.toString() as string : undefined, id: workflowData.id !== undefined ? workflowData.id.toString() : undefined,
name: workflowData.name, name: workflowData.name,
}, },
}; };
// Run the error workflow // Run the error workflow
// To avoid an infinite loop do not run the error workflow again if the error-workflow itself failed and it is its own error-workflow. // To avoid an infinite loop do not run the error workflow again if the error-workflow itself failed and it is its own error-workflow.
if (workflowData.settings !== undefined && workflowData.settings.errorWorkflow && !(mode === 'error' && workflowData.id && workflowData.settings.errorWorkflow.toString() === workflowData.id.toString())) { if (
Logger.verbose(`Start external error workflow`, { executionId, errorWorkflowId: workflowData.settings.errorWorkflow.toString(), workflowId: workflowData.id }); // eslint-disable-next-line @typescript-eslint/prefer-optional-chain
workflowData.settings !== undefined &&
workflowData.settings.errorWorkflow &&
!(
mode === 'error' &&
workflowData.id &&
workflowData.settings.errorWorkflow.toString() === workflowData.id.toString()
)
) {
Logger.verbose(`Start external error workflow`, {
executionId,
errorWorkflowId: workflowData.settings.errorWorkflow.toString(),
workflowId: workflowData.id,
});
// If a specific error workflow is set run only that one // If a specific error workflow is set run only that one
WorkflowHelpers.executeErrorWorkflow(workflowData.settings.errorWorkflow as string, workflowErrorData); // eslint-disable-next-line @typescript-eslint/no-floating-promises
} else if (mode !== 'error' && workflowData.id !== undefined && workflowData.nodes.some((node) => node.type === ERROR_TRIGGER_TYPE)) { 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 }); Logger.verbose(`Start internal error workflow`, { executionId, workflowId: workflowData.id });
// If the workflow contains // If the workflow contains
// eslint-disable-next-line @typescript-eslint/no-floating-promises
WorkflowHelpers.executeErrorWorkflow(workflowData.id.toString(), workflowErrorData); WorkflowHelpers.executeErrorWorkflow(workflowData.id.toString(), workflowErrorData);
} }
} }
@ -114,23 +154,34 @@ function pruneExecutionData(this: WorkflowHooks): void {
date.setHours(date.getHours() - maxAge); date.setHours(date.getHours() - maxAge);
// date reformatting needed - see https://github.com/typeorm/typeorm/issues/2286 // 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); const utcDate = DateUtils.mixedDateToUtcDatetimeString(date);
// throttle just on success to allow for self healing on failure // throttle just on success to allow for self healing on failure
Db.collections.Execution!.delete({ stoppedAt: LessThanOrEqual(utcDate) }) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
.then(data => Db.collections
.Execution!.delete({ stoppedAt: LessThanOrEqual(utcDate) })
.then((data) =>
setTimeout(() => { setTimeout(() => {
throttling = false; throttling = false;
}, timeout * 1000) }, timeout * 1000),
).catch(error => { )
.catch((error) => {
throttling = false; 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 * Returns hook functions to push data to Editor-UI
* *
@ -145,13 +196,21 @@ function hookFunctionsPush(): IWorkflowExecuteHooks {
if (this.sessionId === undefined) { if (this.sessionId === undefined) {
return; 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(); const pushInstance = Push.getInstance();
pushInstance.send('nodeExecuteBefore', { pushInstance.send(
'nodeExecuteBefore',
{
executionId: this.executionId, executionId: this.executionId,
nodeName, nodeName,
}, this.sessionId); },
this.sessionId,
);
}, },
], ],
nodeExecuteAfter: [ nodeExecuteAfter: [
@ -160,37 +219,62 @@ function hookFunctionsPush(): IWorkflowExecuteHooks {
if (this.sessionId === undefined) { if (this.sessionId === undefined) {
return; 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(); const pushInstance = Push.getInstance();
pushInstance.send('nodeExecuteAfter', { pushInstance.send(
'nodeExecuteAfter',
{
executionId: this.executionId, executionId: this.executionId,
nodeName, nodeName,
data, data,
}, this.sessionId); },
this.sessionId,
);
}, },
], ],
workflowExecuteBefore: [ workflowExecuteBefore: [
async function (this: WorkflowHooks): Promise<void> { async function (this: WorkflowHooks): Promise<void> {
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 // Push data to session which started the workflow
if (this.sessionId === undefined) { if (this.sessionId === undefined) {
return; return;
} }
const pushInstance = Push.getInstance(); const pushInstance = Push.getInstance();
pushInstance.send('executionStarted', { pushInstance.send(
'executionStarted',
{
executionId: this.executionId, executionId: this.executionId,
mode: this.mode, mode: this.mode,
startedAt: new Date(), startedAt: new Date(),
retryOf: this.retryOf, retryOf: this.retryOf,
workflowId: this.workflowData.id, sessionId: this.sessionId as string, workflowId: this.workflowData.id,
sessionId: this.sessionId,
workflowName: this.workflowData.name, workflowName: this.workflowData.name,
}, this.sessionId); },
this.sessionId,
);
}, },
], ],
workflowExecuteAfter: [ workflowExecuteAfter: [
async function (this: WorkflowHooks, fullRunData: IRun, newStaticData: IDataObject): Promise<void> { async function (
Logger.debug(`Executing hook (hookFunctionsPush)`, { executionId: this.executionId, sessionId: this.sessionId, workflowId: this.workflowData.id }); this: WorkflowHooks,
fullRunData: IRun,
newStaticData: IDataObject,
): Promise<void> {
Logger.debug(`Executing hook (hookFunctionsPush)`, {
executionId: this.executionId,
sessionId: this.sessionId,
workflowId: this.workflowData.id,
});
// Push data to session which started the workflow // Push data to session which started the workflow
if (this.sessionId === undefined) { if (this.sessionId === undefined) {
return; return;
@ -211,7 +295,10 @@ function hookFunctionsPush(): IWorkflowExecuteHooks {
}; };
// Push data to editor-ui once workflow finished // 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 // TODO: Look at this again
const sendData: IPushDataExecutionFinished = { const sendData: IPushDataExecutionFinished = {
executionId: this.executionId, executionId: this.executionId,
@ -226,7 +313,6 @@ function hookFunctionsPush(): IWorkflowExecuteHooks {
}; };
} }
export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowExecuteHooks { export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowExecuteHooks {
const externalHooks = ExternalHooks(); const externalHooks = ExternalHooks();
@ -237,20 +323,32 @@ export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowEx
}, },
], ],
nodeExecuteAfter: [ nodeExecuteAfter: [
async function (nodeName: string, data: ITaskData, executionData: IRunExecutionData): Promise<void> { async function (
nodeName: string,
data: ITaskData,
executionData: IRunExecutionData,
): Promise<void> {
if (this.workflowData.settings !== undefined) { if (this.workflowData.settings !== undefined) {
if (this.workflowData.settings.saveExecutionProgress === false) { if (this.workflowData.settings.saveExecutionProgress === false) {
return; 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; return;
} }
} else if (!config.get('executions.saveExecutionProgress') as boolean) { } else if (!config.get('executions.saveExecutionProgress')) {
return; return;
} }
try { 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); const execution = await Db.collections.Execution!.findOne(this.executionId);
if (execution === undefined) { if (execution === undefined) {
@ -258,7 +356,8 @@ export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowEx
// This check is here mostly to make typescript happy. // This check is here mostly to make typescript happy.
return; return;
} }
const fullExecutionData: IExecutionResponse = ResponseHelper.unflattenExecutionData(execution); const fullExecutionData: IExecutionResponse =
ResponseHelper.unflattenExecutionData(execution);
if (fullExecutionData.finished) { if (fullExecutionData.finished) {
// We already received ´workflowExecuteAfter´ webhook, so this is just an async call // 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); 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) { } catch (err) {
// TODO: Improve in the future! // TODO: Improve in the future!
// Errors here might happen because of database access // Errors here might happen because of database access
// For busy machines, we may get "Database is locked" errors. // For busy machines, we may get "Database is locked" errors.
// We do this to prevent crashes and executions ending in `unknown` state. // 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 * Returns hook functions to save workflow execution and call error workflow
* *
@ -323,8 +432,15 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
nodeExecuteAfter: [], nodeExecuteAfter: [],
workflowExecuteBefore: [], workflowExecuteBefore: [],
workflowExecuteAfter: [ workflowExecuteAfter: [
async function (this: WorkflowHooks, fullRunData: IRun, newStaticData: IDataObject): Promise<void> { async function (
Logger.debug(`Executing hook (hookFunctionsSave)`, { executionId: this.executionId, workflowId: this.workflowData.id }); this: WorkflowHooks,
fullRunData: IRun,
newStaticData: IDataObject,
): Promise<void> {
Logger.debug(`Executing hook (hookFunctionsSave)`, {
executionId: this.executionId,
workflowId: this.workflowData.id,
});
// Prune old execution data // Prune old execution data
if (config.get('executions.pruneData')) { if (config.get('executions.pruneData')) {
@ -334,23 +450,37 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
const isManualMode = [this.mode, parentProcessMode].includes('manual'); const isManualMode = [this.mode, parentProcessMode].includes('manual');
try { 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 // Workflow is saved so update in database
try { try {
await WorkflowHelpers.saveStaticDataById(this.workflowData.id as string, newStaticData); await WorkflowHelpers.saveStaticDataById(
this.workflowData.id as string,
newStaticData,
);
} catch (e) { } 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; 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 // Apply to workflow override
saveManualExecutions = this.workflowData.settings.saveManualExecutions as boolean; 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 // 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); await Db.collections.Execution!.delete(this.executionId);
return; return;
} }
@ -359,17 +489,28 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
let saveDataErrorExecution = config.get('executions.saveDataOnError') as string; let saveDataErrorExecution = config.get('executions.saveDataOnError') as string;
let saveDataSuccessExecution = config.get('executions.saveDataOnSuccess') as string; let saveDataSuccessExecution = config.get('executions.saveDataOnSuccess') as string;
if (this.workflowData.settings !== undefined) { if (this.workflowData.settings !== undefined) {
saveDataErrorExecution = (this.workflowData.settings.saveDataErrorExecution as string) || saveDataErrorExecution; saveDataErrorExecution =
saveDataSuccessExecution = (this.workflowData.settings.saveDataSuccessExecution as string) || saveDataSuccessExecution; (this.workflowData.settings.saveDataErrorExecution as string) ||
saveDataErrorExecution;
saveDataSuccessExecution =
(this.workflowData.settings.saveDataSuccessExecution as string) ||
saveDataSuccessExecution;
} }
const workflowDidSucceed = !fullRunData.data.resultData.error; const workflowDidSucceed = !fullRunData.data.resultData.error;
if (workflowDidSucceed === true && saveDataSuccessExecution === 'none' || if (
workflowDidSucceed === false && saveDataErrorExecution === 'none' (workflowDidSucceed && saveDataSuccessExecution === 'none') ||
(!workflowDidSucceed && saveDataErrorExecution === 'none')
) { ) {
if (!fullRunData.waitTill) { if (!fullRunData.waitTill) {
if (!isManualMode) { 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 // Data is always saved, so we remove from database
await Db.collections.Execution!.delete(this.executionId); await Db.collections.Execution!.delete(this.executionId);
@ -391,7 +532,10 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
fullExecutionData.retryOf = this.retryOf.toString(); 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(); fullExecutionData.workflowId = this.workflowData.id.toString();
} }
@ -406,16 +550,27 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
const executionData = ResponseHelper.flattenExecutionData(fullExecutionData); const executionData = ResponseHelper.flattenExecutionData(fullExecutionData);
// Save the Execution in DB // 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 (fullRunData.finished === true && this.retryOf !== undefined) {
// If the retry was successful save the reference it on the original execution // 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!.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) { if (!isManualMode) {
executeErrorWorkflow(this.workflowData, fullRunData, this.mode, this.executionId, this.retryOf); executeErrorWorkflow(
this.workflowData,
fullRunData,
this.mode,
this.executionId,
this.retryOf,
);
} }
} catch (error) { } catch (error) {
Logger.error(`Failed saving execution data to DB on execution ID ${this.executionId}`, { Logger.error(`Failed saving execution data to DB on execution ID ${this.executionId}`, {
@ -425,7 +580,13 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
}); });
if (!isManualMode) { 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 * Returns hook functions to save workflow execution and call error workflow
* for running with queues. Manual executions should never run on queues as * for running with queues. Manual executions should never run on queues as
@ -447,20 +607,36 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
nodeExecuteAfter: [], nodeExecuteAfter: [],
workflowExecuteBefore: [], workflowExecuteBefore: [],
workflowExecuteAfter: [ workflowExecuteAfter: [
async function (this: WorkflowHooks, fullRunData: IRun, newStaticData: IDataObject): Promise<void> { async function (
this: WorkflowHooks,
fullRunData: IRun,
newStaticData: IDataObject,
): Promise<void> {
try { 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 // Workflow is saved so update in database
try { try {
await WorkflowHelpers.saveStaticDataById(this.workflowData.id as string, newStaticData); await WorkflowHelpers.saveStaticDataById(
this.workflowData.id as string,
newStaticData,
);
} catch (e) { } 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; const workflowDidSucceed = !fullRunData.data.resultData.error;
if (workflowDidSucceed === false) { if (!workflowDidSucceed) {
executeErrorWorkflow(this.workflowData, fullRunData, this.mode, undefined, this.retryOf); executeErrorWorkflow(
this.workflowData,
fullRunData,
this.mode,
undefined,
this.retryOf,
);
} }
const fullExecutionData: IExecutionDb = { const fullExecutionData: IExecutionDb = {
@ -477,18 +653,26 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
fullExecutionData.retryOf = this.retryOf.toString(); 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(); fullExecutionData.workflowId = this.workflowData.id.toString();
} }
const executionData = ResponseHelper.flattenExecutionData(fullExecutionData); const executionData = ResponseHelper.flattenExecutionData(fullExecutionData);
// Save the Execution in DB // 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 (fullRunData.finished === true && this.retryOf !== undefined) {
// If the retry was successful save the reference it on the original execution // 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) { } catch (error) {
executeErrorWorkflow(this.workflowData, fullRunData, this.mode, undefined, this.retryOf); 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<IWorkflowExecutionDataProcess> { export async function getRunData(
workflowData: IWorkflowBase,
inputData?: INodeExecutionData[],
): Promise<IWorkflowExecutionDataProcess> {
const mode = 'integrated'; const mode = 'integrated';
// Find Start-Node // Find Start-Node
const requiredNodeTypes = ['n8n-nodes-base.start']; const requiredNodeTypes = ['n8n-nodes-base.start'];
let startNode: INode | undefined; 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)) { if (requiredNodeTypes.includes(node.type)) {
startNode = node; startNode = node;
break; break;
@ -525,18 +713,15 @@ export async function getRunData(workflowData: IWorkflowBase, inputData?: INodeE
// Initialize the incoming data // Initialize the incoming data
const nodeExecutionStack: IExecuteData[] = []; const nodeExecutionStack: IExecuteData[] = [];
nodeExecutionStack.push( nodeExecutionStack.push({
{
node: startNode, node: startNode,
data: { data: {
main: [inputData], main: [inputData],
}, },
} });
);
const runExecutionData: IRunExecutionData = { const runExecutionData: IRunExecutionData = {
startData: { startData: {},
},
resultData: { resultData: {
runData: {}, runData: {},
}, },
@ -557,13 +742,14 @@ export async function getRunData(workflowData: IWorkflowBase, inputData?: INodeE
return runData; return runData;
} }
export async function getWorkflowData(workflowInfo: IExecuteWorkflowInfo): Promise<IWorkflowBase> { export async function getWorkflowData(workflowInfo: IExecuteWorkflowInfo): Promise<IWorkflowBase> {
if (workflowInfo.id === undefined && workflowInfo.code === undefined) { 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 // The first time executeWorkflow gets called the Database has
// to get initialized first // to get initialized first
await Db.init(); await Db.init();
@ -571,7 +757,7 @@ export async function getWorkflowData(workflowInfo: IExecuteWorkflowInfo): Promi
let workflowData: IWorkflowBase | undefined; let workflowData: IWorkflowBase | undefined;
if (workflowInfo.id !== undefined) { if (workflowInfo.id !== undefined) {
workflowData = await Db.collections!.Workflow!.findOne(workflowInfo.id); workflowData = await Db.collections.Workflow!.findOne(workflowInfo.id);
if (workflowData === undefined) { if (workflowData === undefined) {
throw new Error(`The workflow with the id "${workflowInfo.id}" does not exist.`); 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!; return workflowData!;
} }
/** /**
* Executes the workflow with the given ID * Executes the workflow with the given ID
* *
@ -592,25 +777,45 @@ export async function getWorkflowData(workflowInfo: IExecuteWorkflowInfo): Promi
* @param {INodeExecutionData[]} [inputData] * @param {INodeExecutionData[]} [inputData]
* @returns {(Promise<Array<INodeExecutionData[] | null>>)} * @returns {(Promise<Array<INodeExecutionData[] | null>>)}
*/ */
export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additionalData: IWorkflowExecuteAdditionalData, inputData?: INodeExecutionData[], parentExecutionId?: string, loadedWorkflowData?: IWorkflowBase, loadedRunData?: IWorkflowExecutionDataProcess): Promise<Array<INodeExecutionData[] | null> | IWorkflowExecuteProcess> { export async function executeWorkflow(
workflowInfo: IExecuteWorkflowInfo,
additionalData: IWorkflowExecuteAdditionalData,
inputData?: INodeExecutionData[],
parentExecutionId?: string,
loadedWorkflowData?: IWorkflowBase,
loadedRunData?: IWorkflowExecutionDataProcess,
): Promise<Array<INodeExecutionData[] | null> | IWorkflowExecuteProcess> {
const externalHooks = ExternalHooks(); const externalHooks = ExternalHooks();
await externalHooks.init(); await externalHooks.init();
const nodeTypes = NodeTypes(); 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 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; let executionId;
if (parentExecutionId !== undefined) { if (parentExecutionId !== undefined) {
executionId = parentExecutionId; executionId = parentExecutionId;
} else { } else {
executionId = parentExecutionId !== undefined ? parentExecutionId : await ActiveExecutions.getInstance().add(runData); executionId =
parentExecutionId !== undefined
? parentExecutionId
: await ActiveExecutions.getInstance().add(runData);
} }
let data; let data;
@ -618,18 +823,29 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi
// Create new additionalData to have different workflow loaded and to call // Create new additionalData to have different workflow loaded and to call
// different webooks // different webooks
const additionalDataIntegrated = await getBase(); 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 // Make sure we pass on the original executeWorkflow function we received
// This one already contains changes to talk to parent process // This one already contains changes to talk to parent process
// and get executionID from `activeExecutions` running on main process // and get executionID from `activeExecutions` running on main process
additionalDataIntegrated.executeWorkflow = additionalData.executeWorkflow; additionalDataIntegrated.executeWorkflow = additionalData.executeWorkflow;
let subworkflowTimeout = additionalData.executionTimeoutTimestamp; 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 // 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 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. // 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; additionalDataIntegrated.executionTimeoutTimestamp = subworkflowTimeout;
@ -637,7 +853,11 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi
const runExecutionData = runData.executionData as IRunExecutionData; const runExecutionData = runData.executionData as IRunExecutionData;
// Execute the workflow // Execute the workflow
const workflowExecute = new WorkflowExecute(additionalDataIntegrated, runData.executionMode, runExecutionData); const workflowExecute = new WorkflowExecute(
additionalDataIntegrated,
runData.executionMode,
runExecutionData,
);
if (parentExecutionId !== undefined) { if (parentExecutionId !== undefined) {
// Must be changed to become typed // Must be changed to become typed
return { return {
@ -678,7 +898,7 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi
await Db.collections.Execution!.update(executionId, executionData as IExecutionFlattedDb); await Db.collections.Execution!.update(executionId, executionData as IExecutionFlattedDb);
throw { throw {
...error, ...error,
stack: error!.stack, stack: error.stack,
}; };
} }
@ -690,19 +910,19 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi
await ActiveExecutions.getInstance().remove(executionId, data); await ActiveExecutions.getInstance().remove(executionId, data);
const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data); const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data);
return returnData!.data!.main; return returnData!.data!.main;
} else { }
await ActiveExecutions.getInstance().remove(executionId, data); await ActiveExecutions.getInstance().remove(executionId, data);
// Workflow did fail // Workflow did fail
const { error } = data.data.resultData; const { error } = data.data.resultData;
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw { throw {
...error, ...error,
stack: error!.stack, stack: error!.stack,
}; };
}
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function sendMessageToUI(source: string, message: any) { // tslint:disable-line:no-any export function sendMessageToUI(source: string, message: any) {
if (this.sessionId === undefined) { if (this.sessionId === undefined) {
return; return;
} }
@ -710,16 +930,19 @@ export function sendMessageToUI(source: string, message: any) { // tslint:disabl
// Push data to session which started workflow // Push data to session which started workflow
try { try {
const pushInstance = Push.getInstance(); const pushInstance = Push.getInstance();
pushInstance.send('sendConsoleMessage', { pushInstance.send(
'sendConsoleMessage',
{
source: `Node: "${source}"`, source: `Node: "${source}"`,
message, message,
}, this.sessionId); },
this.sessionId,
);
} catch (error) { } catch (error) {
Logger.warn(`There was a problem sending messsage to UI: ${error.message}`); Logger.warn(`There was a problem sending messsage to UI: ${error.message}`);
} }
} }
/** /**
* Returns the base additional data without webhooks * Returns the base additional data without webhooks
* *
@ -728,13 +951,16 @@ export function sendMessageToUI(source: string, message: any) { // tslint:disabl
* @param {INodeParameters} currentNodeParameters * @param {INodeParameters} currentNodeParameters
* @returns {Promise<IWorkflowExecuteAdditionalData>} * @returns {Promise<IWorkflowExecuteAdditionalData>}
*/ */
export async function getBase(currentNodeParameters?: INodeParameters, executionTimeoutTimestamp?: number): Promise<IWorkflowExecuteAdditionalData> { export async function getBase(
currentNodeParameters?: INodeParameters,
executionTimeoutTimestamp?: number,
): Promise<IWorkflowExecuteAdditionalData> {
const urlBaseWebhook = WebhookHelpers.getWebhookBaseUrl(); const urlBaseWebhook = WebhookHelpers.getWebhookBaseUrl();
const timezone = config.get('generic.timezone') as string; const timezone = config.get('generic.timezone') as string;
const webhookBaseUrl = urlBaseWebhook + config.get('endpoints.webhook') as string; const webhookBaseUrl = urlBaseWebhook + config.get('endpoints.webhook');
const webhookWaitingBaseUrl = urlBaseWebhook + config.get('endpoints.webhookWaiting') as string; const webhookWaitingBaseUrl = urlBaseWebhook + config.get('endpoints.webhookWaiting');
const webhookTestBaseUrl = urlBaseWebhook + config.get('endpoints.webhookTest') as string; const webhookTestBaseUrl = urlBaseWebhook + config.get('endpoints.webhookTest');
const encryptionKey = await UserSettings.getEncryptionKey(); const encryptionKey = await UserSettings.getEncryptionKey();
if (encryptionKey === undefined) { if (encryptionKey === undefined) {
@ -745,7 +971,7 @@ export async function getBase(currentNodeParameters?: INodeParameters, execution
credentialsHelper: new CredentialsHelper(encryptionKey), credentialsHelper: new CredentialsHelper(encryptionKey),
encryptionKey, encryptionKey,
executeWorkflow, executeWorkflow,
restApiUrl: urlBaseWebhook + config.get('endpoints.rest') as string, restApiUrl: urlBaseWebhook + config.get('endpoints.rest'),
timezone, timezone,
webhookBaseUrl, webhookBaseUrl,
webhookWaitingBaseUrl, webhookWaitingBaseUrl,
@ -755,12 +981,16 @@ export async function getBase(currentNodeParameters?: INodeParameters, execution
}; };
} }
/** /**
* Returns WorkflowHooks instance for running integrated workflows * Returns WorkflowHooks instance for running integrated workflows
* (Workflows which get started inside of another workflow) * (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 || {}; optionalParameters = optionalParameters || {};
const hookFunctions = hookFunctionsSave(optionalParameters.parentProcessMode); const hookFunctions = hookFunctionsSave(optionalParameters.parentProcessMode);
const preExecuteFunctions = hookFunctionsPreExecute(optionalParameters.parentProcessMode); const preExecuteFunctions = hookFunctionsPreExecute(optionalParameters.parentProcessMode);
@ -777,7 +1007,12 @@ export function getWorkflowHooksIntegrated(mode: WorkflowExecuteMode, executionI
* Returns WorkflowHooks instance for running integrated workflows * Returns WorkflowHooks instance for running integrated workflows
* (Workflows which get started inside of another workflow) * (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 || {}; optionalParameters = optionalParameters || {};
const hookFunctions = hookFunctionsSaveWorker(); const hookFunctions = hookFunctionsSaveWorker();
const preExecuteFunctions = hookFunctionsPreExecute(optionalParameters.parentProcessMode); 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 * 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 || {}; optionalParameters = optionalParameters || {};
const hookFunctions = hookFunctionsPush(); const hookFunctions = hookFunctionsPush();
const preExecuteFunctions = hookFunctionsPreExecute(optionalParameters.parentProcessMode); const preExecuteFunctions = hookFunctionsPreExecute(optionalParameters.parentProcessMode);
@ -812,7 +1052,6 @@ export function getWorkflowHooksWorkerMain(mode: WorkflowExecuteMode, executionI
return new WorkflowHooks(hookFunctions, mode, executionId, workflowData, optionalParameters); return new WorkflowHooks(hookFunctions, mode, executionId, workflowData, optionalParameters);
} }
/** /**
* Returns WorkflowHooks instance for running the main workflow * Returns WorkflowHooks instance for running the main workflow
* *
@ -821,7 +1060,11 @@ export function getWorkflowHooksWorkerMain(mode: WorkflowExecuteMode, executionI
* @param {string} executionId * @param {string} executionId
* @returns {WorkflowHooks} * @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 hookFunctions = hookFunctionsSave();
const pushFunctions = hookFunctionsPush(); const pushFunctions = hookFunctionsPush();
for (const key of Object.keys(pushFunctions)) { 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,
});
} }

View file

@ -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 { import {
CredentialTypes, CredentialTypes,
Db, Db,
@ -7,28 +28,17 @@ import {
IWorkflowExecutionDataProcess, IWorkflowExecutionDataProcess,
NodeTypes, NodeTypes,
ResponseHelper, ResponseHelper,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
WorkflowCredentials, WorkflowCredentials,
WorkflowRunner, WorkflowRunner,
} from './'; } from '.';
import {
IDataObject,
IExecuteData,
INode,
IRun,
IRunExecutionData,
ITaskData,
IWorkflowCredentials,
LoggerProxy as Logger,
Workflow,} from 'n8n-workflow';
import * as config from '../config'; import * as config from '../config';
// eslint-disable-next-line import/no-cycle
import { WorkflowEntity } from './databases/entities/WorkflowEntity'; import { WorkflowEntity } from './databases/entities/WorkflowEntity';
import { validate } from 'class-validator';
const ERROR_TRIGGER_TYPE = config.get('nodes.errorTriggerType') as string; const ERROR_TRIGGER_TYPE = config.get('nodes.errorTriggerType') as string;
/** /**
* Returns the data of the last executed node * 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)} * @returns {(ITaskData | undefined)}
*/ */
export function getDataLastExecutedNodeData(inputData: IRun): ITaskData | undefined { export function getDataLastExecutedNodeData(inputData: IRun): ITaskData | undefined {
const runData = inputData.data.resultData.runData; const { runData } = inputData.data.resultData;
const lastNodeExecuted = inputData.data.resultData.lastNodeExecuted; const { lastNodeExecuted } = inputData.data.resultData;
if (lastNodeExecuted === undefined) { if (lastNodeExecuted === undefined) {
return undefined; return undefined;
@ -51,8 +61,6 @@ export function getDataLastExecutedNodeData(inputData: IRun): ITaskData | undefi
return runData[lastNodeExecuted][runData[lastNodeExecuted].length - 1]; return runData[lastNodeExecuted][runData[lastNodeExecuted].length - 1];
} }
/** /**
* Returns if the given id is a valid workflow id * Returns if the given id is a valid workflow id
* *
@ -60,20 +68,18 @@ export function getDataLastExecutedNodeData(inputData: IRun): ITaskData | undefi
* @returns {boolean} * @returns {boolean}
* @memberof App * @memberof App
*/ */
export function isWorkflowIdValid (id: string | null | undefined | number): boolean { export function isWorkflowIdValid(id: string | null | undefined | number): boolean {
if (typeof id === 'string') { if (typeof id === 'string') {
id = parseInt(id, 10); id = parseInt(id, 10);
} }
// eslint-disable-next-line no-restricted-globals
if (isNaN(id as number)) { if (isNaN(id as number)) {
return false; return false;
} }
return true; return true;
} }
/** /**
* Executes the error workflow * Executes the error workflow
* *
@ -82,21 +88,37 @@ export function isWorkflowIdValid (id: string | null | undefined | number): bool
* @param {IWorkflowErrorData} workflowErrorData The error data * @param {IWorkflowErrorData} workflowErrorData The error data
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
export async function executeErrorWorkflow(workflowId: string, workflowErrorData: IWorkflowErrorData): Promise<void> { export async function executeErrorWorkflow(
workflowId: string,
workflowErrorData: IWorkflowErrorData,
): Promise<void> {
// Wrap everything in try/catch to make sure that no errors bubble up and all get caught here // Wrap everything in try/catch to make sure that no errors bubble up and all get caught here
try { try {
const workflowData = await Db.collections.Workflow!.findOne({ id: Number(workflowId) }); const workflowData = await Db.collections.Workflow!.findOne({ id: Number(workflowId) });
if (workflowData === undefined) { if (workflowData === undefined) {
// The error workflow could not be found // 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; return;
} }
const executionMode = 'error'; const executionMode = 'error';
const nodeTypes = NodeTypes(); 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 node: INode;
let workflowStartNode: INode | undefined; let workflowStartNode: INode | undefined;
@ -108,7 +130,9 @@ export async function executeErrorWorkflow(workflowId: string, workflowErrorData
} }
if (workflowStartNode === undefined) { 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; return;
} }
@ -116,8 +140,7 @@ export async function executeErrorWorkflow(workflowId: string, workflowErrorData
// Initialize the data of the webhook node // Initialize the data of the webhook node
const nodeExecutionStack: IExecuteData[] = []; const nodeExecutionStack: IExecuteData[] = [];
nodeExecutionStack.push( nodeExecutionStack.push({
{
node: workflowStartNode, node: workflowStartNode,
data: { data: {
main: [ main: [
@ -128,12 +151,10 @@ export async function executeErrorWorkflow(workflowId: string, workflowErrorData
], ],
], ],
}, },
} });
);
const runExecutionData: IRunExecutionData = { const runExecutionData: IRunExecutionData = {
startData: { startData: {},
},
resultData: { resultData: {
runData: {}, runData: {},
}, },
@ -153,12 +174,13 @@ export async function executeErrorWorkflow(workflowId: string, workflowErrorData
const workflowRunner = new WorkflowRunner(); const workflowRunner = new WorkflowRunner();
await workflowRunner.run(runData); await workflowRunner.run(runData);
} catch (error) { } 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 * Returns all the defined NodeTypes
* *
@ -185,8 +207,6 @@ export function getAllNodeTypeData(): ITransferNodeTypes {
return returnData; return returnData;
} }
/** /**
* Returns the data of the node types that are needed * Returns the data of the node types that are needed
* to execute the given nodes * to execute the given nodes
@ -199,6 +219,7 @@ export function getNodeTypeData(nodes: INode[]): ITransferNodeTypes {
const nodeTypes = NodeTypes(); const nodeTypes = NodeTypes();
// Check which node-types have to be loaded // Check which node-types have to be loaded
// eslint-disable-next-line @typescript-eslint/no-use-before-define
const neededNodeTypes = getNeededNodeTypes(nodes); const neededNodeTypes = getNeededNodeTypes(nodes);
// Get all the data of the needed node types that they // Get all the data of the needed node types that they
@ -218,8 +239,6 @@ export function getNodeTypeData(nodes: INode[]): ITransferNodeTypes {
return returnData; return returnData;
} }
/** /**
* Returns the credentials data of the given type and its parent types * Returns the credentials data of the given type and its parent types
* it extends * it extends
@ -251,8 +270,6 @@ export function getCredentialsDataWithParents(type: string): ICredentialsTypeDat
return credentialTypeData; return credentialTypeData;
} }
/** /**
* Returns all the credentialTypes which are needed to resolve * Returns all the credentialTypes which are needed to resolve
* the given workflow credentials * the given workflow credentials
@ -262,14 +279,13 @@ export function getCredentialsDataWithParents(type: string): ICredentialsTypeDat
* @returns {ICredentialsTypeData} * @returns {ICredentialsTypeData}
*/ */
export function getCredentialsDataByNodes(nodes: INode[]): ICredentialsTypeData { export function getCredentialsDataByNodes(nodes: INode[]): ICredentialsTypeData {
const credentialTypeData: ICredentialsTypeData = {}; const credentialTypeData: ICredentialsTypeData = {};
for (const node of nodes) { for (const node of nodes) {
const credentialsUsedByThisNode = node.credentials; const credentialsUsedByThisNode = node.credentials;
if (credentialsUsedByThisNode) { if (credentialsUsedByThisNode) {
// const credentialTypesUsedByThisNode = Object.keys(credentialsUsedByThisNode!); // const credentialTypesUsedByThisNode = Object.keys(credentialsUsedByThisNode!);
for (const credentialType of Object.keys(credentialsUsedByThisNode!)) { for (const credentialType of Object.keys(credentialsUsedByThisNode)) {
if (credentialTypeData[credentialType] !== undefined) { if (credentialTypeData[credentialType] !== undefined) {
continue; continue;
} }
@ -277,14 +293,11 @@ export function getCredentialsDataByNodes(nodes: INode[]): ICredentialsTypeData
Object.assign(credentialTypeData, getCredentialsDataWithParents(credentialType)); Object.assign(credentialTypeData, getCredentialsDataWithParents(credentialType));
} }
} }
} }
return credentialTypeData; return credentialTypeData;
} }
/** /**
* Returns the names of the NodeTypes which are are needed * Returns the names of the NodeTypes which are are needed
* to execute the gives nodes * to execute the gives nodes
@ -305,8 +318,6 @@ export function getNeededNodeTypes(nodes: INode[]): string[] {
return neededNodeTypes; return neededNodeTypes;
} }
/** /**
* Saves the static data if it changed * Saves the static data if it changed
* *
@ -314,23 +325,25 @@ export function getNeededNodeTypes(nodes: INode[]): string[] {
* @param {Workflow} workflow * @param {Workflow} workflow
* @returns {Promise <void>} * @returns {Promise <void>}
*/ */
export async function saveStaticData(workflow: Workflow): Promise <void> { export async function saveStaticData(workflow: Workflow): Promise<void> {
if (workflow.staticData.__dataChanged === true) { if (workflow.staticData.__dataChanged === true) {
// Static data of workflow changed and so has to be saved // 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 // Workflow is saved so update in database
try { try {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
await saveStaticDataById(workflow.id!, workflow.staticData); await saveStaticDataById(workflow.id!, workflow.staticData);
workflow.staticData.__dataChanged = false; workflow.staticData.__dataChanged = false;
} catch (e) { } 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 * Saves the given static data on workflow
* *
@ -339,15 +352,15 @@ export async function saveStaticData(workflow: Workflow): Promise <void> {
* @param {IDataObject} newStaticData The static data to save * @param {IDataObject} newStaticData The static data to save
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
export async function saveStaticDataById(workflowId: string | number, newStaticData: IDataObject): Promise<void> { export async function saveStaticDataById(
await Db.collections.Workflow! workflowId: string | number,
.update(workflowId, { newStaticData: IDataObject,
): Promise<void> {
await Db.collections.Workflow!.update(workflowId, {
staticData: newStaticData, staticData: newStaticData,
}); });
} }
/** /**
* Returns the static data of workflow * 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 * @param {(string | number)} workflowId The id of the workflow to get static data of
* @returns * @returns
*/ */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export async function getStaticDataById(workflowId: string | number) { export async function getStaticDataById(workflowId: string | number) {
const workflowData = await Db.collections.Workflow! const workflowData = await Db.collections.Workflow!.findOne(workflowId, {
.findOne(workflowId, { select: ['staticData']}); select: ['staticData'],
});
if (workflowData === undefined) { if (workflowData === undefined) {
return {}; return {};
} }
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
return workflowData.staticData || {}; return workflowData.staticData || {};
} }
// TODO: Deduplicate `validateWorkflow` and `throwDuplicateEntryError` with TagHelpers? // TODO: Deduplicate `validateWorkflow` and `throwDuplicateEntryError` with TagHelpers?
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export async function validateWorkflow(newWorkflow: WorkflowEntity) { export async function validateWorkflow(newWorkflow: WorkflowEntity) {
const errors = await validate(newWorkflow); 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) { export function throwDuplicateEntryError(error: Error) {
const errorMessage = error.message.toLowerCase(); const errorMessage = error.message.toLowerCase();
if (errorMessage.includes('unique') || errorMessage.includes('duplicate')) { 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); throw new ResponseHelper.ResponseError(errorMessage, undefined, 400);
@ -391,6 +412,5 @@ export type WorkflowNameRequest = Express.Request & {
query: { query: {
name?: string; name?: string;
offset?: string; offset?: string;
} };
}; };

View file

@ -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 { import {
ActiveExecutions, ActiveExecutions,
CredentialsOverwrites, CredentialsOverwrites,
@ -20,38 +54,17 @@ import {
ResponseHelper, ResponseHelper,
WorkflowExecuteAdditionalData, WorkflowExecuteAdditionalData,
WorkflowHelpers, WorkflowHelpers,
} from './'; } 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';
import * as Queue from './Queue'; import * as Queue from './Queue';
export class WorkflowRunner { export class WorkflowRunner {
activeExecutions: ActiveExecutions.ActiveExecutions; activeExecutions: ActiveExecutions.ActiveExecutions;
credentialsOverwrites: ICredentialsOverwrite;
push: Push.Push;
jobQueue: Bull.Queue;
credentialsOverwrites: ICredentialsOverwrite;
push: Push.Push;
jobQueue: Bull.Queue;
constructor() { constructor() {
this.push = Push.getInstance(); this.push = Push.getInstance();
@ -65,7 +78,6 @@ export class WorkflowRunner {
} }
} }
/** /**
* The process did send a hook message so execute the appropiate hook * The process did send a hook message so execute the appropiate hook
* *
@ -74,10 +86,10 @@ export class WorkflowRunner {
* @memberof WorkflowRunner * @memberof WorkflowRunner
*/ */
processHookMessage(workflowHooks: WorkflowHooks, hookData: IProcessMessageDataHook) { processHookMessage(workflowHooks: WorkflowHooks, hookData: IProcessMessageDataHook) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
workflowHooks.executeHookFunctions(hookData.hook, hookData.parameters); workflowHooks.executeHookFunctions(hookData.hook, hookData.parameters);
} }
/** /**
* The process did error * The process did error
* *
@ -87,7 +99,13 @@ export class WorkflowRunner {
* @param {string} executionId * @param {string} executionId
* @memberof WorkflowRunner * @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 = { const fullRunData: IRun = {
data: { data: {
resultData: { resultData: {
@ -123,7 +141,12 @@ export class WorkflowRunner {
* @returns {Promise<string>} * @returns {Promise<string>}
* @memberof WorkflowRunner * @memberof WorkflowRunner
*/ */
async run(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean, realtime?: boolean, executionId?: string): Promise<string> { async run(
data: IWorkflowExecutionDataProcess,
loadStaticData?: boolean,
realtime?: boolean,
executionId?: string,
): Promise<string> {
const executionsProcess = config.get('executions.process') as string; const executionsProcess = config.get('executions.process') as string;
const executionsMode = config.get('executions.mode') as string; const executionsMode = config.get('executions.mode') as string;
@ -139,11 +162,12 @@ export class WorkflowRunner {
const externalHooks = ExternalHooks(); const externalHooks = ExternalHooks();
if (externalHooks.exists('workflow.postExecute')) { if (externalHooks.exists('workflow.postExecute')) {
this.activeExecutions.getPostExecutePromise(executionId) this.activeExecutions
.getPostExecutePromise(executionId)
.then(async (executionData) => { .then(async (executionData) => {
await externalHooks.run('workflow.postExecute', [executionData, data.workflowData]); await externalHooks.run('workflow.postExecute', [executionData, data.workflowData]);
}) })
.catch(error => { .catch((error) => {
console.error('There was a problem running hook "workflow.postExecute"', error); console.error('There was a problem running hook "workflow.postExecute"', error);
}); });
} }
@ -151,7 +175,6 @@ export class WorkflowRunner {
return executionId; return executionId;
} }
/** /**
* Run the workflow in current process * Run the workflow in current process
* *
@ -161,9 +184,15 @@ export class WorkflowRunner {
* @returns {Promise<string>} * @returns {Promise<string>}
* @memberof WorkflowRunner * @memberof WorkflowRunner
*/ */
async runMainProcess(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean, restartExecutionId?: string): Promise<string> { async runMainProcess(
data: IWorkflowExecutionDataProcess,
loadStaticData?: boolean,
restartExecutionId?: string,
): Promise<string> {
if (loadStaticData === true && data.workflowData.id) { 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(); const nodeTypes = NodeTypes();
@ -174,67 +203,120 @@ export class WorkflowRunner {
let executionTimeout: NodeJS.Timeout; let executionTimeout: NodeJS.Timeout;
let workflowTimeout = config.get('executions.timeout') as number; // initialize with default let workflowTimeout = config.get('executions.timeout') as number; // initialize with default
if (data.workflowData.settings && data.workflowData.settings.executionTimeout) { 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) { if (workflowTimeout > 0) {
workflowTimeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number); 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 workflow = new Workflow({
const additionalData = await WorkflowExecuteAdditionalData.getBase(undefined, workflowTimeout <= 0 ? undefined : Date.now() + workflowTimeout * 1000); 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 // 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; 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<IRun>; let workflowExecution: PCancelable<IRun>;
try { try {
Logger.verbose(`Execution for workflow ${data.workflowData.name} was assigned id ${executionId}`, { executionId }); Logger.verbose(
additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId, true); `Execution for workflow ${data.workflowData.name} was assigned id ${executionId}`,
additionalData.sendMessageToUI = WorkflowExecuteAdditionalData.sendMessageToUI.bind({sessionId: data.sessionId}); { executionId },
);
additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(
data,
executionId,
true,
);
additionalData.sendMessageToUI = WorkflowExecuteAdditionalData.sendMessageToUI.bind({
sessionId: data.sessionId,
});
if (data.executionData !== undefined) { if (data.executionData !== undefined) {
Logger.debug(`Execution ID ${executionId} had Execution data. Running with payload.`, {executionId}); Logger.debug(`Execution ID ${executionId} had Execution data. Running with payload.`, {
const workflowExecute = new WorkflowExecute(additionalData, data.executionMode, data.executionData); executionId,
});
const workflowExecute = new WorkflowExecute(
additionalData,
data.executionMode,
data.executionData,
);
workflowExecution = workflowExecute.processRunExecutionData(workflow); workflowExecution = workflowExecute.processRunExecutionData(workflow);
} else if (data.runData === undefined || data.startNodes === undefined || data.startNodes.length === 0 || data.destinationNode === undefined) { } else if (
Logger.debug(`Execution ID ${executionId} will run executing all nodes.`, {executionId}); 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 // Execute all nodes
// Can execute without webhook so go on // Can execute without webhook so go on
const workflowExecute = new WorkflowExecute(additionalData, data.executionMode); const workflowExecute = new WorkflowExecute(additionalData, data.executionMode);
workflowExecution = workflowExecute.run(workflow, undefined, data.destinationNode); workflowExecution = workflowExecute.run(workflow, undefined, data.destinationNode);
} else { } 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 // Execute only the nodes between start and destination nodes
const workflowExecute = new WorkflowExecute(additionalData, data.executionMode); 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); this.activeExecutions.attachWorkflowExecution(executionId, workflowExecution);
if (workflowTimeout > 0) { 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(() => { executionTimeout = setTimeout(() => {
this.activeExecutions.stopExecution(executionId, 'timeout'); this.activeExecutions.stopExecution(executionId, 'timeout');
}, timeout); }, timeout);
} }
workflowExecution.then((fullRunData) => { workflowExecution
.then((fullRunData) => {
clearTimeout(executionTimeout); clearTimeout(executionTimeout);
if (workflowExecution.isCanceled) { if (workflowExecution.isCanceled) {
fullRunData.finished = false; fullRunData.finished = false;
} }
this.activeExecutions.remove(executionId, fullRunData); this.activeExecutions.remove(executionId, fullRunData);
}).catch((error) => { })
this.processError(error, new Date(), data.executionMode, executionId, additionalData.hooks); .catch((error) => {
this.processError(
error,
new Date(),
data.executionMode,
executionId,
additionalData.hooks,
);
}); });
} catch (error) { } 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; throw error;
} }
@ -242,8 +324,12 @@ export class WorkflowRunner {
return executionId; return executionId;
} }
async runBull(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean, realtime?: boolean, restartExecutionId?: string): Promise<string> { async runBull(
data: IWorkflowExecutionDataProcess,
loadStaticData?: boolean,
realtime?: boolean,
restartExecutionId?: string,
): Promise<string> {
// TODO: If "loadStaticData" is set to true it has to load data new on worker // TODO: If "loadStaticData" is set to true it has to load data new on worker
// Register the active execution // Register the active execution
@ -271,9 +357,14 @@ export class WorkflowRunner {
try { try {
job = await this.jobQueue.add(jobData, jobOptions); 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 // Normally also workflow should be supplied here but as it only used for sending
// data to editor-UI is not needed. // data to editor-UI is not needed.
@ -281,19 +372,30 @@ export class WorkflowRunner {
} catch (error) { } catch (error) {
// We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the // We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the
// "workflowExecuteAfter" which we require. // "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); await this.processError(error, new Date(), data.executionMode, executionId, hooks);
throw error; throw error;
} }
const workflowExecution: PCancelable<IRun> = new PCancelable(async (resolve, reject, onCancel) => { const workflowExecution: PCancelable<IRun> = new PCancelable(
async (resolve, reject, onCancel) => {
onCancel.shouldReject = false; onCancel.shouldReject = false;
onCancel(async () => { onCancel(async () => {
await Queue.getInstance().stopJob(job); await Queue.getInstance().stopJob(job);
// We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the // We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the
// "workflowExecuteAfter" which we require. // "workflowExecuteAfter" which we require.
const hooksWorker = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(data.executionMode, executionId, data.workflowData, { retryOf: data.retryOf ? data.retryOf.toString() : undefined }); 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!'); const error = new WorkflowOperationError('Workflow-Execution has been canceled!');
await this.processError(error, new Date(), data.executionMode, executionId, hooksWorker); await this.processError(error, new Date(), data.executionMode, executionId, hooksWorker);
@ -309,7 +411,7 @@ export class WorkflowRunner {
let clearWatchdogInterval; let clearWatchdogInterval;
if (queueRecoveryInterval > 0) { if (queueRecoveryInterval > 0) {
/************************************************* /** ***********************************************
* Long explanation about what this solves: * * Long explanation about what this solves: *
* This only happens in a very specific scenario * * This only happens in a very specific scenario *
* when Redis crashes and recovers shortly * * when Redis crashes and recovers shortly *
@ -320,7 +422,7 @@ export class WorkflowRunner {
* the queue that allows us to identify that the * * the queue that allows us to identify that the *
* execution finished and get information from * * execution finished and get information from *
* the database. * * the database. *
*************************************************/ ************************************************ */
let watchDogInterval: NodeJS.Timeout | undefined; let watchDogInterval: NodeJS.Timeout | undefined;
const watchDog: Promise<object> = new Promise((res) => { const watchDog: Promise<object> = new Promise((res) => {
@ -329,7 +431,7 @@ export class WorkflowRunner {
// When null means job is finished (not found in queue) // When null means job is finished (not found in queue)
if (currentJob === null) { if (currentJob === null) {
// Mimic worker's success message // Mimic worker's success message
res({success: true}); res({ success: true });
} }
}, queueRecoveryInterval * 1000); }, queueRecoveryInterval * 1000);
}); });
@ -352,7 +454,12 @@ export class WorkflowRunner {
} catch (error) { } catch (error) {
// We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the // We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the
// "workflowExecuteAfter" which we require. // "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 },
);
Logger.error(`Problem with execution ${executionId}: ${error.message}. Aborting.`); Logger.error(`Problem with execution ${executionId}: ${error.message}. Aborting.`);
if (clearWatchdogInterval !== undefined) { if (clearWatchdogInterval !== undefined) {
clearWatchdogInterval(); clearWatchdogInterval();
@ -362,8 +469,10 @@ export class WorkflowRunner {
reject(error); reject(error);
} }
const executionDb = await Db.collections.Execution!.findOne(executionId) as IExecutionFlattedDb; const executionDb = (await Db.collections.Execution!.findOne(
const fullExecutionData = ResponseHelper.unflattenExecutionData(executionDb) as IExecutionResponse; executionId,
)) as IExecutionFlattedDb;
const fullExecutionData = ResponseHelper.unflattenExecutionData(executionDb);
const runData = { const runData = {
data: fullExecutionData.data, data: fullExecutionData.data,
finished: fullExecutionData.finished, finished: fullExecutionData.finished,
@ -382,29 +491,35 @@ export class WorkflowRunner {
let saveDataErrorExecution = config.get('executions.saveDataOnError') as string; let saveDataErrorExecution = config.get('executions.saveDataOnError') as string;
let saveDataSuccessExecution = config.get('executions.saveDataOnSuccess') as string; let saveDataSuccessExecution = config.get('executions.saveDataOnSuccess') as string;
if (data.workflowData.settings !== undefined) { if (data.workflowData.settings !== undefined) {
saveDataErrorExecution = (data.workflowData.settings.saveDataErrorExecution as string) || saveDataErrorExecution; saveDataErrorExecution =
saveDataSuccessExecution = (data.workflowData.settings.saveDataSuccessExecution as string) || saveDataSuccessExecution; (data.workflowData.settings.saveDataErrorExecution as string) ||
saveDataErrorExecution;
saveDataSuccessExecution =
(data.workflowData.settings.saveDataSuccessExecution as string) ||
saveDataSuccessExecution;
} }
const workflowDidSucceed = !runData.data.resultData.error; const workflowDidSucceed = !runData.data.resultData.error;
if (workflowDidSucceed === true && saveDataSuccessExecution === 'none' || if (
workflowDidSucceed === false && saveDataErrorExecution === 'none' (workflowDidSucceed && saveDataSuccessExecution === 'none') ||
(!workflowDidSucceed && saveDataErrorExecution === 'none')
) { ) {
await Db.collections.Execution!.delete(executionId); await Db.collections.Execution!.delete(executionId);
} }
// eslint-disable-next-line id-denylist
} catch (err) { } catch (err) {
// We don't want errors here to crash n8n. Just log and proceed. // We don't want errors here to crash n8n. Just log and proceed.
console.log('Error removing saved execution from database. More details: ', err); console.log('Error removing saved execution from database. More details: ', err);
} }
resolve(runData); resolve(runData);
}); },
);
this.activeExecutions.attachWorkflowExecution(executionId, workflowExecution); this.activeExecutions.attachWorkflowExecution(executionId, workflowExecution);
return executionId; return executionId;
} }
/** /**
* Run the workflow * Run the workflow
* *
@ -414,12 +529,18 @@ export class WorkflowRunner {
* @returns {Promise<string>} * @returns {Promise<string>}
* @memberof WorkflowRunner * @memberof WorkflowRunner
*/ */
async runSubprocess(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean, restartExecutionId?: string): Promise<string> { async runSubprocess(
data: IWorkflowExecutionDataProcess,
loadStaticData?: boolean,
restartExecutionId?: string,
): Promise<string> {
let startedAt = new Date(); let startedAt = new Date();
const subprocess = fork(pathJoin(__dirname, 'WorkflowRunnerProcess.js')); const subprocess = fork(pathJoin(__dirname, 'WorkflowRunnerProcess.js'));
if (loadStaticData === true && data.workflowData.id) { 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 // Register the active execution
@ -437,8 +558,9 @@ export class WorkflowRunner {
} }
let nodeTypeData: ITransferNodeTypes; let nodeTypeData: ITransferNodeTypes;
let credentialTypeData: ICredentialsTypeData; let credentialTypeData: ICredentialsTypeData;
// eslint-disable-next-line prefer-destructuring
let credentialsOverwrites = this.credentialsOverwrites; let credentialsOverwrites = this.credentialsOverwrites;
if (loadAllNodeTypes === true) { if (loadAllNodeTypes) {
// Supply all nodeTypes and credentialTypes // Supply all nodeTypes and credentialTypes
nodeTypeData = WorkflowHelpers.getAllNodeTypeData(); nodeTypeData = WorkflowHelpers.getAllNodeTypeData();
const credentialTypes = CredentialTypes(); const credentialTypes = CredentialTypes();
@ -458,8 +580,10 @@ export class WorkflowRunner {
(data as unknown as IWorkflowExecutionDataProcessWithExecution).executionId = executionId; (data as unknown as IWorkflowExecutionDataProcessWithExecution).executionId = executionId;
(data as unknown as IWorkflowExecutionDataProcessWithExecution).nodeTypeData = nodeTypeData; (data as unknown as IWorkflowExecutionDataProcessWithExecution).nodeTypeData = nodeTypeData;
(data as unknown as IWorkflowExecutionDataProcessWithExecution).credentialsOverwrite = this.credentialsOverwrites; (data as unknown as IWorkflowExecutionDataProcessWithExecution).credentialsOverwrite =
(data as unknown as IWorkflowExecutionDataProcessWithExecution).credentialsTypeData = credentialTypeData; this.credentialsOverwrites;
(data as unknown as IWorkflowExecutionDataProcessWithExecution).credentialsTypeData =
credentialTypeData;
const workflowHooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId); const workflowHooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId);
@ -475,7 +599,7 @@ export class WorkflowRunner {
let executionTimeout: NodeJS.Timeout; let executionTimeout: NodeJS.Timeout;
let workflowTimeout = config.get('executions.timeout') as number; // initialize with default let workflowTimeout = config.get('executions.timeout') as number; // initialize with default
if (data.workflowData.settings && data.workflowData.settings.executionTimeout) { 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) => { const processTimeoutFunction = (timeout: number) => {
@ -484,11 +608,16 @@ export class WorkflowRunner {
}; };
if (workflowTimeout > 0) { 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. // 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 // 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. // 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 // Create a list of child spawned executions
@ -498,7 +627,10 @@ export class WorkflowRunner {
// Listen to data from the subprocess // Listen to data from the subprocess
subprocess.on('message', async (message: IProcessMessage) => { 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') { if (message.type === 'start') {
// Now that the execution actually started set the timeout again so that does not time out to early. // Now that the execution actually started set the timeout again so that does not time out to early.
startedAt = new Date(); startedAt = new Date();
@ -506,18 +638,25 @@ export class WorkflowRunner {
clearTimeout(executionTimeout); clearTimeout(executionTimeout);
executionTimeout = setTimeout(processTimeoutFunction, workflowTimeout, workflowTimeout); executionTimeout = setTimeout(processTimeoutFunction, workflowTimeout, workflowTimeout);
} }
} else if (message.type === 'end') { } else if (message.type === 'end') {
clearTimeout(executionTimeout); clearTimeout(executionTimeout);
this.activeExecutions.remove(executionId!, message.data.runData); this.activeExecutions.remove(executionId, message.data.runData);
} else if (message.type === 'sendMessageToUI') { } 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') { } else if (message.type === 'processError') {
clearTimeout(executionTimeout); clearTimeout(executionTimeout);
const executionError = message.data.executionError as ExecutionError; 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') { } else if (message.type === 'processHook') {
this.processHookMessage(workflowHooks, message.data as IProcessMessageDataHook); this.processHookMessage(workflowHooks, message.data as IProcessMessageDataHook);
} else if (message.type === 'timeout') { } else if (message.type === 'timeout') {
@ -529,43 +668,61 @@ export class WorkflowRunner {
} else if (message.type === 'startExecution') { } else if (message.type === 'startExecution') {
const executionId = await this.activeExecutions.add(message.data.runData); const executionId = await this.activeExecutions.add(message.data.runData);
childExecutionIds.push(executionId); childExecutionIds.push(executionId);
subprocess.send({ type: 'executionId', data: {executionId} } as IProcessMessage); subprocess.send({ type: 'executionId', data: { executionId } } as IProcessMessage);
} else if (message.type === 'finishExecution') { } else if (message.type === 'finishExecution') {
const executionIdIndex = childExecutionIds.indexOf(message.data.executionId); const executionIdIndex = childExecutionIds.indexOf(message.data.executionId);
if (executionIdIndex !== -1) { if (executionIdIndex !== -1) {
childExecutionIds.splice(executionIdIndex, 1); childExecutionIds.splice(executionIdIndex, 1);
} }
// eslint-disable-next-line @typescript-eslint/await-thenable
await this.activeExecutions.remove(message.data.executionId, message.data.result); 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 // Also get informed when the processes does exit especially when it did crash or timed out
subprocess.on('exit', async (code, signal) => { subprocess.on('exit', async (code, signal) => {
if (signal === 'SIGTERM'){ if (signal === 'SIGTERM') {
Logger.debug(`Subprocess for execution ID ${executionId} timed out.`, {executionId}); Logger.debug(`Subprocess for execution ID ${executionId} timed out.`, { executionId });
// Execution timed out and its process has been terminated // Execution timed out and its process has been terminated
const timeoutError = new WorkflowOperationError('Workflow execution timed out!'); 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) { } 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. // 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 // When the child process exits, if we still have
// pending child executions, we mark them as finished // pending child executions, we mark them as finished
// They will display as unknown to the user // They will display as unknown to the user
// Instead of pending forever as executing when it // Instead of pending forever as executing when it
// actually isn't anymore. // actually isn't anymore.
// eslint-disable-next-line @typescript-eslint/await-thenable, no-await-in-loop
await this.activeExecutions.remove(executionId); await this.activeExecutions.remove(executionId);
} }
clearTimeout(executionTimeout); clearTimeout(executionTimeout);
}); });

View file

@ -1,20 +1,11 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { /* eslint-disable consistent-return */
CredentialsOverwrites, /* eslint-disable @typescript-eslint/no-unsafe-assignment */
CredentialTypes, /* eslint-disable @typescript-eslint/no-shadow */
Db, /* eslint-disable @typescript-eslint/no-non-null-assertion */
ExternalHooks, /* eslint-disable @typescript-eslint/no-use-before-define */
IWorkflowExecuteProcess, /* eslint-disable @typescript-eslint/unbound-method */
IWorkflowExecutionDataProcessWithExecution, import { IProcessMessage, WorkflowExecute } from 'n8n-core';
NodeTypes,
WorkflowExecuteAdditionalData,
WorkflowHelpers,
} from './';
import {
IProcessMessage,
WorkflowExecute,
} from 'n8n-core';
import { import {
ExecutionError, ExecutionError,
@ -34,24 +25,41 @@ import {
WorkflowHooks, WorkflowHooks,
WorkflowOperationError, WorkflowOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
getLogger, CredentialsOverwrites,
} from '../src/Logger'; CredentialTypes,
Db,
ExternalHooks,
IWorkflowExecuteProcess,
IWorkflowExecutionDataProcessWithExecution,
NodeTypes,
WorkflowExecuteAdditionalData,
WorkflowHelpers,
} from '.';
import { getLogger } from './Logger';
import * as config from '../config'; import * as config from '../config';
export class WorkflowRunnerProcess { export class WorkflowRunnerProcess {
data: IWorkflowExecutionDataProcessWithExecution | undefined; data: IWorkflowExecutionDataProcessWithExecution | undefined;
logger: ILogger; logger: ILogger;
startedAt = new Date(); startedAt = new Date();
workflow: Workflow | undefined; workflow: Workflow | undefined;
workflowExecute: WorkflowExecute | undefined; workflowExecute: WorkflowExecute | undefined;
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
executionIdCallback: (executionId: string) => void | undefined; executionIdCallback: (executionId: string) => void | undefined;
childExecutions: { childExecutions: {
[key: string]: IWorkflowExecuteProcess, [key: string]: IWorkflowExecuteProcess;
} = {}; } = {};
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
static async stopProcess() { static async stopProcess() {
setTimeout(() => { setTimeout(() => {
// Attempt a graceful shutdown, giving executions 30 seconds to finish // Attempt a graceful shutdown, giving executions 30 seconds to finish
@ -59,17 +67,20 @@ export class WorkflowRunnerProcess {
}, 30000); }, 30000);
} }
async runWorkflow(inputData: IWorkflowExecutionDataProcessWithExecution): Promise<IRun> { async runWorkflow(inputData: IWorkflowExecutionDataProcessWithExecution): Promise<IRun> {
process.on('SIGTERM', WorkflowRunnerProcess.stopProcess); process.on('SIGTERM', WorkflowRunnerProcess.stopProcess);
process.on('SIGINT', 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); LoggerProxy.init(logger);
this.data = inputData; 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 className: string;
let tempNode: INodeType; let tempNode: INodeType;
@ -78,13 +89,16 @@ export class WorkflowRunnerProcess {
this.startedAt = new Date(); this.startedAt = new Date();
const nodeTypesData: INodeTypeData = {}; const nodeTypesData: INodeTypeData = {};
// eslint-disable-next-line no-restricted-syntax
for (const nodeTypeName of Object.keys(this.data.nodeTypeData)) { for (const nodeTypeName of Object.keys(this.data.nodeTypeData)) {
className = this.data.nodeTypeData[nodeTypeName].className; className = this.data.nodeTypeData[nodeTypeName].className;
filePath = this.data.nodeTypeData[nodeTypeName].sourcePath; 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); const tempModule = require(filePath);
try { try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
tempNode = new tempModule[className]() as INodeType; tempNode = new tempModule[className]() as INodeType;
} catch (error) { } catch (error) {
throw new Error(`Error loading node "${nodeTypeName}" from: "${filePath}"`); 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 // We check if any node uses credentials. If it does, then
// init database. // init database.
let shouldInitializaDb = false; 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) { if (Object.keys(node.credentials === undefined ? {} : node.credentials).length > 0) {
shouldInitializaDb = true; shouldInitializaDb = true;
} }
@ -126,45 +141,77 @@ export class WorkflowRunnerProcess {
if (shouldInitializaDb) { if (shouldInitializaDb) {
// initialize db as we need to load credentials // initialize db as we need to load credentials
await Db.init(); 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 // Workflow settings specifying it should save
await Db.init(); 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 // Workflow settings not saying anything about saving but default settings says so
await Db.init(); 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 // Workflow settings not saying anything about saving but default settings says so
await Db.init(); await Db.init();
} }
// Start timeout for the execution // Start timeout for the execution
let workflowTimeout = config.get('executions.timeout') as number; // initialize with default 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) { 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) { if (workflowTimeout > 0) {
workflowTimeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number); 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 }); this.workflow = new Workflow({
const additionalData = await WorkflowExecuteAdditionalData.getBase(undefined, workflowTimeout <= 0 ? undefined : Date.now() + workflowTimeout * 1000); 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.hooks = this.getProcessForwardHooks();
additionalData.executionId = inputData.executionId; 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') { if (workflowRunner.data!.executionMode !== 'manual') {
return; return;
} }
try { try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
await sendToParentProcess('sendMessageToUI', { source, message }); await sendToParentProcess('sendMessageToUI', { source, message });
} catch (error) { } 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; const executeWorkflowFunction = additionalData.executeWorkflow;
additionalData.executeWorkflow = async (workflowInfo: IExecuteWorkflowInfo, additionalData: IWorkflowExecuteAdditionalData, inputData?: INodeExecutionData[] | undefined): Promise<Array<INodeExecutionData[] | null> | IRun> => { additionalData.executeWorkflow = async (
workflowInfo: IExecuteWorkflowInfo,
additionalData: IWorkflowExecuteAdditionalData,
inputData?: INodeExecutionData[] | undefined,
): Promise<Array<INodeExecutionData[] | null> | IRun> => {
const workflowData = await WorkflowExecuteAdditionalData.getWorkflowData(workflowInfo); const workflowData = await WorkflowExecuteAdditionalData.getWorkflowData(workflowInfo);
const runData = await WorkflowExecuteAdditionalData.getRunData(workflowData, inputData); const runData = await WorkflowExecuteAdditionalData.getRunData(workflowData, inputData);
await sendToParentProcess('startExecution', { runData }); await sendToParentProcess('startExecution', { runData });
@ -175,11 +222,18 @@ export class WorkflowRunnerProcess {
}); });
let result: IRun; let result: IRun;
try { try {
const executeWorkflowFunctionOutput = await executeWorkflowFunction(workflowInfo, additionalData, inputData, executionId, workflowData, runData) as {workflowExecute: WorkflowExecute, workflow: Workflow} as IWorkflowExecuteProcess; const executeWorkflowFunctionOutput = (await executeWorkflowFunction(
const workflowExecute = executeWorkflowFunctionOutput.workflowExecute; workflowInfo,
additionalData,
inputData,
executionId,
workflowData,
runData,
)) as { workflowExecute: WorkflowExecute; workflow: Workflow } as IWorkflowExecuteProcess;
const { workflowExecute } = executeWorkflowFunctionOutput;
this.childExecutions[executionId] = executeWorkflowFunctionOutput; this.childExecutions[executionId] = executeWorkflowFunctionOutput;
const workflow = executeWorkflowFunctionOutput.workflow; const { workflow } = executeWorkflowFunctionOutput;
result = await workflowExecute.processRunExecutionData(workflow) as IRun; result = await workflowExecute.processRunExecutionData(workflow);
await externalHooks.run('workflow.postExecute', [result, workflowData]); await externalHooks.run('workflow.postExecute', [result, workflowData]);
await sendToParentProcess('finishExecution', { executionId, result }); await sendToParentProcess('finishExecution', { executionId, result });
delete this.childExecutions[executionId]; delete this.childExecutions[executionId];
@ -197,21 +251,34 @@ export class WorkflowRunnerProcess {
}; };
if (this.data.executionData !== undefined) { 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); 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 // Execute all nodes
// Can execute without webhook so go on // Can execute without webhook so go on
this.workflowExecute = new WorkflowExecute(additionalData, this.data.executionMode); this.workflowExecute = new WorkflowExecute(additionalData, this.data.executionMode);
return this.workflowExecute.run(this.workflow, undefined, this.data.destinationNode); return this.workflowExecute.run(this.workflow, undefined, this.data.destinationNode);
} else { }
// Execute only the nodes between start and destination nodes // Execute only the nodes between start and destination nodes
this.workflowExecute = new WorkflowExecute(additionalData, this.data.executionMode); this.workflowExecute = new WorkflowExecute(additionalData, this.data.executionMode);
return this.workflowExecute.runPartialWorkflow(this.workflow, this.data.runData, this.data.startNodes, this.data.destinationNode); 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 * Sends hook data to the parent process that it executes them
@ -220,18 +287,18 @@ export class WorkflowRunnerProcess {
* @param {any[]} parameters * @param {any[]} parameters
* @memberof WorkflowRunnerProcess * @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 { try {
await sendToParentProcess('processHook', { await sendToParentProcess('processHook', {
hook, hook,
parameters, parameters,
}); });
} catch (error) { } 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 * Create a wrapper for hooks which simply forwards the data to
* the parent process where they then can be executed with access * the parent process where they then can be executed with access
@ -264,6 +331,7 @@ export class WorkflowRunnerProcess {
}; };
const preExecuteFunctions = WorkflowExecuteAdditionalData.hookFunctionsPreExecute(); const preExecuteFunctions = WorkflowExecuteAdditionalData.hookFunctionsPreExecute();
// eslint-disable-next-line no-restricted-syntax
for (const key of Object.keys(preExecuteFunctions)) { for (const key of Object.keys(preExecuteFunctions)) {
if (hookFunctions[key] === undefined) { if (hookFunctions[key] === undefined) {
hookFunctions[key] = []; hookFunctions[key] = [];
@ -271,13 +339,16 @@ export class WorkflowRunnerProcess {
hookFunctions[key]!.push.apply(hookFunctions[key], preExecuteFunctions[key]); 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 * Sends data to parent process
* *
@ -285,25 +356,27 @@ export class WorkflowRunnerProcess {
* @param {*} data The data * @param {*} data The data
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async function sendToParentProcess(type: string, data: any): Promise<void> { // tslint:disable-line:no-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
async function sendToParentProcess(type: string, data: any): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
process.send!({ process.send!(
{
type, type,
data, data,
}, (error: Error) => { },
(error: Error) => {
if (error) { if (error) {
return reject(error); return reject(error);
} }
resolve(); resolve();
}); },
);
}); });
} }
const workflowRunner = new WorkflowRunnerProcess(); const workflowRunner = new WorkflowRunnerProcess();
// Listen to messages from parent process which send the data of // Listen to messages from parent process which send the data of
// the worflow to process // the worflow to process
process.on('message', async (message: IProcessMessage) => { process.on('message', async (message: IProcessMessage) => {
@ -324,25 +397,42 @@ process.on('message', async (message: IProcessMessage) => {
let runData: IRun; let runData: IRun;
if (workflowRunner.workflowExecute !== undefined) { if (workflowRunner.workflowExecute !== undefined) {
const executionIds = Object.keys(workflowRunner.childExecutions); const executionIds = Object.keys(workflowRunner.childExecutions);
// eslint-disable-next-line no-restricted-syntax
for (const executionId of executionIds) { for (const executionId of executionIds) {
const childWorkflowExecute = workflowRunner.childExecutions[executionId]; const childWorkflowExecute = workflowRunner.childExecutions[executionId];
runData = childWorkflowExecute.workflowExecute.getFullRunData(workflowRunner.childExecutions[executionId].startedAt); runData = childWorkflowExecute.workflowExecute.getFullRunData(
const timeOutError = message.type === 'timeout' ? new WorkflowOperationError('Workflow execution timed out!') : new WorkflowOperationError('Workflow-Execution has been canceled!'); 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 // 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 // Workflow started already executing
runData = workflowRunner.workflowExecute.getFullRunData(workflowRunner.startedAt); 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 // 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 { } else {
// Workflow did not get started yet // Workflow did not get started yet
runData = { runData = {
@ -352,11 +442,14 @@ process.on('message', async (message: IProcessMessage) => {
}, },
}, },
finished: false, finished: false,
mode: workflowRunner.data ? workflowRunner.data!.executionMode : 'own' as WorkflowExecuteMode, mode: workflowRunner.data
? workflowRunner.data.executionMode
: ('own' as WorkflowExecuteMode),
startedAt: workflowRunner.startedAt, startedAt: workflowRunner.startedAt,
stoppedAt: new Date(), stoppedAt: new Date(),
}; };
// eslint-disable-next-line @typescript-eslint/no-floating-promises
workflowRunner.sendHookToParentProcess('workflowExecuteAfter', [runData]); workflowRunner.sendHookToParentProcess('workflowExecuteAfter', [runData]);
} }
@ -367,16 +460,16 @@ process.on('message', async (message: IProcessMessage) => {
// Stop process // Stop process
process.exit(); process.exit();
} else if (message.type === 'executionId') { } else if (message.type === 'executionId') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
workflowRunner.executionIdCallback(message.data.executionId); workflowRunner.executionIdCallback(message.data.executionId);
} }
} catch (error) { } catch (error) {
// Catch all uncaught errors and forward them to parent process // Catch all uncaught errors and forward them to parent process
const executionError = { const executionError = {
...error, ...error,
name: error!.name || 'Error', name: error.name || 'Error',
message: error!.message, message: error.message,
stack: error!.stack, stack: error.stack,
} as ExecutionError; } as ExecutionError;
await sendToParentProcess('processError', { await sendToParentProcess('processError', {

View file

@ -1,15 +1,6 @@
import { /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
ICredentialNodeAccess, /* eslint-disable import/no-cycle */
} from 'n8n-workflow'; import { ICredentialNodeAccess } from 'n8n-workflow';
import {
getTimestampSyntax,
resolveDataType
} from '../utils';
import {
ICredentialsDb,
} from '../..';
import { import {
BeforeUpdate, BeforeUpdate,
@ -20,10 +11,12 @@ import {
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
UpdateDateColumn, UpdateDateColumn,
} from 'typeorm'; } from 'typeorm';
import { getTimestampSyntax, resolveDataType } from '../utils';
import { ICredentialsDb } from '../..';
@Entity() @Entity()
export class CredentialsEntity implements ICredentialsDb { export class CredentialsEntity implements ICredentialsDb {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;
@ -47,7 +40,11 @@ export class CredentialsEntity implements ICredentialsDb {
@CreateDateColumn({ precision: 3, default: () => getTimestampSyntax() }) @CreateDateColumn({ precision: 3, default: () => getTimestampSyntax() })
createdAt: Date; createdAt: Date;
@UpdateDateColumn({ precision: 3, default: () => getTimestampSyntax(), onUpdate: getTimestampSyntax() }) @UpdateDateColumn({
precision: 3,
default: () => getTimestampSyntax(),
onUpdate: getTimestampSyntax(),
})
updatedAt: Date; updatedAt: Date;
@BeforeUpdate() @BeforeUpdate()

View file

@ -1,27 +1,13 @@
import { /* eslint-disable import/no-cycle */
WorkflowExecuteMode, import { WorkflowExecuteMode } from 'n8n-workflow';
} from 'n8n-workflow';
import { import { Column, ColumnOptions, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
IExecutionFlattedDb, import { IExecutionFlattedDb, IWorkflowDb } from '../..';
IWorkflowDb,
} from '../../';
import { import { resolveDataType } from '../utils';
resolveDataType
} from '../utils';
import {
Column,
ColumnOptions,
Entity,
Index,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity() @Entity()
export class ExecutionEntity implements IExecutionFlattedDb { export class ExecutionEntity implements IExecutionFlattedDb {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;

View file

@ -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 { IsDate, IsOptional, IsString, Length } from 'class-validator';
import { ITagDb } from '../../Interfaces'; import { ITagDb } from '../../Interfaces';
@ -7,7 +18,6 @@ import { getTimestampSyntax } from '../utils';
@Entity() @Entity()
export class TagEntity implements ITagDb { export class TagEntity implements ITagDb {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;
@ -22,12 +32,16 @@ export class TagEntity implements ITagDb {
@IsDate() @IsDate()
createdAt: Date; 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 @IsOptional() // ignored by validation because set at DB level
@IsDate() @IsDate()
updatedAt: Date; updatedAt: Date;
@ManyToMany(() => WorkflowEntity, workflow => workflow.tags) @ManyToMany(() => WorkflowEntity, (workflow) => workflow.tags)
workflows: WorkflowEntity[]; workflows: WorkflowEntity[];
@BeforeUpdate() @BeforeUpdate()

View file

@ -1,18 +1,11 @@
import { import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
Column,
Entity,
Index,
PrimaryColumn,
} from 'typeorm';
import { // eslint-disable-next-line import/no-cycle
IWebhookDb, import { IWebhookDb } from '../../Interfaces';
} from '../../Interfaces';
@Entity() @Entity()
@Index(['webhookId', 'method', 'pathLength']) @Index(['webhookId', 'method', 'pathLength'])
export class WebhookEntity implements IWebhookDb { export class WebhookEntity implements IWebhookDb {
@Column() @Column()
workflowId: number; workflowId: number;

View file

@ -1,13 +1,8 @@
import { /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
Length, /* eslint-disable import/no-cycle */
} from 'class-validator'; import { Length } from 'class-validator';
import { import { IConnections, IDataObject, INode, IWorkflowSettings } from 'n8n-workflow';
IConnections,
IDataObject,
INode,
IWorkflowSettings,
} from 'n8n-workflow';
import { import {
BeforeUpdate, BeforeUpdate,
@ -22,22 +17,14 @@ import {
UpdateDateColumn, UpdateDateColumn,
} from 'typeorm'; } from 'typeorm';
import { import { IWorkflowDb } from '../..';
IWorkflowDb,
} from '../../';
import { import { getTimestampSyntax, resolveDataType } from '../utils';
getTimestampSyntax,
resolveDataType
} from '../utils';
import { import { TagEntity } from './TagEntity';
TagEntity,
} from './TagEntity';
@Entity() @Entity()
export class WorkflowEntity implements IWorkflowDb { export class WorkflowEntity implements IWorkflowDb {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;
@ -58,7 +45,11 @@ export class WorkflowEntity implements IWorkflowDb {
@CreateDateColumn({ precision: 3, default: () => getTimestampSyntax() }) @CreateDateColumn({ precision: 3, default: () => getTimestampSyntax() })
createdAt: Date; createdAt: Date;
@UpdateDateColumn({ precision: 3, default: () => getTimestampSyntax(), onUpdate: getTimestampSyntax() }) @UpdateDateColumn({
precision: 3,
default: () => getTimestampSyntax(),
onUpdate: getTimestampSyntax(),
})
updatedAt: Date; updatedAt: Date;
@Column({ @Column({
@ -73,16 +64,16 @@ export class WorkflowEntity implements IWorkflowDb {
}) })
staticData?: IDataObject; staticData?: IDataObject;
@ManyToMany(() => TagEntity, tag => tag.workflows) @ManyToMany(() => TagEntity, (tag) => tag.workflows)
@JoinTable({ @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: { joinColumn: {
name: "workflowId", name: 'workflowId',
referencedColumnName: "id", referencedColumnName: 'id',
}, },
inverseJoinColumn: { inverseJoinColumn: {
name: "tagId", name: 'tagId',
referencedColumnName: "id", referencedColumnName: 'id',
}, },
}) })
tags: TagEntity[]; tags: TagEntity[];

View file

@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable import/no-cycle */
import { CredentialsEntity } from './CredentialsEntity'; import { CredentialsEntity } from './CredentialsEntity';
import { ExecutionEntity } from './ExecutionEntity'; import { ExecutionEntity } from './ExecutionEntity';
import { WorkflowEntity } from './WorkflowEntity'; import { WorkflowEntity } from './WorkflowEntity';

View file

@ -1,7 +1,6 @@
import { /* eslint-disable import/no-cycle */
DatabaseType, import { DatabaseType } from '../index';
} from '../index'; import { getConfigValueSync } from '../GenericHelpers';
import { getConfigValueSync } from '../../src/GenericHelpers';
/** /**
* Resolves the data type for the used database type * Resolves the data type for the used database type
@ -10,6 +9,7 @@ import { getConfigValueSync } from '../../src/GenericHelpers';
* @param {string} dataType * @param {string} dataType
* @returns {string} * @returns {string}
*/ */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function resolveDataType(dataType: string) { export function resolveDataType(dataType: string) {
const dbType = getConfigValueSync('database.type') as DatabaseType; const dbType = getConfigValueSync('database.type') as DatabaseType;
@ -27,16 +27,16 @@ export function resolveDataType(dataType: string) {
return typeMap[dbType][dataType] ?? dataType; return typeMap[dbType][dataType] ?? dataType;
} }
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function getTimestampSyntax() { export function getTimestampSyntax() {
const dbType = getConfigValueSync('database.type') as DatabaseType; const dbType = getConfigValueSync('database.type') as DatabaseType;
const map: { [key in DatabaseType]: string } = { const map: { [key in DatabaseType]: string } = {
sqlite: "STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')", sqlite: "STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')",
postgresdb: "CURRENT_TIMESTAMP(3)", postgresdb: 'CURRENT_TIMESTAMP(3)',
mysqldb: "CURRENT_TIMESTAMP(3)", mysqldb: 'CURRENT_TIMESTAMP(3)',
mariadb: "CURRENT_TIMESTAMP(3)", mariadb: 'CURRENT_TIMESTAMP(3)',
}; };
return map[dbType]; return map[dbType];
} }

View file

@ -1,3 +1,5 @@
/* eslint-disable import/first */
/* eslint-disable import/no-cycle */
export * from './CredentialsHelper'; export * from './CredentialsHelper';
export * from './CredentialTypes'; export * from './CredentialTypes';
export * from './CredentialsOverwrites'; export * from './CredentialsOverwrites';
@ -22,6 +24,7 @@ import * as WebhookHelpers from './WebhookHelpers';
import * as WebhookServer from './WebhookServer'; import * as WebhookServer from './WebhookServer';
import * as WorkflowExecuteAdditionalData from './WorkflowExecuteAdditionalData'; import * as WorkflowExecuteAdditionalData from './WorkflowExecuteAdditionalData';
import * as WorkflowHelpers from './WorkflowHelpers'; import * as WorkflowHelpers from './WorkflowHelpers';
export { export {
ActiveExecutions, ActiveExecutions,
ActiveWorkflowRunner, ActiveWorkflowRunner,

View file

@ -1,7 +1,5 @@
describe('Placeholder', () => { describe('Placeholder', () => {
test('example', () => { test('example', () => {
expect(1 + 1).toEqual(2); expect(1 + 1).toEqual(2);
}); });
}); });

View file

@ -17,8 +17,9 @@
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"dev": "npm run watch", "dev": "npm run watch",
"tslint": "tslint -p tsconfig.json -c tslint.json", "format": "cd ../.. && node_modules/prettier/bin-prettier.js packages/core/**/**.ts --write",
"tslintfix": "tslint --fix -p tsconfig.json -c tslint.json", "lint": "cd ../.. && node_modules/eslint/bin/eslint.js packages/core",
"lintfix": "cd ../.. && node_modules/eslint/bin/eslint.js packages/core --fix",
"watch": "tsc --watch", "watch": "tsc --watch",
"test": "jest" "test": "jest"
}, },
@ -38,7 +39,7 @@
"source-map-support": "^0.5.9", "source-map-support": "^0.5.9",
"ts-jest": "^26.3.0", "ts-jest": "^26.3.0",
"tslint": "^6.1.2", "tslint": "^6.1.2",
"typescript": "~3.9.7" "typescript": "~4.3.5"
}, },
"dependencies": { "dependencies": {
"client-oauth2": "^4.2.5", "client-oauth2": "^4.2.5",

View file

@ -6,10 +6,8 @@ import {
WorkflowExecuteMode, WorkflowExecuteMode,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { // eslint-disable-next-line import/no-cycle
NodeExecuteFunctions, import { NodeExecuteFunctions } from '.';
} from './';
export class ActiveWebhooks { export class ActiveWebhooks {
private workflowWebhooks: { private workflowWebhooks: {
@ -22,7 +20,6 @@ export class ActiveWebhooks {
testWebhooks = false; testWebhooks = false;
/** /**
* Adds a new webhook * Adds a new webhook
* *
@ -31,19 +28,31 @@ export class ActiveWebhooks {
* @returns {Promise<void>} * @returns {Promise<void>}
* @memberof ActiveWebhooks * @memberof ActiveWebhooks
*/ */
async add(workflow: Workflow, webhookData: IWebhookData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): Promise<void> { async add(
workflow: Workflow,
webhookData: IWebhookData,
mode: WorkflowExecuteMode,
activation: WorkflowActivateMode,
): Promise<void> {
if (workflow.id === undefined) { if (workflow.id === undefined) {
throw new Error('Webhooks can only be added for saved workflows as an id is needed!'); throw new Error('Webhooks can only be added for saved workflows as an id is needed!');
} }
if (webhookData.path.endsWith('/')) { if (webhookData.path.endsWith('/')) {
// eslint-disable-next-line no-param-reassign
webhookData.path = webhookData.path.slice(0, -1); 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) { 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) { if (this.workflowWebhooks[webhookData.workflowId] === undefined) {
@ -58,18 +67,33 @@ export class ActiveWebhooks {
this.webhookUrls[webhookKey].push(webhookData); this.webhookUrls[webhookKey].push(webhookData);
try { 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 (webhookExists !== true) {
// If webhook does not exist yet create it // 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) { } catch (error) {
// If there was a problem unregister the webhook again // If there was a problem unregister the webhook again
if (this.webhookUrls[webhookKey].length <= 1) { if (this.webhookUrls[webhookKey].length <= 1) {
delete this.webhookUrls[webhookKey]; delete this.webhookUrls[webhookKey];
} else { } 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; throw error;
@ -77,7 +101,6 @@ export class ActiveWebhooks {
this.workflowWebhooks[webhookData.workflowId].push(webhookData); this.workflowWebhooks[webhookData.workflowId].push(webhookData);
} }
/** /**
* Returns webhookData if a webhook with matches is currently registered * Returns webhookData if a webhook with matches is currently registered
* *
@ -98,9 +121,9 @@ export class ActiveWebhooks {
const pathElementsSet = new Set(path.split('/')); const pathElementsSet = new Set(path.split('/'));
// check if static elements match in path // check if static elements match in path
// if more results have been returned choose the one with the most static-route matches // if more results have been returned choose the one with the most static-route matches
this.webhookUrls[webhookKey].forEach(dynamicWebhook => { this.webhookUrls[webhookKey].forEach((dynamicWebhook) => {
const staticElements = dynamicWebhook.path.split('/').filter(ele => !ele.startsWith(':')); const staticElements = dynamicWebhook.path.split('/').filter((ele) => !ele.startsWith(':'));
const allStaticExist = staticElements.every(staticEle => pathElementsSet.has(staticEle)); const allStaticExist = staticElements.every((staticEle) => pathElementsSet.has(staticEle));
if (allStaticExist && staticElements.length > maxMatches) { if (allStaticExist && staticElements.length > maxMatches) {
maxMatches = staticElements.length; maxMatches = staticElements.length;
@ -120,11 +143,12 @@ export class ActiveWebhooks {
* @param path * @param path
*/ */
getWebhookMethods(path: string): string[] { getWebhookMethods(path: string): string[] {
const methods : string[] = []; const methods: string[] = [];
Object.keys(this.webhookUrls) Object.keys(this.webhookUrls)
.filter(key => key.includes(path)) .filter((key) => key.includes(path))
.map(key => { // eslint-disable-next-line array-callback-return
.map((key) => {
methods.push(key.split('|')[0]); methods.push(key.split('|')[0]);
}); });
@ -141,7 +165,6 @@ export class ActiveWebhooks {
return Object.keys(this.workflowWebhooks); return Object.keys(this.workflowWebhooks);
} }
/** /**
* Returns key to uniquely identify a webhook * Returns key to uniquely identify a webhook
* *
@ -155,6 +178,7 @@ export class ActiveWebhooks {
if (webhookId) { if (webhookId) {
if (path.startsWith(webhookId)) { if (path.startsWith(webhookId)) {
const cutFromIndex = path.indexOf('/') + 1; const cutFromIndex = path.indexOf('/') + 1;
// eslint-disable-next-line no-param-reassign
path = path.slice(cutFromIndex); path = path.slice(cutFromIndex);
} }
return `${httpMethod}|${webhookId}|${path.split('/').length}`; return `${httpMethod}|${webhookId}|${path.split('/').length}`;
@ -162,7 +186,6 @@ export class ActiveWebhooks {
return `${httpMethod}|${path}`; return `${httpMethod}|${path}`;
} }
/** /**
* Removes all webhooks of a workflow * Removes all webhooks of a workflow
* *
@ -171,6 +194,7 @@ export class ActiveWebhooks {
* @memberof ActiveWebhooks * @memberof ActiveWebhooks
*/ */
async removeWorkflow(workflow: Workflow): Promise<boolean> { async removeWorkflow(workflow: Workflow): Promise<boolean> {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const workflowId = workflow.id!.toString(); const workflowId = workflow.id!.toString();
if (this.workflowWebhooks[workflowId] === undefined) { if (this.workflowWebhooks[workflowId] === undefined) {
@ -183,10 +207,21 @@ export class ActiveWebhooks {
const mode = 'internal'; const mode = 'internal';
// Go through all the registered webhooks of the workflow and remove them // Go through all the registered webhooks of the workflow and remove them
// eslint-disable-next-line no-restricted-syntax
for (const webhookData of webhooks) { 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 // Remove also the workflow-webhook entry
@ -195,18 +230,16 @@ export class ActiveWebhooks {
return true; return true;
} }
/** /**
* Removes all the webhooks of the given workflows * Removes all the webhooks of the given workflows
*/ */
async removeAll(workflows: Workflow[]): Promise<void> { async removeAll(workflows: Workflow[]): Promise<void> {
const removePromises = []; const removePromises = [];
// eslint-disable-next-line no-restricted-syntax
for (const workflow of workflows) { for (const workflow of workflows) {
removePromises.push(this.removeWorkflow(workflow)); removePromises.push(this.removeWorkflow(workflow));
} }
await Promise.all(removePromises); await Promise.all(removePromises);
return;
} }
} }

View file

@ -1,3 +1,6 @@
/* eslint-disable no-continue */
/* eslint-disable no-await-in-loop */
/* eslint-disable no-restricted-syntax */
import { CronJob } from 'cron'; import { CronJob } from 'cron';
import { import {
@ -13,18 +16,14 @@ import {
WorkflowExecuteMode, WorkflowExecuteMode,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { // eslint-disable-next-line import/no-cycle
ITriggerTime, import { ITriggerTime, IWorkflowData } from '.';
IWorkflowData,
} from './';
export class ActiveWorkflows { export class ActiveWorkflows {
private workflowData: { private workflowData: {
[key: string]: IWorkflowData; [key: string]: IWorkflowData;
} = {}; } = {};
/** /**
* Returns if the workflow is active * Returns if the workflow is active
* *
@ -33,10 +32,10 @@ export class ActiveWorkflows {
* @memberof ActiveWorkflows * @memberof ActiveWorkflows
*/ */
isActive(id: string): boolean { isActive(id: string): boolean {
// eslint-disable-next-line no-prototype-builtins
return this.workflowData.hasOwnProperty(id); return this.workflowData.hasOwnProperty(id);
} }
/** /**
* Returns the ids of the currently active workflows * Returns the ids of the currently active workflows
* *
@ -47,7 +46,6 @@ export class ActiveWorkflows {
return Object.keys(this.workflowData); return Object.keys(this.workflowData);
} }
/** /**
* Returns the Workflow data for the workflow with * Returns the Workflow data for the workflow with
* the given id if it is currently active * the given id if it is currently active
@ -60,7 +58,6 @@ export class ActiveWorkflows {
return this.workflowData[id]; return this.workflowData[id];
} }
/** /**
* Makes a workflow active * Makes a workflow active
* *
@ -70,16 +67,31 @@ export class ActiveWorkflows {
* @returns {Promise<void>} * @returns {Promise<void>}
* @memberof ActiveWorkflows * @memberof ActiveWorkflows
*/ */
async add(id: string, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode, getTriggerFunctions: IGetExecuteTriggerFunctions, getPollFunctions: IGetExecutePollFunctions): Promise<void> { async add(
id: string,
workflow: Workflow,
additionalData: IWorkflowExecuteAdditionalData,
mode: WorkflowExecuteMode,
activation: WorkflowActivateMode,
getTriggerFunctions: IGetExecuteTriggerFunctions,
getPollFunctions: IGetExecutePollFunctions,
): Promise<void> {
this.workflowData[id] = {}; this.workflowData[id] = {};
const triggerNodes = workflow.getTriggerNodes(); const triggerNodes = workflow.getTriggerNodes();
let triggerResponse: ITriggerResponse | undefined; let triggerResponse: ITriggerResponse | undefined;
this.workflowData[id].triggerResponses = []; this.workflowData[id].triggerResponses = [];
for (const triggerNode of triggerNodes) { 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 (triggerResponse !== undefined) {
// If a response was given save it // If a response was given save it
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.workflowData[id].triggerResponses!.push(triggerResponse); this.workflowData[id].triggerResponses!.push(triggerResponse);
} }
} }
@ -88,12 +100,21 @@ export class ActiveWorkflows {
if (pollNodes.length) { if (pollNodes.length) {
this.workflowData[id].pollResponses = []; this.workflowData[id].pollResponses = [];
for (const pollNode of pollNodes) { 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 * Activates polling for the given node
* *
@ -104,7 +125,14 @@ export class ActiveWorkflows {
* @returns {Promise<IPollResponse>} * @returns {Promise<IPollResponse>}
* @memberof ActiveWorkflows * @memberof ActiveWorkflows
*/ */
async activatePolling(node: INode, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, getPollFunctions: IGetExecutePollFunctions, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): Promise<IPollResponse> { async activatePolling(
node: INode,
workflow: Workflow,
additionalData: IWorkflowExecuteAdditionalData,
getPollFunctions: IGetExecutePollFunctions,
mode: WorkflowExecuteMode,
activation: WorkflowActivateMode,
): Promise<IPollResponse> {
const pollFunctions = getPollFunctions(workflow, node, additionalData, mode, activation); const pollFunctions = getPollFunctions(workflow, node, additionalData, mode, activation);
const pollTimes = pollFunctions.getNodeParameter('pollTimes') as unknown as { const pollTimes = pollFunctions.getNodeParameter('pollTimes') as unknown as {
@ -165,10 +193,15 @@ export class ActiveWorkflows {
// The trigger function to execute when the cron-time got reached // The trigger function to execute when the cron-time got reached
const executeTrigger = async () => { 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); const pollResponse = await workflow.runPoll(node, pollFunctions);
if (pollResponse !== null) { if (pollResponse !== null) {
// eslint-disable-next-line no-underscore-dangle
pollFunctions.__emit(pollResponse); pollFunctions.__emit(pollResponse);
} }
}; };
@ -180,6 +213,7 @@ export class ActiveWorkflows {
// Start the cron-jobs // Start the cron-jobs
const cronJobs: CronJob[] = []; const cronJobs: CronJob[] = [];
// eslint-disable-next-line @typescript-eslint/no-shadow
for (const cronTime of cronTimes) { for (const cronTime of cronTimes) {
const cronTimeParts = cronTime.split(' '); const cronTimeParts = cronTime.split(' ');
if (cronTimeParts.length > 0 && cronTimeParts[0].includes('*')) { if (cronTimeParts.length > 0 && cronTimeParts[0].includes('*')) {
@ -201,7 +235,6 @@ export class ActiveWorkflows {
}; };
} }
/** /**
* Makes a workflow inactive * Makes a workflow inactive
* *
@ -212,7 +245,9 @@ export class ActiveWorkflows {
async remove(id: string): Promise<void> { async remove(id: string): Promise<void> {
if (!this.isActive(id)) { if (!this.isActive(id)) {
// Workflow is currently not registered // 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]; const workflowData = this.workflowData[id];
@ -235,5 +270,4 @@ export class ActiveWorkflows {
delete this.workflowData[id]; delete this.workflowData[id];
} }
} }

View file

@ -7,16 +7,13 @@ import {
import { AES, enc } from 'crypto-js'; import { AES, enc } from 'crypto-js';
export class Credentials extends ICredentials { export class Credentials extends ICredentials {
/** /**
* Returns if the given nodeType has access to data * Returns if the given nodeType has access to data
*/ */
hasNodeAccess(nodeType: string): boolean { hasNodeAccess(nodeType: string): boolean {
// eslint-disable-next-line no-restricted-syntax
for (const accessData of this.nodesAccess) { for (const accessData of this.nodesAccess) {
if (accessData.nodeType === nodeType) { if (accessData.nodeType === nodeType) {
return true; return true;
} }
@ -25,7 +22,6 @@ export class Credentials extends ICredentials {
return false; return false;
} }
/** /**
* Sets new credential object * Sets new credential object
*/ */
@ -33,7 +29,6 @@ export class Credentials extends ICredentials {
this.data = AES.encrypt(JSON.stringify(data), encryptionKey).toString(); this.data = AES.encrypt(JSON.stringify(data), encryptionKey).toString();
} }
/** /**
* Sets new credentials for given key * Sets new credentials for given key
*/ */
@ -50,13 +45,14 @@ export class Credentials extends ICredentials {
return this.setData(fullData, encryptionKey); return this.setData(fullData, encryptionKey);
} }
/** /**
* Returns the decrypted credential object * Returns the decrypted credential object
*/ */
getData(encryptionKey: string, nodeType?: string): ICredentialDataDecryptedObject { getData(encryptionKey: string, nodeType?: string): ICredentialDataDecryptedObject {
if (nodeType && !this.hasNodeAccess(nodeType)) { 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) { if (this.data === undefined) {
@ -66,13 +62,15 @@ export class Credentials extends ICredentials {
const decryptedData = AES.decrypt(this.data, encryptionKey); const decryptedData = AES.decrypt(this.data, encryptionKey);
try { try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return JSON.parse(decryptedData.toString(enc.Utf8)); return JSON.parse(decryptedData.toString(enc.Utf8));
} catch (e) { } 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 * Returns the decrypted credentials for given key
*/ */
@ -83,6 +81,7 @@ export class Credentials extends ICredentials {
throw new Error(`No data was set.`); throw new Error(`No data was set.`);
} }
// eslint-disable-next-line no-prototype-builtins
if (!fullData.hasOwnProperty(key)) { if (!fullData.hasOwnProperty(key)) {
throw new Error(`No data for key "${key}" exists.`); throw new Error(`No data for key "${key}" exists.`);
} }
@ -90,7 +89,6 @@ export class Credentials extends ICredentials {
return fullData[key]; return fullData[key];
} }
/** /**
* Returns the encrypted credentials to be saved * Returns the encrypted credentials to be saved
*/ */

View file

@ -5,10 +5,10 @@ export interface IDeferredPromise<T> {
resolve: (result: T) => void; resolve: (result: T) => void;
} }
export function createDeferredPromise<T>(): Promise<IDeferredPromise<T>> { export async function createDeferredPromise<T>(): Promise<IDeferredPromise<T>> {
return new Promise<IDeferredPromise<T>>(resolveCreate => { return new Promise<IDeferredPromise<T>>((resolveCreate) => {
const promise = new Promise<T>((resolve, reject) => { const promise = new Promise<T>((resolve, reject) => {
resolveCreate({ promise: () => promise, resolve, reject }); resolveCreate({ promise: async () => promise, resolve, reject });
}); });
}); });
} }

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { import {
IAllExecuteFunctions, IAllExecuteFunctions,
IBinaryData, IBinaryData,
@ -16,70 +17,116 @@ import {
ITriggerResponse, ITriggerResponse,
IWebhookFunctions as IWebhookFunctionsBase, IWebhookFunctions as IWebhookFunctionsBase,
IWorkflowSettings as IWorkflowSettingsWorkflow, IWorkflowSettings as IWorkflowSettingsWorkflow,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { OptionsWithUri, OptionsWithUrl } from 'request'; import { OptionsWithUri, OptionsWithUrl } from 'request';
import * as requestPromise from 'request-promise-native'; import * as requestPromise from 'request-promise-native';
interface Constructable<T> { interface Constructable<T> {
new(): T; new (): T;
} }
export interface IProcessMessage { export interface IProcessMessage {
data?: any; // tslint:disable-line:no-any data?: any;
type: string; type: string;
} }
export interface IExecuteFunctions extends IExecuteFunctionsBase { export interface IExecuteFunctions extends IExecuteFunctionsBase {
helpers: { helpers: {
prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise<IBinaryData>; prepareBinaryData(
binaryData: Buffer,
filePath?: string,
mimeType?: string,
): Promise<IBinaryData>;
getBinaryDataBuffer(itemIndex: number, propertyName: string): Promise<Buffer>; getBinaryDataBuffer(itemIndex: number, propertyName: string): Promise<Buffer>;
request: requestPromise.RequestPromiseAPI; request: requestPromise.RequestPromiseAPI;
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise<any>; // tslint:disable-line:no-any requestOAuth2(
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any>; // tslint:disable-line:no-any this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
oAuth2Options?: IOAuth2Options,
): Promise<any>; // tslint:disable-line:no-any
requestOAuth1(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions,
): Promise<any>; // tslint:disable-line:no-any
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
}; };
} }
export interface IExecuteSingleFunctions extends IExecuteSingleFunctionsBase { export interface IExecuteSingleFunctions extends IExecuteSingleFunctionsBase {
helpers: { helpers: {
prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise<IBinaryData>; prepareBinaryData(
request: requestPromise.RequestPromiseAPI, binaryData: Buffer,
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise<any>, // tslint:disable-line:no-any filePath?: string,
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any mimeType?: string,
): Promise<IBinaryData>;
request: requestPromise.RequestPromiseAPI;
requestOAuth2(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
oAuth2Options?: IOAuth2Options,
): Promise<any>; // tslint:disable-line:no-any
requestOAuth1(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions,
): Promise<any>; // tslint:disable-line:no-any
}; };
} }
export interface IPollFunctions extends IPollFunctionsBase { export interface IPollFunctions extends IPollFunctionsBase {
helpers: { helpers: {
prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise<IBinaryData>; prepareBinaryData(
request: requestPromise.RequestPromiseAPI, binaryData: Buffer,
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise<any>, // tslint:disable-line:no-any filePath?: string,
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any mimeType?: string,
): Promise<IBinaryData>;
request: requestPromise.RequestPromiseAPI;
requestOAuth2(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
oAuth2Options?: IOAuth2Options,
): Promise<any>; // tslint:disable-line:no-any
requestOAuth1(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions,
): Promise<any>; // tslint:disable-line:no-any
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
}; };
} }
export interface IResponseError extends Error { export interface IResponseError extends Error {
statusCode?: number; statusCode?: number;
} }
export interface ITriggerFunctions extends ITriggerFunctionsBase { export interface ITriggerFunctions extends ITriggerFunctionsBase {
helpers: { helpers: {
prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise<IBinaryData>; prepareBinaryData(
request: requestPromise.RequestPromiseAPI, binaryData: Buffer,
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise<any>, // tslint:disable-line:no-any filePath?: string,
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any mimeType?: string,
): Promise<IBinaryData>;
request: requestPromise.RequestPromiseAPI;
requestOAuth2(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
oAuth2Options?: IOAuth2Options,
): Promise<any>; // tslint:disable-line:no-any
requestOAuth1(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions,
): Promise<any>; // tslint:disable-line:no-any
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
}; };
} }
export interface ITriggerTime { export interface ITriggerTime {
mode: string; mode: string;
hour: number; hour: number;
@ -89,7 +136,6 @@ export interface ITriggerTime {
[key: string]: string | number; [key: string]: string | number;
} }
export interface IUserSettings { export interface IUserSettings {
encryptionKey?: string; encryptionKey?: string;
tunnelSubdomain?: string; tunnelSubdomain?: string;
@ -97,28 +143,57 @@ export interface IUserSettings {
export interface ILoadOptionsFunctions extends ILoadOptionsFunctionsBase { export interface ILoadOptionsFunctions extends ILoadOptionsFunctionsBase {
helpers: { helpers: {
request?: requestPromise.RequestPromiseAPI, request?: requestPromise.RequestPromiseAPI;
requestOAuth2?: (this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options) => Promise<any>, // tslint:disable-line:no-any requestOAuth2?: (
requestOAuth1?(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
oAuth2Options?: IOAuth2Options,
) => Promise<any>; // tslint:disable-line:no-any
requestOAuth1?(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions,
): Promise<any>; // tslint:disable-line:no-any
}; };
} }
export interface IHookFunctions extends IHookFunctionsBase { export interface IHookFunctions extends IHookFunctionsBase {
helpers: { helpers: {
request: requestPromise.RequestPromiseAPI, request: requestPromise.RequestPromiseAPI;
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise<any>, // tslint:disable-line:no-any requestOAuth2(
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
oAuth2Options?: IOAuth2Options,
): Promise<any>; // tslint:disable-line:no-any
requestOAuth1(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions,
): Promise<any>; // tslint:disable-line:no-any
}; };
} }
export interface IWebhookFunctions extends IWebhookFunctionsBase { export interface IWebhookFunctions extends IWebhookFunctionsBase {
helpers: { helpers: {
prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise<IBinaryData>; prepareBinaryData(
request: requestPromise.RequestPromiseAPI, binaryData: Buffer,
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise<any>, // tslint:disable-line:no-any filePath?: string,
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any mimeType?: string,
): Promise<IBinaryData>;
request: requestPromise.RequestPromiseAPI;
requestOAuth2(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
oAuth2Options?: IOAuth2Options,
): Promise<any>; // tslint:disable-line:no-any
requestOAuth1(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions,
): Promise<any>; // tslint:disable-line:no-any
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
}; };
} }
@ -129,19 +204,16 @@ export interface IWorkflowSettings extends IWorkflowSettingsWorkflow {
saveManualRuns?: boolean; saveManualRuns?: boolean;
} }
// New node definition in file // New node definition in file
export interface INodeDefinitionFile { export interface INodeDefinitionFile {
[key: string]: Constructable<INodeType | ICredentialType>; [key: string]: Constructable<INodeType | ICredentialType>;
} }
// Is identical to TaskDataConnections but does not allow null value to be used as input for nodes // Is identical to TaskDataConnections but does not allow null value to be used as input for nodes
export interface INodeInputDataConnections { export interface INodeInputDataConnections {
[key: string]: INodeExecutionData[][]; [key: string]: INodeExecutionData[][];
} }
export interface IWorkflowData { export interface IWorkflowData {
pollResponses?: IPollResponse[]; pollResponses?: IPollResponse[];
triggerResponses?: ITriggerResponse[]; triggerResponses?: ITriggerResponse[];

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { import {
INode, INode,
INodeCredentials, INodeCredentials,
@ -8,21 +9,24 @@ import {
Workflow, Workflow,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { // eslint-disable-next-line import/no-cycle
NodeExecuteFunctions, import { NodeExecuteFunctions } from '.';
} from './';
const TEMP_NODE_NAME = 'Temp-Node'; const TEMP_NODE_NAME = 'Temp-Node';
const TEMP_WORKFLOW_NAME = 'Temp-Workflow'; const TEMP_WORKFLOW_NAME = 'Temp-Workflow';
export class LoadNodeParameterOptions { export class LoadNodeParameterOptions {
path: string; path: string;
workflow: Workflow; workflow: Workflow;
constructor(
constructor(nodeTypeName: string, nodeTypes: INodeTypes, path: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials) { nodeTypeName: string,
nodeTypes: INodeTypes,
path: string,
currentNodeParameters: INodeParameters,
credentials?: INodeCredentials,
) {
this.path = path; this.path = path;
const nodeType = nodeTypes.getByName(nodeTypeName); const nodeType = nodeTypes.getByName(nodeTypeName);
@ -35,10 +39,7 @@ export class LoadNodeParameterOptions {
name: TEMP_NODE_NAME, name: TEMP_NODE_NAME,
type: nodeTypeName, type: nodeTypeName,
typeVersion: 1, typeVersion: 1,
position: [ position: [0, 0],
0,
0,
],
}; };
if (credentials) { if (credentials) {
@ -46,22 +47,25 @@ export class LoadNodeParameterOptions {
} }
const workflowData = { const workflowData = {
nodes: [ nodes: [nodeData],
nodeData,
],
connections: {}, 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 data of a fake workflow
* *
* @returns * @returns
* @memberof LoadNodeParameterOptions * @memberof LoadNodeParameterOptions
*/ */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
getWorkflowData() { getWorkflowData() {
return { return {
name: TEMP_WORKFLOW_NAME, name: TEMP_WORKFLOW_NAME,
@ -73,7 +77,6 @@ export class LoadNodeParameterOptions {
}; };
} }
/** /**
* Returns the available options * Returns the available options
* *
@ -82,18 +85,31 @@ export class LoadNodeParameterOptions {
* @returns {Promise<INodePropertyOptions[]>} * @returns {Promise<INodePropertyOptions[]>}
* @memberof LoadNodeParameterOptions * @memberof LoadNodeParameterOptions
*/ */
getOptions(methodName: string, additionalData: IWorkflowExecuteAdditionalData): Promise<INodePropertyOptions[]> { async getOptions(
methodName: string,
additionalData: IWorkflowExecuteAdditionalData,
): Promise<INodePropertyOptions[]> {
const node = this.workflow.getNode(TEMP_NODE_NAME); const node = this.workflow.getNode(TEMP_NODE_NAME);
const nodeType = this.workflow.nodeTypes.getByName(node!.type); const nodeType = this.workflow.nodeTypes.getByName(node!.type);
if (nodeType!.methods === undefined || nodeType!.methods.loadOptions === undefined || nodeType!.methods.loadOptions[methodName] === undefined) { if (
throw new Error(`The node-type "${node!.type}" does not have the method "${methodName}" defined!`); 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); return nodeType!.methods.loadOptions[methodName].call(thisArgs);
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -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 { import {
ENCRYPTION_KEY_ENV_OVERWRITE, ENCRYPTION_KEY_ENV_OVERWRITE,
EXTENSIONS_SUBDIRECTORY, EXTENSIONS_SUBDIRECTORY,
@ -7,20 +15,15 @@ import {
USER_SETTINGS_SUBFOLDER, USER_SETTINGS_SUBFOLDER,
} from '.'; } from '.';
// eslint-disable-next-line @typescript-eslint/no-var-requires
import * as fs from 'fs';
import * as path from 'path';
import { randomBytes } from 'crypto';
const { promisify } = require('util'); const { promisify } = require('util');
const fsAccess = promisify(fs.access); const fsAccess = promisify(fs.access);
const fsReadFile = promisify(fs.readFile); const fsReadFile = promisify(fs.readFile);
const fsMkdir = promisify(fs.mkdir); const fsMkdir = promisify(fs.mkdir);
const fsWriteFile = promisify(fs.writeFile); const fsWriteFile = promisify(fs.writeFile);
let settingsCache: IUserSettings | undefined;
let settingsCache: IUserSettings | undefined = undefined;
/** /**
* Creates the user settings if they do not exist yet * Creates the user settings if they do not exist yet
@ -49,12 +52,12 @@ export async function prepareUserSettings(): Promise<IUserSettings> {
userSettings.encryptionKey = randomBytes(24).toString('base64'); userSettings.encryptionKey = randomBytes(24).toString('base64');
} }
// eslint-disable-next-line no-console
console.log(`UserSettings were generated and saved to: ${settingsPath}`); console.log(`UserSettings were generated and saved to: ${settingsPath}`);
return writeUserSettings(userSettings, settingsPath); return writeUserSettings(userSettings, settingsPath);
} }
/** /**
* Returns the encryption key which is used to encrypt * Returns the encryption key which is used to encrypt
* the credentials. * the credentials.
@ -62,6 +65,7 @@ export async function prepareUserSettings(): Promise<IUserSettings> {
* @export * @export
* @returns * @returns
*/ */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export async function getEncryptionKey() { export async function getEncryptionKey() {
if (process.env[ENCRYPTION_KEY_ENV_OVERWRITE] !== undefined) { if (process.env[ENCRYPTION_KEY_ENV_OVERWRITE] !== undefined) {
return process.env[ENCRYPTION_KEY_ENV_OVERWRITE]; return process.env[ENCRYPTION_KEY_ENV_OVERWRITE];
@ -80,7 +84,6 @@ export async function getEncryptionKey() {
return userSettings.encryptionKey; return userSettings.encryptionKey;
} }
/** /**
* Adds/Overwrite the given settings in the currently * Adds/Overwrite the given settings in the currently
* saved user settings * saved user settings
@ -90,7 +93,10 @@ export async function getEncryptionKey() {
* @param {string} [settingsPath] Optional settings file path * @param {string} [settingsPath] Optional settings file path
* @returns {Promise<IUserSettings>} * @returns {Promise<IUserSettings>}
*/ */
export async function addToUserSettings(addSettings: IUserSettings, settingsPath?: string): Promise<IUserSettings> { export async function addToUserSettings(
addSettings: IUserSettings,
settingsPath?: string,
): Promise<IUserSettings> {
if (settingsPath === undefined) { if (settingsPath === undefined) {
settingsPath = getUserSettingsPath(); settingsPath = getUserSettingsPath();
} }
@ -107,7 +113,6 @@ export async function addToUserSettings(addSettings: IUserSettings, settingsPath
return writeUserSettings(userSettings, settingsPath); return writeUserSettings(userSettings, settingsPath);
} }
/** /**
* Writes a user settings file * Writes a user settings file
* *
@ -116,7 +121,10 @@ export async function addToUserSettings(addSettings: IUserSettings, settingsPath
* @param {string} [settingsPath] Optional settings file path * @param {string} [settingsPath] Optional settings file path
* @returns {Promise<IUserSettings>} * @returns {Promise<IUserSettings>}
*/ */
export async function writeUserSettings(userSettings: IUserSettings, settingsPath?: string): Promise<IUserSettings> { export async function writeUserSettings(
userSettings: IUserSettings,
settingsPath?: string,
): Promise<IUserSettings> {
if (settingsPath === undefined) { if (settingsPath === undefined) {
settingsPath = getUserSettingsPath(); settingsPath = getUserSettingsPath();
} }
@ -139,14 +147,16 @@ export async function writeUserSettings(userSettings: IUserSettings, settingsPat
return userSettings; return userSettings;
} }
/** /**
* Returns the content of the user settings * Returns the content of the user settings
* *
* @export * @export
* @returns {UserSettings} * @returns {UserSettings}
*/ */
export async function getUserSettings(settingsPath?: string, ignoreCache?: boolean): Promise<IUserSettings | undefined> { export async function getUserSettings(
settingsPath?: string,
ignoreCache?: boolean,
): Promise<IUserSettings | undefined> {
if (settingsCache !== undefined && ignoreCache !== true) { if (settingsCache !== undefined && ignoreCache !== true) {
return settingsCache; return settingsCache;
} }
@ -167,13 +177,14 @@ export async function getUserSettings(settingsPath?: string, ignoreCache?: boole
try { try {
settingsCache = JSON.parse(settingsFile); settingsCache = JSON.parse(settingsFile);
} catch (error) { } 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; return settingsCache as IUserSettings;
} }
/** /**
* Returns the path to the user settings * Returns the path to the user settings
* *
@ -186,8 +197,6 @@ export function getUserSettingsPath(): string {
return path.join(n8nFolder, USER_SETTINGS_FILE_NAME); return path.join(n8nFolder, USER_SETTINGS_FILE_NAME);
} }
/** /**
* Retruns the path to the n8n folder in which all n8n * Retruns the path to the n8n folder in which all n8n
* related data gets saved * related data gets saved
@ -206,7 +215,6 @@ export function getUserN8nFolderPath(): string {
return path.join(userFolder, USER_SETTINGS_SUBFOLDER); return path.join(userFolder, USER_SETTINGS_SUBFOLDER);
} }
/** /**
* Returns the path to the n8n user folder with the custom * Returns the path to the n8n user folder with the custom
* extensions like nodes and credentials * extensions like nodes and credentials
@ -218,7 +226,6 @@ export function getUserN8nFolderCustomExtensionPath(): string {
return path.join(getUserN8nFolderPath(), EXTENSIONS_SUBDIRECTORY); return path.join(getUserN8nFolderPath(), EXTENSIONS_SUBDIRECTORY);
} }
/** /**
* Returns the home folder path of the user if * Returns the home folder path of the user if
* none can be found it falls back to the current * none can be found it falls back to the current

View file

@ -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 * as PCancelable from 'p-cancelable';
import { import {
@ -20,23 +31,27 @@ import {
WorkflowExecuteMode, WorkflowExecuteMode,
WorkflowOperationError, WorkflowOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { // eslint-disable-next-line import/no-extraneous-dependencies
NodeExecuteFunctions,
} from './';
import { get } from 'lodash'; import { get } from 'lodash';
// eslint-disable-next-line import/no-cycle
import { NodeExecuteFunctions } from '.';
export class WorkflowExecute { export class WorkflowExecute {
runExecutionData: IRunExecutionData; runExecutionData: IRunExecutionData;
private additionalData: IWorkflowExecuteAdditionalData; private additionalData: IWorkflowExecuteAdditionalData;
private mode: WorkflowExecuteMode; private mode: WorkflowExecuteMode;
constructor(additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, runExecutionData?: IRunExecutionData) { constructor(
additionalData: IWorkflowExecuteAdditionalData,
mode: WorkflowExecuteMode,
runExecutionData?: IRunExecutionData,
) {
this.additionalData = additionalData; this.additionalData = additionalData;
this.mode = mode; this.mode = mode;
this.runExecutionData = runExecutionData || { this.runExecutionData = runExecutionData || {
startData: { startData: {},
},
resultData: { resultData: {
runData: {}, runData: {},
}, },
@ -48,8 +63,6 @@ export class WorkflowExecute {
}; };
} }
/** /**
* Executes the given workflow. * Executes the given workflow.
* *
@ -59,7 +72,8 @@ export class WorkflowExecute {
* @returns {(Promise<string>)} * @returns {(Promise<string>)}
* @memberof WorkflowExecute * @memberof WorkflowExecute
*/ */
run(workflow: Workflow, startNode?: INode, destinationNode?: string): PCancelable<IRun> { // @ts-ignore
async run(workflow: Workflow, startNode?: INode, destinationNode?: string): PCancelable<IRun> {
// Get the nodes to start workflow execution from // Get the nodes to start workflow execution from
startNode = startNode || workflow.getStartNode(destinationNode); 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 // 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) { if (destinationNode) {
runNodeFilter = workflow.getParentNodes(destinationNode); runNodeFilter = workflow.getParentNodes(destinationNode);
runNodeFilter.push(destinationNode); runNodeFilter.push(destinationNode);
@ -108,8 +122,6 @@ export class WorkflowExecute {
return this.processRunExecutionData(workflow); return this.processRunExecutionData(workflow);
} }
/** /**
* Executes the given workflow but only * Executes the given workflow but only
* *
@ -121,7 +133,13 @@ export class WorkflowExecute {
* @memberof WorkflowExecute * @memberof WorkflowExecute
*/ */
// @ts-ignore // @ts-ignore
async runPartialWorkflow(workflow: Workflow, runData: IRunData, startNodes: string[], destinationNode: string): PCancelable<IRun> { async runPartialWorkflow(
workflow: Workflow,
runData: IRunData,
startNodes: string[],
destinationNode: string,
// @ts-ignore
): PCancelable<IRun> {
let incomingNodeConnections: INodeConnections | undefined; let incomingNodeConnections: INodeConnections | undefined;
let connection: IConnection; let connection: IConnection;
@ -149,7 +167,8 @@ export class WorkflowExecute {
for (let inputIndex = 0; inputIndex < connections.length; inputIndex++) { for (let inputIndex = 0; inputIndex < connections.length; inputIndex++) {
connection = connections[inputIndex]; connection = connections[inputIndex];
incomingData.push( 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] = []; waitingExecution[destinationNode][runIndex][connection.type] = [];
} }
if (runData[connection.node] !== undefined) {
if (runData[connection.node!] !== undefined) {
// Input data exists so add as waiting // Input data exists so add as waiting
// incomingDataDestination.push(runData[connection.node!][runIndex].data![connection.type][connection.index]); // 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 { } else {
waitingExecution[destinationNode][runIndex][connection.type].push(null); waitingExecution[destinationNode][runIndex][connection.type].push(null);
} }
@ -196,7 +216,8 @@ export class WorkflowExecute {
} }
// Only run the parent nodes and no others // 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 = workflow.getParentNodes(destinationNode);
runNodeFilter.push(destinationNode); runNodeFilter.push(destinationNode);
@ -218,8 +239,6 @@ export class WorkflowExecute {
return this.processRunExecutionData(workflow); return this.processRunExecutionData(workflow);
} }
/** /**
* Executes the hook with the given name * Executes the hook with the given name
* *
@ -228,22 +247,31 @@ export class WorkflowExecute {
* @returns {Promise<IRun>} * @returns {Promise<IRun>}
* @memberof WorkflowExecute * @memberof WorkflowExecute
*/ */
async executeHook(hookName: string, parameters: any[]): Promise<void> { // tslint:disable-line:no-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
async executeHook(hookName: string, parameters: any[]): Promise<void> {
// tslint:disable-line:no-any
if (this.additionalData.hooks === undefined) { if (this.additionalData.hooks === undefined) {
return; return;
} }
// eslint-disable-next-line consistent-return
return this.additionalData.hooks.executeHookFunctions(hookName, parameters); return this.additionalData.hooks.executeHookFunctions(hookName, parameters);
} }
/** /**
* Checks the incoming connection does not receive any data * 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 workflow.connectionsByDestinationNode[nodeToAdd].main[0]) {
for (const inputConnection of inputConnections) { 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) { if (nodeIncomingData !== undefined && (nodeIncomingData as object[]).length !== 0) {
return false; return false;
} }
@ -251,79 +279,117 @@ export class WorkflowExecute {
return true; return true;
} }
addNodeToBeExecuted(
addNodeToBeExecuted(workflow: Workflow, connectionData: IConnection, outputIndex: number, parentNodeName: string, nodeSuccessData: INodeExecutionData[][], runIndex: number): void { workflow: Workflow,
connectionData: IConnection,
outputIndex: number,
parentNodeName: string,
nodeSuccessData: INodeExecutionData[][],
runIndex: number,
): void {
let stillDataMissing = false; let stillDataMissing = false;
// Check if node has multiple inputs as then we have to wait for all input data // 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 // 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 // Node has multiple inputs
let nodeWasWaiting = true; let nodeWasWaiting = true;
// Check if there is already data for the node // 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 // Node does not have data yet so create a new empty one
this.runExecutionData.executionData!.waitingExecution[connectionData.node] = {}; this.runExecutionData.executionData!.waitingExecution[connectionData.node] = {};
nodeWasWaiting = false; 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 // Node does not have data for runIndex yet so create also empty one and init it
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] = { this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] = {
main: [], main: [],
}; };
for (let i = 0; i < workflow.connectionsByDestinationNode[connectionData.node]['main'].length; i++) { for (
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main.push(null); 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 // Add the new data
if (nodeSuccessData === null) { 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 { } 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 // Check if all data exists now
let thisExecutionData: INodeExecutionData[] | null; let thisExecutionData: INodeExecutionData[] | null;
let allDataFound = true; let allDataFound = true;
for (let i = 0; i < this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main.length; i++) { for (
thisExecutionData = this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main[i]; 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) { if (thisExecutionData === null) {
allDataFound = false; allDataFound = false;
break; break;
} }
} }
if (allDataFound === true) { if (allDataFound) {
// All data exists for node to be executed // All data exists for node to be executed
// So add it to the execution stack // So add it to the execution stack
this.runExecutionData.executionData!.nodeExecutionStack.push({ this.runExecutionData.executionData!.nodeExecutionStack.push({
node: workflow.nodes[connectionData.node], 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 // Remove the data from waiting
delete this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex]; 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 // No more data left for the node so also delete that one
delete this.runExecutionData.executionData!.waitingExecution[connectionData.node]; delete this.runExecutionData.executionData!.waitingExecution[connectionData.node];
} }
return; 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 // Get a list of all the output nodes that we can check for siblings easier
const checkOutputNodes = []; const checkOutputNodes = [];
// eslint-disable-next-line @typescript-eslint/no-for-in-array
for (const outputIndexParent in workflow.connectionsBySourceNode[parentNodeName].main) { for (const outputIndexParent in workflow.connectionsBySourceNode[parentNodeName].main) {
if (!workflow.connectionsBySourceNode[parentNodeName].main.hasOwnProperty(outputIndexParent)) { if (
!workflow.connectionsBySourceNode[parentNodeName].main.hasOwnProperty(outputIndexParent)
) {
continue; continue;
} }
for (const connectionDataCheck of workflow.connectionsBySourceNode[parentNodeName].main[outputIndexParent]) { for (const connectionDataCheck of workflow.connectionsBySourceNode[parentNodeName].main[
outputIndexParent
]) {
checkOutputNodes.push(connectionDataCheck.node); 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 // checked. So we have to go through all the inputs and check if they
// are already on the list to be processed. // are already on the list to be processed.
// If that is not the case add it. // If that is not the case add it.
for (let inputIndex = 0; inputIndex < workflow.connectionsByDestinationNode[connectionData.node]['main'].length; inputIndex++) { for (
for (const inputData of workflow.connectionsByDestinationNode[connectionData.node]['main'][inputIndex]) { 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) { if (inputData.node === parentNodeName) {
// Is the node we come from so its data will be available for sure // Is the node we come from so its data will be available for sure
continue; 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 // Check if that node is also an output connection of the
// previously processed one // previously processed one
@ -348,7 +422,13 @@ export class WorkflowExecute {
// will then process this node next. So nothing to do // will then process this node next. So nothing to do
// unless the incoming data of the node is empty // unless the incoming data of the node is empty
// because then it would not be executed // 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; continue;
} }
} }
@ -401,7 +481,10 @@ export class WorkflowExecute {
nodeToAdd = parentNode; nodeToAdd = parentNode;
} }
const parentNodesNodeToAdd = workflow.getParentNodes(nodeToAdd as string); 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 // 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 // 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. // specifically not run, as it did not receive any data.
@ -418,18 +501,21 @@ export class WorkflowExecute {
if (workflow.connectionsByDestinationNode[nodeToAdd] === undefined) { if (workflow.connectionsByDestinationNode[nodeToAdd] === undefined) {
// Add empty item if the node does not have any input connections // Add empty item if the node does not have any input connections
addEmptyItem = true; addEmptyItem = true;
} else { } else if (
if (this.incomingConnectionIsEmpty(this.runExecutionData.resultData.runData, workflow.connectionsByDestinationNode[nodeToAdd].main[0], runIndex)) { this.incomingConnectionIsEmpty(
this.runExecutionData.resultData.runData,
workflow.connectionsByDestinationNode[nodeToAdd].main[0],
runIndex,
)
) {
// Add empty item also if the input data is empty // Add empty item also if the input data is empty
addEmptyItem = true; addEmptyItem = true;
} }
}
if (addEmptyItem === true) { if (addEmptyItem) {
// Add only node if it does not have any inputs because else it will // Add only node if it does not have any inputs because else it will
// be added by its input node later anyway. // be added by its input node later anyway.
this.runExecutionData.executionData!.nodeExecutionStack.push( this.runExecutionData.executionData!.nodeExecutionStack.push({
{
node: workflow.getNode(nodeToAdd) as INode, node: workflow.getNode(nodeToAdd) as INode,
data: { data: {
main: [ main: [
@ -440,8 +526,7 @@ export class WorkflowExecute {
], ],
], ],
}, },
}, });
);
} }
} }
} }
@ -461,9 +546,11 @@ export class WorkflowExecute {
connectionDataArray[connectionData.index] = nodeSuccessData[outputIndex]; connectionDataArray[connectionData.index] = nodeSuccessData[outputIndex];
} }
if (stillDataMissing === true) { if (stillDataMissing) {
// Additional data is needed to run node so add it to waiting // 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] = {};
} }
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] = { this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] = {
@ -480,7 +567,6 @@ export class WorkflowExecute {
} }
} }
/** /**
* Runs the given execution data. * Runs the given execution data.
* *
@ -488,14 +574,17 @@ export class WorkflowExecute {
* @returns {Promise<string>} * @returns {Promise<string>}
* @memberof WorkflowExecute * @memberof WorkflowExecute
*/ */
processRunExecutionData(workflow: Workflow): PCancelable<IRun> { // @ts-ignore
async processRunExecutionData(workflow: Workflow): PCancelable<IRun> {
Logger.verbose('Workflow execution started', { workflowId: workflow.id }); Logger.verbose('Workflow execution started', { workflowId: workflow.id });
const startedAt = new Date(); const startedAt = new Date();
const workflowIssues = workflow.checkReadyForExecution(); const workflowIssues = workflow.checkReadyForExecution();
if (workflowIssues !== null) { 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 // Variables which hold temporary data for each node-execution
@ -521,7 +610,7 @@ export class WorkflowExecute {
let currentExecutionTry = ''; let currentExecutionTry = '';
let lastExecutionTry = ''; let lastExecutionTry = '';
return new PCancelable((resolve, reject, onCancel) => { return new PCancelable(async (resolve, reject, onCancel) => {
let gotCancel = false; let gotCancel = false;
onCancel.shouldReject = false; onCancel.shouldReject = false;
@ -533,7 +622,6 @@ export class WorkflowExecute {
try { try {
await this.executeHook('workflowExecuteBefore', [workflow]); await this.executeHook('workflowExecuteBefore', [workflow]);
} catch (error) { } catch (error) {
// Set the error that it can be saved correctly // Set the error that it can be saved correctly
executionError = { executionError = {
...error, ...error,
@ -542,16 +630,17 @@ export class WorkflowExecute {
}; };
// Set the incoming data of the node that it can be saved correctly // 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 = { this.runExecutionData.resultData = {
runData: { runData: {
[executionData.node.name]: [ [executionData.node.name]: [
{ {
startTime, startTime,
executionTime: (new Date().getTime()) - startTime, executionTime: new Date().getTime() - startTime,
data: ({ data: {
'main': executionData.data.main, main: executionData.data.main,
} as ITaskDataConnections), } as ITaskDataConnections,
}, },
], ],
}, },
@ -562,24 +651,31 @@ export class WorkflowExecute {
throw error; throw error;
} }
executionLoop: executionLoop: while (
while (this.runExecutionData.executionData!.nodeExecutionStack.length !== 0) { this.runExecutionData.executionData!.nodeExecutionStack.length !== 0
) {
if (this.additionalData.executionTimeoutTimestamp !== undefined && Date.now() >= this.additionalData.executionTimeoutTimestamp) { if (
this.additionalData.executionTimeoutTimestamp !== undefined &&
Date.now() >= this.additionalData.executionTimeoutTimestamp
) {
gotCancel = true; gotCancel = true;
} }
// @ts-ignore // @ts-ignore
if (gotCancel === true) { if (gotCancel) {
return Promise.resolve(); return Promise.resolve();
} }
nodeSuccessData = null; nodeSuccessData = null;
executionError = undefined; executionError = undefined;
executionData = this.runExecutionData.executionData!.nodeExecutionStack.shift() as IExecuteData; executionData =
this.runExecutionData.executionData!.nodeExecutionStack.shift() as IExecuteData;
executionNode = executionData.node; 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]); await this.executeHook('nodeExecuteBefore', [executionNode.name]);
// Get the index of the current run // 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.'); 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 // 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 // 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. // they have the same parent and it executes all child nodes.
@ -608,17 +707,24 @@ export class WorkflowExecute {
let inputConnections: IConnection[][]; let inputConnections: IConnection[][];
let connectionIndex: number; 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++) { for (
if (workflow.getHighestNode(executionNode.name, 'main', connectionIndex).length === 0) { 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) // 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 // then ignore that it has inputs and simply execute it as it is without
// any data // any data
continue; continue;
} }
if (!executionData.data!.hasOwnProperty('main')) { if (!executionData.data.hasOwnProperty('main')) {
// ExecutionData does not even have the connection set up so can // ExecutionData does not even have the connection set up so can
// not have that data, so add it again to be executed later // not have that data, so add it again to be executed later
this.runExecutionData.executionData!.nodeExecutionStack.push(executionData); this.runExecutionData.executionData!.nodeExecutionStack.push(executionData);
@ -629,7 +735,10 @@ export class WorkflowExecute {
// Check if it has the data for all the inputs // 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 // 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. // 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 // Does not have the data of the connections so add back to stack
this.runExecutionData.executionData!.nodeExecutionStack.push(executionData); this.runExecutionData.executionData!.nodeExecutionStack.push(executionData);
lastExecutionTry = currentExecutionTry; lastExecutionTry = currentExecutionTry;
@ -653,22 +762,25 @@ export class WorkflowExecute {
let waitBetweenTries = 0; let waitBetweenTries = 0;
if (executionData.node.retryOnFail === true) { if (executionData.node.retryOnFail === true) {
// TODO: Remove the hardcoded default-values here and also in NodeSettings.vue // 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++) { for (let tryIndex = 0; tryIndex < maxTries; tryIndex++) {
// @ts-ignore // @ts-ignore
if (gotCancel === true) { if (gotCancel) {
return Promise.resolve(); return Promise.resolve();
} }
try { try {
if (tryIndex !== 0) { if (tryIndex !== 0) {
// Reset executionError from previous error try // Reset executionError from previous error try
executionError = undefined; executionError = undefined;
if (waitBetweenTries !== 0) { if (waitBetweenTries !== 0) {
// TODO: Improve that in the future and check if other nodes can // TODO: Improve that in the future and check if other nodes can
// be executed in the meantime // be executed in the meantime
// eslint-disable-next-line @typescript-eslint/no-shadow
await new Promise((resolve) => { await new Promise((resolve) => {
setTimeout(() => { setTimeout(() => {
resolve(undefined); resolve(undefined);
@ -677,9 +789,23 @@ export class WorkflowExecute {
} }
} }
Logger.debug(`Running node "${executionNode.name}" started`, { node: executionNode.name, workflowId: workflow.id }); Logger.debug(`Running node "${executionNode.name}" started`, {
nodeSuccessData = await workflow.runNode(executionData.node, executionData.data, this.runExecutionData, runIndex, this.additionalData, NodeExecuteFunctions, this.mode); node: executionNode.name,
Logger.debug(`Running node "${executionNode.name}" finished successfully`, { node: executionNode.name, workflowId: workflow.id }); 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) { if (nodeSuccessData === undefined) {
// Node did not get executed // 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 // If null gets returned it means that the node did succeed
// but did not have any data. So the branch should end // but did not have any data. So the branch should end
// (meaning the nodes afterwards should not be processed) // (meaning the nodes afterwards should not be processed)
@ -708,7 +834,6 @@ export class WorkflowExecute {
break; break;
} catch (error) { } catch (error) {
this.runExecutionData.resultData.lastNodeExecuted = executionData.node.name; this.runExecutionData.resultData.lastNodeExecuted = executionData.node.name;
executionError = { executionError = {
@ -717,7 +842,10 @@ export class WorkflowExecute {
stack: error.stack, 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 = { taskData = {
startTime, startTime,
executionTime: (new Date().getTime()) - startTime, executionTime: new Date().getTime() - startTime,
}; };
if (executionError !== undefined) { 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 // Simply get the input data of the node if it has any and pass it through
// to the next node // to the next node
if (executionData.data.main[0] !== null) { if (executionData.data.main[0] !== null) {
nodeSuccessData = [executionData.data.main[0] as INodeExecutionData[]]; nodeSuccessData = [executionData.data.main[0]];
} }
} }
} else { } else {
@ -751,30 +879,46 @@ export class WorkflowExecute {
// Add the execution data again so that it can get restarted // Add the execution data again so that it can get restarted
this.runExecutionData.executionData!.nodeExecutionStack.unshift(executionData); 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; break;
} }
} }
// Node executed successfully. So add data and go on. // Node executed successfully. So add data and go on.
taskData.data = ({ taskData.data = {
'main': nodeSuccessData, main: nodeSuccessData,
} as ITaskDataConnections); } as ITaskDataConnections;
this.runExecutionData.resultData.runData[executionNode.name].push(taskData); 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 // Before stopping, make sure we are executing hooks so
// That frontend is notified for example for manual executions. // 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 // If destination node is defined and got executed stop execution
continue; continue;
} }
if (this.runExecutionData.waitTill!!) { if (this.runExecutionData.waitTill!) {
await this.executeHook('nodeExecuteAfter', [executionNode.name, taskData, this.runExecutionData]); 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 // Add the node back to the stack that the workflow can start to execute again from that node
this.runExecutionData.executionData!.nodeExecutionStack.unshift(executionData); this.runExecutionData.executionData!.nodeExecutionStack.unshift(executionData);
@ -786,24 +930,46 @@ export class WorkflowExecute {
// be executed next // be executed next
if (workflow.connectionsBySourceNode.hasOwnProperty(executionNode.name)) { if (workflow.connectionsBySourceNode.hasOwnProperty(executionNode.name)) {
if (workflow.connectionsBySourceNode[executionNode.name].hasOwnProperty('main')) { if (workflow.connectionsBySourceNode[executionNode.name].hasOwnProperty('main')) {
let outputIndex: string, connectionData: IConnection; let outputIndex: string;
let connectionData: IConnection;
// Iterate over all the outputs // Iterate over all the outputs
// Add the nodes to be executed // Add the nodes to be executed
for (outputIndex in workflow.connectionsBySourceNode[executionNode.name]['main']) { // eslint-disable-next-line @typescript-eslint/no-for-in-array
if (!workflow.connectionsBySourceNode[executionNode.name]['main'].hasOwnProperty(outputIndex)) { for (outputIndex in workflow.connectionsBySourceNode[executionNode.name].main) {
if (
!workflow.connectionsBySourceNode[executionNode.name].main.hasOwnProperty(
outputIndex,
)
) {
continue; continue;
} }
// Iterate over all the different connections of this output // 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)) { 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 // 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,15 +980,22 @@ export class WorkflowExecute {
// Execute hooks now to make sure that all hooks are executed properly // 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 // Await is needed to make sure that we don't fall into concurrency problems
// When saving node execution data // 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(); return Promise.resolve();
})() })()
.then(async () => { .then(async () => {
if (gotCancel && executionError === undefined) { if (gotCancel && executionError === undefined) {
return this.processSuccessExecution(startedAt, workflow, new WorkflowOperationError('Workflow has been canceled or timed out!')); return this.processSuccessExecution(
startedAt,
workflow,
new WorkflowOperationError('Workflow has been canceled or timed out!'),
);
} }
return this.processSuccessExecution(startedAt, workflow, executionError); return this.processSuccessExecution(startedAt, workflow, executionError);
}) })
@ -837,13 +1010,18 @@ export class WorkflowExecute {
// Check if static data changed // Check if static data changed
let newStaticData: IDataObject | undefined; let newStaticData: IDataObject | undefined;
// eslint-disable-next-line no-underscore-dangle
if (workflow.staticData.__dataChanged === true) { if (workflow.staticData.__dataChanged === true) {
// Static data of workflow changed // Static data of workflow changed
newStaticData = workflow.staticData; newStaticData = workflow.staticData;
} }
await this.executeHook('workflowExecuteAfter', [fullRunData, newStaticData]).catch(error => { 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); console.error('There was a problem running hook "workflowExecuteAfter"', error);
}); },
);
return fullRunData; return fullRunData;
}); });
@ -852,20 +1030,29 @@ export class WorkflowExecute {
}); });
} }
async processSuccessExecution(
startedAt: Date,
workflow: Workflow,
executionError?: ExecutionError,
// @ts-ignore // @ts-ignore
async processSuccessExecution(startedAt: Date, workflow: Workflow, executionError?: ExecutionError): PCancelable<IRun> { ): PCancelable<IRun> {
const fullRunData = this.getFullRunData(startedAt); const fullRunData = this.getFullRunData(startedAt);
if (executionError !== undefined) { 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 = { fullRunData.data.resultData.error = {
...executionError, ...executionError,
message: executionError.message, message: executionError.message,
stack: executionError.stack, stack: executionError.stack,
} as ExecutionError; } as ExecutionError;
} else if (this.runExecutionData.waitTill!!) { } else if (this.runExecutionData.waitTill!) {
Logger.verbose(`Workflow execution will wait until ${this.runExecutionData.waitTill}`, { workflowId: workflow.id }); // 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; fullRunData.waitTill = this.runExecutionData.waitTill;
} else { } else {
Logger.verbose(`Workflow execution finished successfully`, { workflowId: workflow.id }); Logger.verbose(`Workflow execution finished successfully`, { workflowId: workflow.id });
@ -874,6 +1061,7 @@ export class WorkflowExecute {
// Check if static data changed // Check if static data changed
let newStaticData: IDataObject | undefined; let newStaticData: IDataObject | undefined;
// eslint-disable-next-line no-underscore-dangle
if (workflow.staticData.__dataChanged === true) { if (workflow.staticData.__dataChanged === true) {
// Static data of workflow changed // Static data of workflow changed
newStaticData = workflow.staticData; newStaticData = workflow.staticData;
@ -894,5 +1082,4 @@ export class WorkflowExecute {
return fullRunData; return fullRunData;
} }
} }

View file

@ -1,8 +1,12 @@
try { /* eslint-disable import/no-cycle */
require('source-map-support').install(); import * as NodeExecuteFunctions from './NodeExecuteFunctions';
} catch (error) { 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 './ActiveWorkflows';
export * from './ActiveWebhooks'; export * from './ActiveWebhooks';
@ -13,10 +17,4 @@ export * from './Interfaces';
export * from './LoadNodeParameterOptions'; export * from './LoadNodeParameterOptions';
export * from './NodeExecuteFunctions'; export * from './NodeExecuteFunctions';
export * from './WorkflowExecute'; export * from './WorkflowExecute';
export { NodeExecuteFunctions, UserSettings };
import * as NodeExecuteFunctions from './NodeExecuteFunctions';
import * as UserSettings from './UserSettings';
export {
NodeExecuteFunctions,
UserSettings,
};

View file

@ -1,12 +1,8 @@
import { Credentials } from '../src'; import { Credentials } from '../src';
describe('Credentials', () => { describe('Credentials', () => {
describe('without nodeType set', () => { describe('without nodeType set', () => {
test('should be able to set and read key data without initial data set', () => { test('should be able to set and read key data without initial data set', () => {
const credentials = new Credentials('testName', 'testType', []); const credentials = new Credentials('testName', 'testType', []);
const key = 'key1'; const key = 'key1';
@ -20,7 +16,6 @@ describe('Credentials', () => {
}); });
test('should be able to set and read key data with initial data set', () => { test('should be able to set and read key data with initial data set', () => {
const key = 'key2'; const key = 'key2';
const password = 'password'; const password = 'password';
@ -39,13 +34,10 @@ describe('Credentials', () => {
// Read the data which got provided encrypted on init // Read the data which got provided encrypted on init
expect(credentials.getDataKey('key1', password)).toEqual(initialData); expect(credentials.getDataKey('key1', password)).toEqual(initialData);
}); });
}); });
describe('with nodeType set', () => { describe('with nodeType set', () => {
test('should be able to set and read key data without initial data set', () => { test('should be able to set and read key data without initial data set', () => {
const nodeAccess = [ const nodeAccess = [
{ {
nodeType: 'base.noOp', nodeType: 'base.noOp',
@ -72,7 +64,9 @@ describe('Credentials', () => {
credentials.getDataKey(key, password, 'base.otherNode'); credentials.getDataKey(key, password, 'base.otherNode');
expect(true).toBe(false); expect(true).toBe(false);
} catch (e) { } catch (e) {
expect(e.message).toBe('The node of type "base.otherNode" does not have access to credentials "testName" of type "testType".'); 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 // Get the data which will be saved in database
@ -81,8 +75,9 @@ describe('Credentials', () => {
expect(dbData.type).toEqual('testType'); expect(dbData.type).toEqual('testType');
expect(dbData.nodesAccess).toEqual(nodeAccess); expect(dbData.nodesAccess).toEqual(nodeAccess);
// Compare only the first 6 characters as the rest seems to change with each execution // 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)); expect(dbData.data!.slice(0, 6)).toEqual(
'U2FsdGVkX1+wpQWkj+YTzaPSNTFATjnlmFKIsUTZdhk='.slice(0, 6),
);
}); });
}); });
}); });

View file

@ -18,30 +18,27 @@ import {
WorkflowHooks, WorkflowHooks,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import { Credentials, IDeferredPromise, IExecuteFunctions } from '../src';
Credentials,
IDeferredPromise,
IExecuteFunctions,
} from '../src';
export class CredentialsHelper extends ICredentialsHelper { export class CredentialsHelper extends ICredentialsHelper {
getDecrypted(name: string, type: string): Promise<ICredentialDataDecryptedObject> { getDecrypted(name: string, type: string): Promise<ICredentialDataDecryptedObject> {
return new Promise(res => res({})); return new Promise((res) => res({}));
} }
getCredentials(name: string, type: string): Promise<Credentials> { getCredentials(name: string, type: string): Promise<Credentials> {
return new Promise(res => { return new Promise((res) => {
res(new Credentials('', '', [], '')); res(new Credentials('', '', [], ''));
}); });
} }
async updateCredentials(name: string, type: string, data: ICredentialDataDecryptedObject): Promise<void> {} async updateCredentials(
name: string,
type: string,
data: ICredentialDataDecryptedObject,
): Promise<void> {}
} }
class NodeTypesClass implements INodeTypes { class NodeTypesClass implements INodeTypes {
nodeTypes: INodeTypeData = { nodeTypes: INodeTypeData = {
'n8n-nodes-base.if': { 'n8n-nodes-base.if': {
sourcePath: '', sourcePath: '',
@ -161,9 +158,7 @@ class NodeTypesClass implements INodeTypes {
type: 'number', type: 'number',
displayOptions: { displayOptions: {
hide: { hide: {
operation: [ operation: ['isEmpty'],
'isEmpty',
],
}, },
}, },
default: 0, default: 0,
@ -229,10 +224,7 @@ class NodeTypesClass implements INodeTypes {
type: 'string', type: 'string',
displayOptions: { displayOptions: {
hide: { hide: {
operation: [ operation: ['isEmpty', 'regex'],
'isEmpty',
'regex',
],
}, },
}, },
default: '', default: '',
@ -244,9 +236,7 @@ class NodeTypesClass implements INodeTypes {
type: 'string', type: 'string',
displayOptions: { displayOptions: {
show: { show: {
operation: [ operation: ['regex'],
'regex',
],
}, },
}, },
default: '', default: '',
@ -274,7 +264,8 @@ class NodeTypesClass implements INodeTypes {
}, },
], ],
default: 'all', 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: { const compareOperationFunctions: {
[key: string]: (value1: NodeParameterValue, value2: NodeParameterValue) => boolean; [key: string]: (value1: NodeParameterValue, value2: NodeParameterValue) => boolean;
} = { } = {
contains: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || '').toString().includes((value2 || '').toString()), contains: (value1: NodeParameterValue, value2: NodeParameterValue) =>
notContains: (value1: NodeParameterValue, value2: NodeParameterValue) => !(value1 || '').toString().includes((value2 || '').toString()), (value1 || '').toString().includes((value2 || '').toString()),
endsWith: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 as string).endsWith(value2 as string), 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, equal: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 === value2,
notEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 !== value2, notEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 !== value2,
larger: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) > (value2 || 0), larger: (value1: NodeParameterValue, value2: NodeParameterValue) =>
largerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) >= (value2 || 0), (value1 || 0) > (value2 || 0),
smaller: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) < (value2 || 0), largerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) =>
smallerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) <= (value2 || 0), (value1 || 0) >= (value2 || 0),
startsWith: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 as string).startsWith(value2 as string), smaller: (value1: NodeParameterValue, value2: NodeParameterValue) =>
isEmpty: (value1: NodeParameterValue) => [undefined, null, ''].includes(value1 as string), (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) => { regex: (value1: NodeParameterValue, value2: NodeParameterValue) => {
const regexMatch = (value2 || '').toString().match(new RegExp('^/(.*?)/([gimusy]*)$')); const regexMatch = (value2 || '')
.toString()
.match(new RegExp('^/(.*?)/([gimusy]*)$'));
let regex: RegExp; let regex: RegExp;
if (!regexMatch) { if (!regexMatch) {
@ -319,18 +321,13 @@ class NodeTypesClass implements INodeTypes {
}; };
// The different dataTypes to check the values in // The different dataTypes to check the values in
const dataTypes = [ const dataTypes = ['boolean', 'number', 'string'];
'boolean',
'number',
'string',
];
// Itterate over all items to check which ones should be output as via output "true" and // Itterate over all items to check which ones should be output as via output "true" and
// which ones via output "false" // which ones via output "false"
let dataType: string; let dataType: string;
let compareOperationResult: boolean; let compareOperationResult: boolean;
itemLoop: itemLoop: for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
item = items[itemIndex]; item = items[itemIndex];
let compareData: INodeParameters; let compareData: INodeParameters;
@ -340,9 +337,16 @@ class NodeTypesClass implements INodeTypes {
// Check all the values of the different dataTypes // Check all the values of the different dataTypes
for (dataType of dataTypes) { for (dataType of dataTypes) {
// Check all the values of the current dataType // 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 // 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 (compareOperationResult === true && combineOperation === 'any') {
// If it passes and the operation is "any" we do not have to check 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', name: 'Append',
value: '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', name: 'Pass-through',
value: 'passThrough', 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', name: 'Wait',
value: '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', default: 'append',
description: 'How data should be merged. If it should simply<br />be appended or merged depending on a property.', description:
'How data should be merged. If it should simply<br />be appended or merged depending on a property.',
}, },
{ {
displayName: 'Output Data', displayName: 'Output Data',
@ -419,9 +427,7 @@ class NodeTypesClass implements INodeTypes {
type: 'options', type: 'options',
displayOptions: { displayOptions: {
show: { show: {
mode: [ mode: ['passThrough'],
'passThrough',
],
}, },
}, },
options: [ options: [
@ -512,7 +518,8 @@ class NodeTypesClass implements INodeTypes {
name: 'keepOnlySet', name: 'keepOnlySet',
type: 'boolean', type: 'boolean',
default: false, default: false,
description: 'If only the values set on this node should be<br />kept and all others removed.', description:
'If only the values set on this node should be<br />kept and all others removed.',
}, },
{ {
displayName: 'Values to Set', displayName: 'Values to Set',
@ -534,7 +541,8 @@ class NodeTypesClass implements INodeTypes {
name: 'name', name: 'name',
type: 'string', type: 'string',
default: 'propertyName', default: 'propertyName',
description: 'Name of the property to write data to.<br />Supports dot-notation.<br />Example: "data.person[0].name"', description:
'Name of the property to write data to.<br />Supports dot-notation.<br />Example: "data.person[0].name"',
}, },
{ {
displayName: 'Value', displayName: 'Value',
@ -554,7 +562,8 @@ class NodeTypesClass implements INodeTypes {
name: 'name', name: 'name',
type: 'string', type: 'string',
default: 'propertyName', default: 'propertyName',
description: 'Name of the property to write data to.<br />Supports dot-notation.<br />Example: "data.person[0].name"', description:
'Name of the property to write data to.<br />Supports dot-notation.<br />Example: "data.person[0].name"',
}, },
{ {
displayName: 'Value', displayName: 'Value',
@ -574,7 +583,8 @@ class NodeTypesClass implements INodeTypes {
name: 'name', name: 'name',
type: 'string', type: 'string',
default: 'propertyName', default: 'propertyName',
description: 'Name of the property to write data to.<br />Supports dot-notation.<br />Example: "data.person[0].name"', description:
'Name of the property to write data to.<br />Supports dot-notation.<br />Example: "data.person[0].name"',
}, },
{ {
displayName: 'Value', displayName: 'Value',
@ -610,7 +620,6 @@ class NodeTypesClass implements INodeTypes {
], ],
}, },
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData(); const items = this.getInputData();
if (items.length === 0) { if (items.length === 0) {
@ -643,31 +652,37 @@ class NodeTypesClass implements INodeTypes {
} }
// Add boolean values // Add boolean values
(this.getNodeParameter('values.boolean', itemIndex, []) as INodeParameters[]).forEach((setItem) => { (this.getNodeParameter('values.boolean', itemIndex, []) as INodeParameters[]).forEach(
(setItem) => {
if (options.dotNotation === false) { if (options.dotNotation === false) {
newItem.json[setItem.name as string] = !!setItem.value; newItem.json[setItem.name as string] = !!setItem.value;
} else { } else {
set(newItem.json, setItem.name as string, !!setItem.value); set(newItem.json, setItem.name as string, !!setItem.value);
} }
}); },
);
// Add number values // Add number values
(this.getNodeParameter('values.number', itemIndex, []) as INodeParameters[]).forEach((setItem) => { (this.getNodeParameter('values.number', itemIndex, []) as INodeParameters[]).forEach(
(setItem) => {
if (options.dotNotation === false) { if (options.dotNotation === false) {
newItem.json[setItem.name as string] = setItem.value; newItem.json[setItem.name as string] = setItem.value;
} else { } else {
set(newItem.json, setItem.name as string, setItem.value); set(newItem.json, setItem.name as string, setItem.value);
} }
}); },
);
// Add string values // Add string values
(this.getNodeParameter('values.string', itemIndex, []) as INodeParameters[]).forEach((setItem) => { (this.getNodeParameter('values.string', itemIndex, []) as INodeParameters[]).forEach(
(setItem) => {
if (options.dotNotation === false) { if (options.dotNotation === false) {
newItem.json[setItem.name as string] = setItem.value; newItem.json[setItem.name as string] = setItem.value;
} else { } else {
set(newItem.json, setItem.name as string, setItem.value); set(newItem.json, setItem.name as string, setItem.value);
} }
}); },
);
returnData.push(newItem); returnData.push(newItem);
} }
@ -702,7 +717,7 @@ class NodeTypesClass implements INodeTypes {
}, },
}; };
async init(nodeTypes: INodeTypeData): Promise<void> { } async init(nodeTypes: INodeTypeData): Promise<void> {}
getAll(): INodeType[] { getAll(): INodeType[] {
return Object.values(this.nodeTypes).map((data) => data.type); return Object.values(this.nodeTypes).map((data) => data.type);
@ -715,7 +730,6 @@ class NodeTypesClass implements INodeTypes {
let nodeTypesInstance: NodeTypesClass | undefined; let nodeTypesInstance: NodeTypesClass | undefined;
export function NodeTypes(): NodeTypesClass { export function NodeTypes(): NodeTypesClass {
if (nodeTypesInstance === undefined) { if (nodeTypesInstance === undefined) {
nodeTypesInstance = new NodeTypesClass(); nodeTypesInstance = new NodeTypesClass();
@ -725,8 +739,10 @@ export function NodeTypes(): NodeTypesClass {
return nodeTypesInstance; return nodeTypesInstance;
} }
export function WorkflowExecuteAdditionalData(
export function WorkflowExecuteAdditionalData(waitPromise: IDeferredPromise<IRun>, nodeExecutionOrder: string[]): IWorkflowExecuteAdditionalData { waitPromise: IDeferredPromise<IRun>,
nodeExecutionOrder: string[],
): IWorkflowExecuteAdditionalData {
const hookFunctions = { const hookFunctions = {
nodeExecuteAfter: [ nodeExecuteAfter: [
async (nodeName: string, data: ITaskData): Promise<void> => { async (nodeName: string, data: ITaskData): Promise<void> => {
@ -752,7 +768,7 @@ export function WorkflowExecuteAdditionalData(waitPromise: IDeferredPromise<IRun
return { return {
credentialsHelper: new CredentialsHelper(''), credentialsHelper: new CredentialsHelper(''),
hooks: new WorkflowHooks(hookFunctions, 'trigger', '1', workflowData), hooks: new WorkflowHooks(hookFunctions, 'trigger', '1', workflowData),
executeWorkflow: async (workflowInfo: IExecuteWorkflowInfo): Promise<any> => {}, // tslint:disable-line:no-any executeWorkflow: async (workflowInfo: IExecuteWorkflowInfo): Promise<any> => {},
sendMessageToUI: (message: string) => {}, sendMessageToUI: (message: string) => {},
restApiUrl: '', restApiUrl: '',
encryptionKey: 'test', encryptionKey: 'test',

File diff suppressed because it is too large Load diff

View file

@ -20,11 +20,10 @@
"build:storybook": "build-storybook", "build:storybook": "build-storybook",
"storybook": "start-storybook -p 6006", "storybook": "start-storybook -p 6006",
"test:unit": "vue-cli-service test:unit --passWithNoTests", "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", "build:theme": "gulp build:theme",
"watch:theme": "gulp watch:theme", "watch:theme": "gulp watch:theme"
"tslint": "tslint -p tsconfig.json -c tslint.json",
"tslintfix": "tslint --fix -p tsconfig.json -c tslint.json"
}, },
"peerDependencies": { "peerDependencies": {
"@fortawesome/fontawesome-svg-core": "1.x", "@fortawesome/fontawesome-svg-core": "1.x",
@ -49,27 +48,27 @@
"@storybook/addon-links": "^6.3.6", "@storybook/addon-links": "^6.3.6",
"@storybook/vue": "^6.3.6", "@storybook/vue": "^6.3.6",
"@types/jest": "^26.0.13", "@types/jest": "^26.0.13",
"@typescript-eslint/eslint-plugin": "^4.18.0", "@typescript-eslint/eslint-plugin": "^4.29.0",
"@typescript-eslint/parser": "^4.18.0", "@typescript-eslint/parser": "^4.29.0",
"@vue/cli-plugin-babel": "~4.5.0", "@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~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-plugin-unit-jest": "~4.5.0",
"@vue/cli-service": "~4.5.0", "@vue/cli-service": "~4.5.0",
"@vue/eslint-config-prettier": "^6.0.0", "@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-typescript": "^7.0.0", "@vue/eslint-config-typescript": "^7.0.0",
"@vue/test-utils": "^1.0.3", "@vue/test-utils": "^1.0.3",
"babel-loader": "^8.2.2", "babel-loader": "^8.2.2",
"eslint": "^6.8.0", "eslint": "^7.32.0",
"eslint-plugin-prettier": "^3.3.1", "eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-vue": "^6.2.2", "eslint-plugin-vue": "^7.16.0",
"fibers": "^5.0.0", "fibers": "^5.0.0",
"gulp": "^4.0.0", "gulp": "^4.0.0",
"prettier": "^2.2.1", "prettier": "^2.3.2",
"sass": "^1.26.5", "sass": "^1.26.5",
"sass-loader": "^8.0.2", "sass-loader": "^8.0.2",
"storybook-addon-designs": "^6.0.1", "storybook-addon-designs": "^6.0.1",
"typescript": "~3.9.7", "typescript": "~4.3.5",
"vue-loader": "^15.9.7", "vue-loader": "^15.9.7",
"vue-template-compiler": "^2.6.11", "vue-template-compiler": "^2.6.11",
"gulp-autoprefixer": "^4.0.0", "gulp-autoprefixer": "^4.0.0",

View file

@ -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

View file

@ -6,5 +6,8 @@ module.exports = {
// transpileDependencies: [ // transpileDependencies: [
// /\/node_modules\/quill/ // /\/node_modules\/quill/
// ] // ]
plugins: [
"@babel/plugin-proposal-class-properties",
],
}; };
// // https://stackoverflow.com/questions/44625868/es6-babel-class-constructor-cannot-be-invoked-without-new // // https://stackoverflow.com/questions/44625868/es6-babel-class-constructor-cannot-be-invoked-without-new

View file

@ -16,11 +16,11 @@
"scripts": { "scripts": {
"build": "cross-env VUE_APP_PUBLIC_PATH=\"/%BASE_PATH%/\" vue-cli-service build", "build": "cross-env VUE_APP_PUBLIC_PATH=\"/%BASE_PATH%/\" vue-cli-service build",
"dev": "npm run serve", "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", "serve": "cross-env VUE_APP_URL_BASE_API=http://localhost:5678/ vue-cli-service serve",
"test": "npm run test:unit", "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:e2e": "vue-cli-service test:e2e",
"test:unit": "vue-cli-service test:unit" "test:unit": "vue-cli-service test:unit"
}, },
@ -44,11 +44,11 @@
"@types/node": "^14.14.40", "@types/node": "^14.14.40",
"@types/quill": "^2.0.1", "@types/quill": "^2.0.1",
"@types/uuid": "^8.3.0", "@types/uuid": "^8.3.0",
"@typescript-eslint/eslint-plugin": "^4.18.0", "@typescript-eslint/eslint-plugin": "^4.29.0",
"@typescript-eslint/parser": "^4.18.0", "@typescript-eslint/parser": "^4.29.0",
"@vue/cli-plugin-babel": "~4.5.0", "@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~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-plugin-unit-jest": "~4.5.0",
"@vue/cli-service": "~4.5.0", "@vue/cli-service": "~4.5.0",
"@vue/eslint-config-standard": "^5.0.1", "@vue/eslint-config-standard": "^5.0.1",
@ -60,9 +60,9 @@
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"dateformat": "^3.0.3", "dateformat": "^3.0.3",
"element-ui": "~2.13.0", "element-ui": "~2.13.0",
"eslint": "^6.8.0", "eslint": "^7.32.0",
"eslint-plugin-import": "^2.19.1", "eslint-plugin-import": "^2.23.4",
"eslint-plugin-vue": "^6.2.2", "eslint-plugin-vue": "^7.16.0",
"file-saver": "^2.0.2", "file-saver": "^2.0.2",
"flatted": "^2.0.0", "flatted": "^2.0.0",
"jquery": "^3.4.1", "jquery": "^3.4.1",
@ -81,7 +81,7 @@
"string-template-parser": "^1.2.6", "string-template-parser": "^1.2.6",
"ts-jest": "^26.3.0", "ts-jest": "^26.3.0",
"tslint": "^6.1.2", "tslint": "^6.1.2",
"typescript": "~3.9.7", "typescript": "~4.3.5",
"uuid": "^8.3.0", "uuid": "^8.3.0",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue-cli-plugin-webpack-bundle-analyzer": "^2.0.0", "vue-cli-plugin-webpack-bundle-analyzer": "^2.0.0",

View file

@ -1,4 +1,5 @@
<template functional> <template functional>
<!-- eslint-disable-next-line vue/no-mutating-props -->
<a v-if="props.version" :set="version = props.version" :href="version.documentationUrl" target="_blank" :class="$style.card"> <a v-if="props.version" :set="version = props.version" :href="version.documentationUrl" target="_blank" :class="$style.card">
<div :class="$style.header"> <div :class="$style.header">
<div> <div>

View file

@ -1,12 +1,7 @@
import { import { UserSettings } from 'n8n-core';
UserSettings,
} from "n8n-core";
import { Command, flags } from '@oclif/command'; import { Command, flags } from '@oclif/command';
import { import { buildFiles, IBuildOptions } from '../src';
buildFiles,
IBuildOptions,
} from '../src';
export class Build extends Command { export class Build extends Command {
static description = 'Builds credentials and nodes and copies it to n8n custom extension folder'; static description = 'Builds credentials and nodes and copies it to n8n custom extension folder';
@ -24,11 +19,14 @@ export class Build extends Command {
description: `The path to copy the compiles files to [default: ${UserSettings.getUserN8nFolderCustomExtensionPath()}]`, description: `The path to copy the compiles files to [default: ${UserSettings.getUserN8nFolderCustomExtensionPath()}]`,
}), }),
watch: flags.boolean({ watch: flags.boolean({
description: 'Starts in watch mode and automatically builds and copies file whenever they change', description:
'Starts in watch mode and automatically builds and copies file whenever they change',
}), }),
}; };
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async run() { async run() {
// eslint-disable-next-line @typescript-eslint/no-shadow
const { flags } = this.parse(Build); const { flags } = this.parse(Build);
this.log('\nBuild credentials and nodes'); this.log('\nBuild credentials and nodes');
@ -47,13 +45,12 @@ export class Build extends Command {
const outputDirectory = await buildFiles(options); const outputDirectory = await buildFiles(options);
this.log(`The nodes got build and saved into the following folder:\n${outputDirectory}`); this.log(`The nodes got build and saved into the following folder:\n${outputDirectory}`);
} catch (error) { } catch (error) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
this.log(`\nGOT ERROR: "${error.message}"`); this.log(`\nGOT ERROR: "${error.message}"`);
this.log('===================================='); this.log('====================================');
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
this.log(error.stack); this.log(error.stack);
return;
} }
} }
} }

View file

@ -1,25 +1,26 @@
/* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import * as changeCase from 'change-case'; import * as changeCase from 'change-case';
import * as fs from 'fs'; import * as fs from 'fs';
import * as inquirer from 'inquirer'; import * as inquirer from 'inquirer';
import { Command } from '@oclif/command'; import { Command } from '@oclif/command';
import { join } from 'path'; import { join } from 'path';
const { promisify } = require('util'); import { createTemplate } from '../src';
const fsAccess = promisify(fs.access);
import { // eslint-disable-next-line @typescript-eslint/no-var-requires
createTemplate const { promisify } = require('util');
} from '../src';
const fsAccess = promisify(fs.access);
export class New extends Command { export class New extends Command {
static description = 'Create new credentials/node'; static description = 'Create new credentials/node';
static examples = [ static examples = [`$ n8n-node-dev new`];
`$ n8n-node-dev new`,
];
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async run() { async run() {
try { try {
this.log('\nCreate new credentials/node'); this.log('\nCreate new credentials/node');
this.log('========================='); this.log('=========================');
@ -30,10 +31,7 @@ export class New extends Command {
type: 'list', type: 'list',
default: 'Node', default: 'Node',
message: 'What do you want to create?', message: 'What do you want to create?',
choices: [ choices: ['Credentials', 'Node'],
'Credentials',
'Node',
],
}; };
const typeAnswers = await inquirer.prompt(typeQuestion); const typeAnswers = await inquirer.prompt(typeQuestion);
@ -43,7 +41,6 @@ export class New extends Command {
let defaultName = ''; let defaultName = '';
let getDescription = false; let getDescription = false;
if (typeAnswers.type === 'Node') { if (typeAnswers.type === 'Node') {
// Create new node // Create new node
@ -54,11 +51,7 @@ export class New extends Command {
type: 'list', type: 'list',
default: 'Execute', default: 'Execute',
message: 'What kind of node do you want to create?', message: 'What kind of node do you want to create?',
choices: [ choices: ['Execute', 'Trigger', 'Webhook'],
'Execute',
'Trigger',
'Webhook',
],
}; };
const nodeTypeAnswers = await inquirer.prompt(nodeTypeQuestion); const nodeTypeAnswers = await inquirer.prompt(nodeTypeQuestion);
@ -91,7 +84,7 @@ export class New extends Command {
}, },
]; ];
if (getDescription === true) { if (getDescription) {
// Get also a node description // Get also a node description
additionalQuestions.push({ additionalQuestions.push({
name: 'description', name: 'description',
@ -101,13 +94,19 @@ export class New extends Command {
}); });
} }
const additionalAnswers = await inquirer.prompt(additionalQuestions as inquirer.QuestionCollection); const additionalAnswers = await inquirer.prompt(
additionalQuestions as inquirer.QuestionCollection,
);
const nodeName = additionalAnswers.name; const nodeName = additionalAnswers.name;
// Define the source file to be used and the location and name of the new // Define the source file to be used and the location and name of the new
// node file // node file
const destinationFilePath = join(process.cwd(), `${changeCase.pascalCase(nodeName)}.${typeAnswers.type.toLowerCase()}.ts`); const destinationFilePath = join(
process.cwd(),
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
`${changeCase.pascalCase(nodeName)}.${typeAnswers.type.toLowerCase()}.ts`,
);
const sourceFilePath = join(__dirname, '../../templates', sourceFolder, sourceFileName); const sourceFilePath = join(__dirname, '../../templates', sourceFolder, sourceFileName);
@ -150,12 +149,13 @@ export class New extends Command {
this.log('\nExecution was successfull:'); this.log('\nExecution was successfull:');
this.log('===================================='); this.log('====================================');
this.log('Node got created: ' + destinationFilePath); this.log(`Node got created: ${destinationFilePath}`);
} catch (error) { } catch (error) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
this.log(`\nGOT ERROR: "${error.message}"`); this.log(`\nGOT ERROR: "${error.message}"`);
this.log('===================================='); this.log('====================================');
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
this.log(error.stack); this.log(error.stack);
return;
} }
} }
} }

View file

@ -21,10 +21,11 @@
"scripts": { "scripts": {
"dev": "npm run watch", "dev": "npm run watch",
"build": "tsc", "build": "tsc",
"format": "cd ../.. && node_modules/prettier/bin-prettier.js packages/node-dev/**/**.ts --write",
"lint": "cd ../.. && node_modules/eslint/bin/eslint.js packages/node-dev",
"lintfix": "cd ../.. && node_modules/eslint/bin/eslint.js packages/node-dev --fix",
"postpack": "rm -f oclif.manifest.json", "postpack": "rm -f oclif.manifest.json",
"prepack": "echo \"Building project...\" && rm -rf dist && tsc -b && oclif-dev manifest", "prepack": "echo \"Building project...\" && rm -rf dist && tsc -b && oclif-dev manifest",
"tslint": "tslint -p tsconfig.json -c tslint.json",
"tslintfix": "tslint --fix -p tsconfig.json -c tslint.json",
"watch": "tsc --watch" "watch": "tsc --watch"
}, },
"bin": { "bin": {
@ -64,7 +65,7 @@
"oauth-1.0a": "^2.2.6", "oauth-1.0a": "^2.2.6",
"replace-in-file": "^6.0.0", "replace-in-file": "^6.0.0",
"request": "^2.88.2", "request": "^2.88.2",
"tmp-promise": "^2.0.2", "tmp-promise": "^3.0.2",
"typescript": "~3.9.7" "typescript": "~4.3.5"
} }
} }

View file

@ -1,26 +1,23 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { ChildProcess, spawn } from 'child_process'; import { ChildProcess, spawn } from 'child_process';
const copyfiles = require('copyfiles');
import { import { readFile as fsReadFile } from 'fs/promises';
readFile as fsReadFile, import { write as fsWrite } from 'fs';
} from 'fs/promises';
import {
write as fsWrite,
} from 'fs';
import { join } from 'path'; import { join } from 'path';
import { file } from 'tmp-promise'; import { file } from 'tmp-promise';
import { promisify } from 'util'; import { promisify } from 'util';
const fsReadFileAsync = promisify(fsReadFile); import { UserSettings } from 'n8n-core';
const fsWriteAsync = promisify(fsWrite); // eslint-disable-next-line import/no-cycle
import { IBuildOptions } from '.'; import { IBuildOptions } from '.';
import { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires
UserSettings, const copyfiles = require('copyfiles');
} from 'n8n-core';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const fsReadFileAsync = promisify(fsReadFile);
const fsWriteAsync = promisify(fsWrite);
/** /**
* Create a custom tsconfig file as tsc currently has no way to define a base * Create a custom tsconfig file as tsc currently has no way to define a base
@ -30,23 +27,26 @@ import {
* @export * @export
* @returns * @returns
*/ */
export async function createCustomTsconfig () { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export async function createCustomTsconfig() {
// Get path to simple tsconfig file which should be used for build // Get path to simple tsconfig file which should be used for build
const tsconfigPath = join(__dirname, '../../src/tsconfig-build.json'); const tsconfigPath = join(__dirname, '../../src/tsconfig-build.json');
// Read the tsconfi file // Read the tsconfi file
const tsConfigString = await fsReadFile(tsconfigPath, { encoding: 'utf8'}) as string; const tsConfigString = await fsReadFile(tsconfigPath, { encoding: 'utf8' });
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const tsConfig = JSON.parse(tsConfigString); const tsConfig = JSON.parse(tsConfigString);
// Set absolute include paths // Set absolute include paths
const newIncludeFiles = []; const newIncludeFiles = [];
// eslint-disable-next-line no-restricted-syntax
for (const includeFile of tsConfig.include) { for (const includeFile of tsConfig.include) {
newIncludeFiles.push(join(process.cwd(), includeFile)); newIncludeFiles.push(join(process.cwd(), includeFile));
} }
tsConfig.include = newIncludeFiles; tsConfig.include = newIncludeFiles;
// Write new custom tsconfig file // Write new custom tsconfig file
// eslint-disable-next-line @typescript-eslint/unbound-method
const { fd, path, cleanup } = await file({ dir: process.cwd() }); const { fd, path, cleanup } = await file({ dir: process.cwd() });
await fsWriteAsync(fd, Buffer.from(JSON.stringify(tsConfig, null, 2), 'utf8')); await fsWriteAsync(fd, Buffer.from(JSON.stringify(tsConfig, null, 2), 'utf8'));
@ -56,7 +56,6 @@ export async function createCustomTsconfig () {
}; };
} }
/** /**
* Builds and copies credentials and nodes * Builds and copies credentials and nodes
* *
@ -64,7 +63,8 @@ export async function createCustomTsconfig () {
* @param {IBuildOptions} [options] Options to overwrite default behaviour * @param {IBuildOptions} [options] Options to overwrite default behaviour
* @returns {Promise<string>} * @returns {Promise<string>}
*/ */
export async function buildFiles (options?: IBuildOptions): Promise<string> { export async function buildFiles(options?: IBuildOptions): Promise<string> {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing, no-param-reassign
options = options || {}; options = options || {};
let typescriptPath; let typescriptPath;
@ -79,24 +79,31 @@ export async function buildFiles (options?: IBuildOptions): Promise<string> {
const tsconfigData = await createCustomTsconfig(); const tsconfigData = await createCustomTsconfig();
const outputDirectory = options.destinationFolder || UserSettings.getUserN8nFolderCustomExtensionPath(); const outputDirectory =
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
options.destinationFolder || UserSettings.getUserN8nFolderCustomExtensionPath();
// Supply a node base path so that it finds n8n-core and n8n-workflow // Supply a node base path so that it finds n8n-core and n8n-workflow
const nodeModulesPath = join(__dirname, '../../node_modules/'); const nodeModulesPath = join(__dirname, '../../node_modules/');
let buildCommand = `${tscPath} --p ${tsconfigData.path} --outDir ${outputDirectory} --rootDir ${process.cwd()} --baseUrl ${nodeModulesPath}`; let buildCommand = `${tscPath} --p ${
tsconfigData.path
} --outDir ${outputDirectory} --rootDir ${process.cwd()} --baseUrl ${nodeModulesPath}`;
if (options.watch === true) { if (options.watch === true) {
buildCommand += ' --watch'; buildCommand += ' --watch';
} }
let buildProcess: ChildProcess; let buildProcess: ChildProcess;
try { try {
buildProcess = spawn('node', buildCommand.split(' '), { windowsVerbatimArguments: true, cwd: process.cwd() }); buildProcess = spawn('node', buildCommand.split(' '), {
windowsVerbatimArguments: true,
cwd: process.cwd(),
});
// Forward the output of the child process to the main one // Forward the output of the child process to the main one
// that the user can see what is happening // that the user can see what is happening
//@ts-ignore // @ts-ignore
buildProcess.stdout.pipe(process.stdout); buildProcess.stdout.pipe(process.stdout);
//@ts-ignore // @ts-ignore
buildProcess.stderr.pipe(process.stderr); buildProcess.stderr.pipe(process.stderr);
// Make sure that the child process gets also always terminated // Make sure that the child process gets also always terminated
@ -105,27 +112,33 @@ export async function buildFiles (options?: IBuildOptions): Promise<string> {
buildProcess.kill(); buildProcess.kill();
}); });
} catch (error) { } catch (error) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
let errorMessage = error.message; let errorMessage = error.message;
if (error.stdout !== undefined) { if (error.stdout !== undefined) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
errorMessage = `${errorMessage}\nGot following output:\n${error.stdout}`; errorMessage = `${errorMessage}\nGot following output:\n${error.stdout}`;
} }
// Remove the tmp tsconfig file // Remove the tmp tsconfig file
// eslint-disable-next-line @typescript-eslint/no-floating-promises
tsconfigData.cleanup(); tsconfigData.cleanup();
throw new Error(errorMessage); throw new Error(errorMessage);
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
['*.png', '*.node.json'].forEach(filenamePattern => { ['*.png', '*.node.json'].forEach((filenamePattern) => {
copyfiles( // eslint-disable-next-line @typescript-eslint/no-unsafe-call
[join(process.cwd(), `./${filenamePattern}`), outputDirectory], copyfiles([join(process.cwd(), `./${filenamePattern}`), outputDirectory], { up: true }, () =>
{ up: true }, resolve(outputDirectory),
() => resolve(outputDirectory)); );
}); });
buildProcess.on('exit', code => { // eslint-disable-next-line @typescript-eslint/no-unused-vars
buildProcess.on('exit', (code) => {
// Remove the tmp tsconfig file // Remove the tmp tsconfig file
// eslint-disable-next-line @typescript-eslint/no-floating-promises
tsconfigData.cleanup(); tsconfigData.cleanup();
}); });
}); });

View file

@ -1,10 +1,11 @@
import * as fs from 'fs'; import * as fs from 'fs';
import {replaceInFile, ReplaceInFileConfig } from 'replace-in-file'; import { replaceInFile, ReplaceInFileConfig } from 'replace-in-file';
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires
const { promisify } = require('util'); const { promisify } = require('util');
const fsCopyFile = promisify(fs.copyFile);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
const fsCopyFile = promisify(fs.copyFile);
/** /**
* Creates a new credentials or node * Creates a new credentials or node
@ -15,16 +16,18 @@ const fsCopyFile = promisify(fs.copyFile);
* @param {object} replaceValues The values to replace in the template file * @param {object} replaceValues The values to replace in the template file
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
export async function createTemplate(sourceFilePath: string, destinationFilePath: string, replaceValues: object): Promise<void> { export async function createTemplate(
sourceFilePath: string,
destinationFilePath: string,
replaceValues: object,
): Promise<void> {
// Copy the file to then replace the values in it // Copy the file to then replace the values in it
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
await fsCopyFile(sourceFilePath, destinationFilePath); await fsCopyFile(sourceFilePath, destinationFilePath);
// Replace the variables in the template file // Replace the variables in the template file
const options: ReplaceInFileConfig = { const options: ReplaceInFileConfig = {
files: [ files: [destinationFilePath],
destinationFilePath,
],
from: [], from: [],
to: [], to: [],
}; };

View file

@ -1,3 +1,4 @@
// eslint-disable-next-line import/no-cycle
export * from './Build'; export * from './Build';
export * from './Create'; export * from './Create';
export * from './Interfaces'; export * from './Interfaces';

View file

@ -1,12 +1,10 @@
import { import { ICredentialType, NodePropertyTypes } from 'n8n-workflow';
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class ClassNameReplace implements ICredentialType { export class ClassNameReplace implements ICredentialType {
name = 'N8nNameReplace'; name = 'N8nNameReplace';
displayName = 'DisplayNameReplace'; displayName = 'DisplayNameReplace';
properties = [ properties = [
// The credentials to get from user and save encrypted. // The credentials to get from user and save encrypted.
// Properties can be defined exactly in the same way // Properties can be defined exactly in the same way

View file

@ -1,10 +1,5 @@
import { IExecuteFunctions } from 'n8n-core'; import { IExecuteFunctions } from 'n8n-core';
import { import { INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
export class ClassNameReplace implements INodeType { export class ClassNameReplace implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
@ -29,13 +24,11 @@ export class ClassNameReplace implements INodeType {
default: '', default: '',
placeholder: 'Placeholder value', placeholder: 'Placeholder value',
description: 'The description text', description: 'The description text',
} },
] ],
}; };
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData(); const items = this.getInputData();
let item: INodeExecutionData; let item: INodeExecutionData;
@ -48,10 +41,9 @@ export class ClassNameReplace implements INodeType {
myString = this.getNodeParameter('myString', itemIndex, '') as string; myString = this.getNodeParameter('myString', itemIndex, '') as string;
item = items[itemIndex]; item = items[itemIndex];
item.json['myString'] = myString; item.json.myString = myString;
} }
return this.prepareOutputData(items); return this.prepareOutputData(items);
} }
} }

View file

@ -1,10 +1,5 @@
import { ITriggerFunctions } from 'n8n-core'; import { ITriggerFunctions } from 'n8n-core';
import { import { INodeType, INodeTypeDescription, ITriggerResponse } from 'n8n-workflow';
INodeType,
INodeTypeDescription,
ITriggerResponse,
} from 'n8n-workflow';
export class ClassNameReplace implements INodeType { export class ClassNameReplace implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
@ -32,12 +27,10 @@ export class ClassNameReplace implements INodeType {
default: 1, default: 1,
description: 'Every how many minutes the workflow should be triggered.', description: 'Every how many minutes the workflow should be triggered.',
}, },
] ],
}; };
async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> { async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> {
const interval = this.getNodeParameter('interval', 1) as number; const interval = this.getNodeParameter('interval', 1) as number;
if (interval <= 0) { if (interval <= 0) {
@ -48,7 +41,7 @@ export class ClassNameReplace implements INodeType {
// Every time the emit function gets called a new workflow // Every time the emit function gets called a new workflow
// executions gets started with the provided entries. // executions gets started with the provided entries.
const entry = { const entry = {
'exampleKey': 'exampleData' exampleKey: 'exampleData',
}; };
this.emit([this.helpers.returnJsonArray([entry])]); this.emit([this.helpers.returnJsonArray([entry])]);
}; };
@ -78,6 +71,5 @@ export class ClassNameReplace implements INodeType {
closeFunction, closeFunction,
manualTriggerFunction, manualTriggerFunction,
}; };
} }
} }

View file

@ -1,14 +1,6 @@
import { import { IWebhookFunctions } from 'n8n-core';
IWebhookFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeTypeDescription,
INodeType,
IWebhookResponseData,
} from 'n8n-workflow';
import { IDataObject, INodeTypeDescription, INodeType, IWebhookResponseData } from 'n8n-workflow';
export class ClassNameReplace implements INodeType { export class ClassNameReplace implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
@ -47,25 +39,18 @@ export class ClassNameReplace implements INodeType {
], ],
}; };
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> { async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
// The data to return and so start the workflow with // The data to return and so start the workflow with
const returnData: IDataObject[] = []; const returnData: IDataObject[] = [];
returnData.push( returnData.push({
{
headers: this.getHeaderData(), headers: this.getHeaderData(),
params: this.getParamsData(), params: this.getParamsData(),
query: this.getQueryData(), query: this.getQueryData(),
body: this.getBodyData(), body: this.getBodyData(),
} });
);
return { return {
workflowData: [ workflowData: [this.helpers.returnJsonArray(returnData)],
this.helpers.returnJsonArray(returnData)
],
}; };
} }
} }

View file

@ -85,6 +85,7 @@ export class Discord implements INodeType {
// Waiting rating limit // Waiting rating limit
await new Promise((resolve) => { await new Promise((resolve) => {
setTimeout(async () => { setTimeout(async () => {
// @ts-ignore
resolve(); resolve();
}, get(error, 'response.body.retry_after', 150)); }, get(error, 'response.body.retry_after', 150));
}); });

View file

@ -552,6 +552,7 @@ export class Slack implements INodeType {
attachment.fields = attachment.fields.item; attachment.fields = attachment.fields.item;
} else { } else {
// If it does not have any items set remove it // If it does not have any items set remove it
// @ts-ignore
delete attachment.fields; delete attachment.fields;
} }
} }
@ -786,6 +787,7 @@ export class Slack implements INodeType {
attachment.fields = attachment.fields.item; attachment.fields = attachment.fields.item;
} else { } else {
// If it does not have any items set remove it // If it does not have any items set remove it
// @ts-ignore
delete attachment.fields; delete attachment.fields;
} }
} }

View file

@ -9,6 +9,7 @@ export function connect(conn: snowflake.Connection) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
conn.connect((err, conn) => { conn.connect((err, conn) => {
if (!err) { if (!err) {
// @ts-ignore
resolve(); resolve();
} else { } else {
reject(err); reject(err);
@ -21,6 +22,7 @@ export function destroy(conn: snowflake.Connection) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
conn.destroy((err, conn) => { conn.destroy((err, conn) => {
if (!err) { if (!err) {
// @ts-ignore
resolve(); resolve();
} else { } else {
reject(err); reject(err);

View file

@ -167,6 +167,7 @@ export async function uploadAttachments(this: IExecuteFunctions, binaryPropertie
const { check_after_secs } = (response.processing_info as IDataObject); const { check_after_secs } = (response.processing_info as IDataObject);
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
setTimeout(() => { setTimeout(() => {
// @ts-ignore
resolve(); resolve();
}, (check_after_secs as number) * 1000); }, (check_after_secs as number) * 1000);
}); });

View file

@ -17,8 +17,9 @@
"scripts": { "scripts": {
"dev": "npm run watch", "dev": "npm run watch",
"build": "tsc && gulp", "build": "tsc && gulp",
"tslint": "tslint -p tsconfig.json -c tslint.json", "format": "cd ../.. && node_modules/prettier/bin-prettier.js packages/nodes-base/**/**.ts --write",
"tslintfix": "tslint --fix -p tsconfig.json -c tslint.json", "lint": "tslint -p tsconfig.json -c tslint.json",
"lintfix": "tslint --fix -p tsconfig.json -c tslint.json",
"nodelinter": "nodelinter", "nodelinter": "nodelinter",
"watch": "tsc --watch", "watch": "tsc --watch",
"test": "jest" "test": "jest"
@ -644,7 +645,7 @@
"nodelinter": "^0.1.9", "nodelinter": "^0.1.9",
"ts-jest": "^26.3.0", "ts-jest": "^26.3.0",
"tslint": "^6.1.2", "tslint": "^6.1.2",
"typescript": "~3.9.7" "typescript": "~4.3.5"
}, },
"dependencies": { "dependencies": {
"@types/lossless-json": "^1.0.0", "@types/lossless-json": "^1.0.0",

View file

@ -17,8 +17,9 @@
"scripts": { "scripts": {
"dev": "npm run watch", "dev": "npm run watch",
"build": "tsc", "build": "tsc",
"tslint": "tslint -p tsconfig.json -c tslint.json", "format": "cd ../.. && node_modules/prettier/bin-prettier.js packages/workflow/**/**.ts --write",
"tslintfix": "tslint --fix -p tsconfig.json -c tslint.json", "lint": "cd ../.. && node_modules/eslint/bin/eslint.js packages/workflow",
"lintfix": "cd ../.. && node_modules/eslint/bin/eslint.js packages/workflow --fix",
"watch": "tsc --watch", "watch": "tsc --watch",
"test": "jest" "test": "jest"
}, },
@ -31,10 +32,18 @@
"@types/lodash.get": "^4.4.6", "@types/lodash.get": "^4.4.6",
"@types/node": "^14.14.40", "@types/node": "^14.14.40",
"@types/xml2js": "^0.4.3", "@types/xml2js": "^0.4.3",
"@typescript-eslint/eslint-plugin": "^4.29.0",
"@typescript-eslint/parser": "^4.29.0",
"eslint": "^7.32.0",
"eslint-config-airbnb-typescript": "^12.3.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-prettier": "^3.4.0",
"jest": "^26.4.2", "jest": "^26.4.2",
"prettier": "^2.3.2",
"ts-jest": "^26.3.0", "ts-jest": "^26.3.0",
"tslint": "^6.1.2", "tslint": "^6.1.2",
"typescript": "~3.9.7" "typescript": "~4.3.5"
}, },
"dependencies": { "dependencies": {
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",

View file

@ -1,4 +1,6 @@
// @ts-ignore
import * as tmpl from 'riot-tmpl';
// eslint-disable-next-line import/no-cycle
import { import {
INode, INode,
INodeExecutionData, INodeExecutionData,
@ -9,28 +11,26 @@ import {
Workflow, Workflow,
WorkflowDataProxy, WorkflowDataProxy,
WorkflowExecuteMode, WorkflowExecuteMode,
} from './'; } from '.';
// @ts-ignore // @ts-ignore
import * as tmpl from 'riot-tmpl';
// Set it to use double curly brackets instead of single ones // Set it to use double curly brackets instead of single ones
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
tmpl.brackets.set('{{ }}'); tmpl.brackets.set('{{ }}');
// Make sure that it does not always print an error when it could not resolve // Make sure that it does not always print an error when it could not resolve
// a variable // a variable
tmpl.tmpl.errorHandler = () => { }; // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
tmpl.tmpl.errorHandler = () => {};
export class Expression { export class Expression {
workflow: Workflow; workflow: Workflow;
constructor(workflow: Workflow) { constructor(workflow: Workflow) {
this.workflow = workflow; this.workflow = workflow;
} }
/** /**
* Converts an object to a string in a way to make it clear that * Converts an object to a string in a way to make it clear that
* the value comes from an object * the value comes from an object
@ -44,8 +44,6 @@ export class Expression {
return `[${typeName}: ${JSON.stringify(value)}]`; return `[${typeName}: ${JSON.stringify(value)}]`;
} }
/** /**
* Resolves the paramter value. If it is an expression it will execute it and * Resolves the paramter value. If it is an expression it will execute it and
* return the result. For everything simply the supplied value will be returned. * return the result. For everything simply the supplied value will be returned.
@ -60,7 +58,19 @@ export class Expression {
* @returns {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[])} * @returns {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[])}
* @memberof Workflow * @memberof Workflow
*/ */
resolveSimpleParameterValue(parameterValue: NodeParameterValue, siblingParameters: INodeParameters, runExecutionData: IRunExecutionData | null, runIndex: number, itemIndex: number, activeNodeName: string, connectionInputData: INodeExecutionData[], mode: WorkflowExecuteMode, additionalKeys: IWorkflowDataProxyAdditionalKeys, returnObjectAsString = false, selfData = {}): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] { resolveSimpleParameterValue(
parameterValue: NodeParameterValue,
siblingParameters: INodeParameters,
runExecutionData: IRunExecutionData | null,
runIndex: number,
itemIndex: number,
activeNodeName: string,
connectionInputData: INodeExecutionData[],
mode: WorkflowExecuteMode,
additionalKeys: IWorkflowDataProxyAdditionalKeys,
returnObjectAsString = false,
selfData = {},
): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] {
// Check if it is an expression // Check if it is an expression
if (typeof parameterValue !== 'string' || parameterValue.charAt(0) !== '=') { if (typeof parameterValue !== 'string' || parameterValue.charAt(0) !== '=') {
// Is no expression so return value // Is no expression so return value
@ -70,30 +80,44 @@ export class Expression {
// Is an expression // Is an expression
// Remove the equal sign // Remove the equal sign
// eslint-disable-next-line no-param-reassign
parameterValue = parameterValue.substr(1); parameterValue = parameterValue.substr(1);
// Generate a data proxy which allows to query workflow data // Generate a data proxy which allows to query workflow data
const dataProxy = new WorkflowDataProxy(this.workflow, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, siblingParameters, mode, additionalKeys, -1, selfData); const dataProxy = new WorkflowDataProxy(
this.workflow,
runExecutionData,
runIndex,
itemIndex,
activeNodeName,
connectionInputData,
siblingParameters,
mode,
additionalKeys,
-1,
selfData,
);
const data = dataProxy.getDataProxy(); const data = dataProxy.getDataProxy();
// Execute the expression // Execute the expression
try { try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
const returnValue = tmpl.tmpl(parameterValue, data); const returnValue = tmpl.tmpl(parameterValue, data);
if (typeof returnValue === 'function') { if (typeof returnValue === 'function') {
throw new Error('Expression resolved to a function. Please add "()"'); throw new Error('Expression resolved to a function. Please add "()"');
} else if (returnValue !== null && typeof returnValue === 'object') { } else if (returnValue !== null && typeof returnValue === 'object') {
if (returnObjectAsString === true) { if (returnObjectAsString) {
return this.convertObjectValueToString(returnValue); return this.convertObjectValueToString(returnValue);
} }
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return returnValue; return returnValue;
} catch (e) { } catch (e) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
throw new Error(`Expression is not valid: ${e.message}`); throw new Error(`Expression is not valid: ${e.message}`);
} }
} }
/** /**
* Resolves value of parameter. But does not work for workflow-data. * Resolves value of parameter. But does not work for workflow-data.
* *
@ -103,7 +127,13 @@ export class Expression {
* @returns {(string | undefined)} * @returns {(string | undefined)}
* @memberof Workflow * @memberof Workflow
*/ */
getSimpleParameterValue(node: INode, parameterValue: string | boolean | undefined, mode: WorkflowExecuteMode, additionalKeys: IWorkflowDataProxyAdditionalKeys, defaultValue?: boolean | number | string): boolean | number | string | undefined { getSimpleParameterValue(
node: INode,
parameterValue: string | boolean | undefined,
mode: WorkflowExecuteMode,
additionalKeys: IWorkflowDataProxyAdditionalKeys,
defaultValue?: boolean | number | string,
): boolean | number | string | undefined {
if (parameterValue === undefined) { if (parameterValue === undefined) {
// Value is not set so return the default // Value is not set so return the default
return defaultValue; return defaultValue;
@ -119,11 +149,18 @@ export class Expression {
}, },
}; };
return this.getParameterValue(parameterValue, runData, runIndex, itemIndex, node.name, connectionInputData, mode, additionalKeys) as boolean | number | string | undefined; return this.getParameterValue(
parameterValue,
runData,
runIndex,
itemIndex,
node.name,
connectionInputData,
mode,
additionalKeys,
) as boolean | number | string | undefined;
} }
/** /**
* Resolves value of complex parameter. But does not work for workflow-data. * Resolves value of complex parameter. But does not work for workflow-data.
* *
@ -133,7 +170,19 @@ export class Expression {
* @returns {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | undefined)} * @returns {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | undefined)}
* @memberof Workflow * @memberof Workflow
*/ */
getComplexParameterValue(node: INode, parameterValue: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[], mode: WorkflowExecuteMode, additionalKeys: IWorkflowDataProxyAdditionalKeys, defaultValue: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | undefined = undefined, selfData = {}): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | undefined { getComplexParameterValue(
node: INode,
parameterValue: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[],
mode: WorkflowExecuteMode,
additionalKeys: IWorkflowDataProxyAdditionalKeys,
defaultValue:
| NodeParameterValue
| INodeParameters
| NodeParameterValue[]
| INodeParameters[]
| undefined = undefined,
selfData = {},
): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | undefined {
if (parameterValue === undefined) { if (parameterValue === undefined) {
// Value is not set so return the default // Value is not set so return the default
return defaultValue; return defaultValue;
@ -150,14 +199,34 @@ export class Expression {
}; };
// Resolve the "outer" main values // Resolve the "outer" main values
const returnData = this.getParameterValue(parameterValue, runData, runIndex, itemIndex, node.name, connectionInputData, mode, additionalKeys, false, selfData); const returnData = this.getParameterValue(
parameterValue,
runData,
runIndex,
itemIndex,
node.name,
connectionInputData,
mode,
additionalKeys,
false,
selfData,
);
// Resolve the "inner" values // Resolve the "inner" values
return this.getParameterValue(returnData, runData, runIndex, itemIndex, node.name, connectionInputData, mode, additionalKeys, false, selfData); return this.getParameterValue(
returnData,
runData,
runIndex,
itemIndex,
node.name,
connectionInputData,
mode,
additionalKeys,
false,
selfData,
);
} }
/** /**
* Returns the resolved node parameter value. If it is an expression it will execute it and * Returns the resolved node parameter value. If it is an expression it will execute it and
* return the result. If the value to resolve is an array or object it will do the same * return the result. If the value to resolve is an array or object it will do the same
@ -173,24 +242,74 @@ export class Expression {
* @returns {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[])} * @returns {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[])}
* @memberof Workflow * @memberof Workflow
*/ */
getParameterValue(parameterValue: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[], runExecutionData: IRunExecutionData | null, runIndex: number, itemIndex: number, activeNodeName: string, connectionInputData: INodeExecutionData[], mode: WorkflowExecuteMode, additionalKeys: IWorkflowDataProxyAdditionalKeys, returnObjectAsString = false, selfData = {}): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] { getParameterValue(
parameterValue: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[],
runExecutionData: IRunExecutionData | null,
runIndex: number,
itemIndex: number,
activeNodeName: string,
connectionInputData: INodeExecutionData[],
mode: WorkflowExecuteMode,
additionalKeys: IWorkflowDataProxyAdditionalKeys,
returnObjectAsString = false,
selfData = {},
): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] {
// Helper function which returns true when the parameter is a complex one or array // Helper function which returns true when the parameter is a complex one or array
const isComplexParameter = (value: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[]) => { const isComplexParameter = (
value: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[],
) => {
return typeof value === 'object'; return typeof value === 'object';
}; };
// Helper function which resolves a parameter value depending on if it is simply or not // Helper function which resolves a parameter value depending on if it is simply or not
const resolveParameterValue = (value: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[], siblingParameters: INodeParameters) => { const resolveParameterValue = (
value: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[],
siblingParameters: INodeParameters,
) => {
if (isComplexParameter(value)) { if (isComplexParameter(value)) {
return this.getParameterValue(value, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, mode, additionalKeys, returnObjectAsString, selfData); return this.getParameterValue(
} else { value,
return this.resolveSimpleParameterValue(value as NodeParameterValue, siblingParameters, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, mode, additionalKeys, returnObjectAsString, selfData); runExecutionData,
runIndex,
itemIndex,
activeNodeName,
connectionInputData,
mode,
additionalKeys,
returnObjectAsString,
selfData,
);
} }
return this.resolveSimpleParameterValue(
value as NodeParameterValue,
siblingParameters,
runExecutionData,
runIndex,
itemIndex,
activeNodeName,
connectionInputData,
mode,
additionalKeys,
returnObjectAsString,
selfData,
);
}; };
// Check if it value is a simple one that we can get it resolved directly // Check if it value is a simple one that we can get it resolved directly
if (!isComplexParameter(parameterValue)) { if (!isComplexParameter(parameterValue)) {
return this.resolveSimpleParameterValue(parameterValue as NodeParameterValue, {}, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, mode, additionalKeys, returnObjectAsString, selfData); return this.resolveSimpleParameterValue(
parameterValue as NodeParameterValue,
{},
runExecutionData,
runIndex,
itemIndex,
activeNodeName,
connectionInputData,
mode,
additionalKeys,
returnObjectAsString,
selfData,
);
} }
// The parameter value is complex so resolve depending on type // The parameter value is complex so resolve depending on type
@ -198,28 +317,33 @@ export class Expression {
if (Array.isArray(parameterValue)) { if (Array.isArray(parameterValue)) {
// Data is an array // Data is an array
const returnData = []; const returnData = [];
// eslint-disable-next-line no-restricted-syntax
for (const item of parameterValue) { for (const item of parameterValue) {
returnData.push(resolveParameterValue(item, {})); returnData.push(resolveParameterValue(item, {}));
} }
if (returnObjectAsString === true && typeof returnData === 'object') { if (returnObjectAsString && typeof returnData === 'object') {
return this.convertObjectValueToString(returnData); return this.convertObjectValueToString(returnData);
} }
return returnData as NodeParameterValue[] | INodeParameters[]; return returnData as NodeParameterValue[] | INodeParameters[];
} else if (parameterValue === null || parameterValue === undefined) { }
if (parameterValue === null || parameterValue === undefined) {
return parameterValue; return parameterValue;
} else { }
// Data is an object // Data is an object
const returnData: INodeParameters = {}; const returnData: INodeParameters = {};
// eslint-disable-next-line no-restricted-syntax
for (const key of Object.keys(parameterValue)) { for (const key of Object.keys(parameterValue)) {
returnData[key] = resolveParameterValue((parameterValue as INodeParameters)[key], parameterValue as INodeParameters); returnData[key] = resolveParameterValue(
(parameterValue as INodeParameters)[key],
parameterValue as INodeParameters,
);
} }
if (returnObjectAsString === true && typeof returnData === 'object') { if (returnObjectAsString && typeof returnData === 'object') {
return this.convertObjectValueToString(returnData); return this.convertObjectValueToString(returnData);
} }
return returnData; return returnData;
} }
}
} }

View file

@ -1,10 +1,22 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable import/no-extraneous-dependencies */
/* eslint-disable import/no-cycle */
// eslint-disable-next-line import/no-extraneous-dependencies
// eslint-disable-next-line max-classes-per-file
import * as express from 'express';
import { Workflow } from './Workflow'; import { Workflow } from './Workflow';
import { WorkflowHooks } from './WorkflowHooks'; import { WorkflowHooks } from './WorkflowHooks';
import { WorkflowOperationError } from './WorkflowErrors'; import { WorkflowOperationError } from './WorkflowErrors';
import { NodeApiError, NodeOperationError } from './NodeErrors'; import { NodeApiError, NodeOperationError } from './NodeErrors';
import * as express from 'express';
export type IAllExecuteFunctions = IExecuteFunctions | IExecuteSingleFunctions | IHookFunctions | ILoadOptionsFunctions | IPollFunctions | ITriggerFunctions | IWebhookFunctions; export type IAllExecuteFunctions =
| IExecuteFunctions
| IExecuteSingleFunctions
| IHookFunctions
| ILoadOptionsFunctions
| IPollFunctions
| ITriggerFunctions
| IWebhookFunctions;
export interface IBinaryData { export interface IBinaryData {
[key: string]: string | undefined; [key: string]: string | undefined;
@ -43,8 +55,11 @@ export interface IGetCredentials {
export abstract class ICredentials { export abstract class ICredentials {
name: string; name: string;
type: string; type: string;
data: string | undefined; data: string | undefined;
nodesAccess: ICredentialNodeAccess[]; nodesAccess: ICredentialNodeAccess[];
constructor(name: string, type: string, nodesAccess: ICredentialNodeAccess[], data?: string) { constructor(name: string, type: string, nodesAccess: ICredentialNodeAccess[], data?: string) {
@ -55,10 +70,15 @@ export abstract class ICredentials {
} }
abstract getData(encryptionKey: string, nodeType?: string): ICredentialDataDecryptedObject; abstract getData(encryptionKey: string, nodeType?: string): ICredentialDataDecryptedObject;
abstract getDataKey(key: string, encryptionKey: string, nodeType?: string): CredentialInformation; abstract getDataKey(key: string, encryptionKey: string, nodeType?: string): CredentialInformation;
abstract getDataToSave(): ICredentialsEncrypted; abstract getDataToSave(): ICredentialsEncrypted;
abstract hasNodeAccess(nodeType: string): boolean; abstract hasNodeAccess(nodeType: string): boolean;
abstract setData(data: ICredentialDataDecryptedObject, encryptionKey: string): void; abstract setData(data: ICredentialDataDecryptedObject, encryptionKey: string): void;
abstract setDataKey(key: string, data: CredentialInformation, encryptionKey: string): void; abstract setDataKey(key: string, data: CredentialInformation, encryptionKey: string): void;
} }
@ -101,8 +121,20 @@ export abstract class ICredentialsHelper {
} }
abstract getCredentials(name: string, type: string): Promise<ICredentials>; abstract getCredentials(name: string, type: string): Promise<ICredentials>;
abstract getDecrypted(name: string, type: string, mode: WorkflowExecuteMode, raw?: boolean, expressionResolveValues?: ICredentialsExpressionResolveValues): Promise<ICredentialDataDecryptedObject>;
abstract updateCredentials(name: string, type: string, data: ICredentialDataDecryptedObject): Promise<void>; abstract getDecrypted(
name: string,
type: string,
mode: WorkflowExecuteMode,
raw?: boolean,
expressionResolveValues?: ICredentialsExpressionResolveValues,
): Promise<ICredentialDataDecryptedObject>;
abstract updateCredentials(
name: string,
type: string,
data: ICredentialDataDecryptedObject,
): Promise<void>;
} }
export interface ICredentialType { export interface ICredentialType {
@ -116,7 +148,7 @@ export interface ICredentialType {
export interface ICredentialTypes { export interface ICredentialTypes {
credentialTypes?: { credentialTypes?: {
[key: string]: ICredentialType [key: string]: ICredentialType;
}; };
init(credentialTypes?: { [key: string]: ICredentialType }): Promise<void>; init(credentialTypes?: { [key: string]: ICredentialType }): Promise<void>;
getAll(): ICredentialType[]; getAll(): ICredentialType[];
@ -133,7 +165,6 @@ export interface ICredentialData {
// The encrypted credentials which the nodes can access // The encrypted credentials which the nodes can access
export type CredentialInformation = string | number | boolean | IDataObject; export type CredentialInformation = string | number | boolean | IDataObject;
// The encrypted credentials which the nodes can access // The encrypted credentials which the nodes can access
export interface ICredentialDataDecryptedObject { export interface ICredentialDataDecryptedObject {
[key: string]: CredentialInformation; [key: string]: CredentialInformation;
@ -159,92 +190,150 @@ export interface IDataObject {
[key: string]: GenericValue | IDataObject | GenericValue[] | IDataObject[]; [key: string]: GenericValue | IDataObject | GenericValue[] | IDataObject[];
} }
export interface IGetExecutePollFunctions { export interface IGetExecutePollFunctions {
(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): IPollFunctions; (
workflow: Workflow,
node: INode,
additionalData: IWorkflowExecuteAdditionalData,
mode: WorkflowExecuteMode,
activation: WorkflowActivateMode,
): IPollFunctions;
} }
export interface IGetExecuteTriggerFunctions { export interface IGetExecuteTriggerFunctions {
(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): ITriggerFunctions; (
workflow: Workflow,
node: INode,
additionalData: IWorkflowExecuteAdditionalData,
mode: WorkflowExecuteMode,
activation: WorkflowActivateMode,
): ITriggerFunctions;
} }
export interface IGetExecuteFunctions { export interface IGetExecuteFunctions {
(workflow: Workflow, runExecutionData: IRunExecutionData, runIndex: number, connectionInputData: INodeExecutionData[], inputData: ITaskDataConnections, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): IExecuteFunctions; (
workflow: Workflow,
runExecutionData: IRunExecutionData,
runIndex: number,
connectionInputData: INodeExecutionData[],
inputData: ITaskDataConnections,
node: INode,
additionalData: IWorkflowExecuteAdditionalData,
mode: WorkflowExecuteMode,
): IExecuteFunctions;
} }
export interface IGetExecuteSingleFunctions { export interface IGetExecuteSingleFunctions {
(workflow: Workflow, runExecutionData: IRunExecutionData, runIndex: number, connectionInputData: INodeExecutionData[], inputData: ITaskDataConnections, node: INode, itemIndex: number, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): IExecuteSingleFunctions; (
workflow: Workflow,
runExecutionData: IRunExecutionData,
runIndex: number,
connectionInputData: INodeExecutionData[],
inputData: ITaskDataConnections,
node: INode,
itemIndex: number,
additionalData: IWorkflowExecuteAdditionalData,
mode: WorkflowExecuteMode,
): IExecuteSingleFunctions;
} }
export interface IGetExecuteHookFunctions { export interface IGetExecuteHookFunctions {
(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode, isTest?: boolean, webhookData?: IWebhookData): IHookFunctions; (
workflow: Workflow,
node: INode,
additionalData: IWorkflowExecuteAdditionalData,
mode: WorkflowExecuteMode,
activation: WorkflowActivateMode,
isTest?: boolean,
webhookData?: IWebhookData,
): IHookFunctions;
} }
export interface IGetExecuteWebhookFunctions { export interface IGetExecuteWebhookFunctions {
(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, webhookData: IWebhookData): IWebhookFunctions; (
workflow: Workflow,
node: INode,
additionalData: IWorkflowExecuteAdditionalData,
mode: WorkflowExecuteMode,
webhookData: IWebhookData,
): IWebhookFunctions;
} }
export interface IExecuteData { export interface IExecuteData {
data: ITaskDataConnections; data: ITaskDataConnections;
node: INode; node: INode;
} }
export type IContextObject = { export type IContextObject = {
[key: string]: any; // tslint:disable-line:no-any [key: string]: any;
}; };
export interface IExecuteContextData { export interface IExecuteContextData {
// Keys are: "flow" | "node:<NODE_NAME>" // Keys are: "flow" | "node:<NODE_NAME>"
[key: string]: IContextObject; [key: string]: IContextObject;
} }
export interface IExecuteFunctions { export interface IExecuteFunctions {
continueOnFail(): boolean; continueOnFail(): boolean;
evaluateExpression(expression: string, itemIndex: number): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[]; evaluateExpression(
executeWorkflow(workflowInfo: IExecuteWorkflowInfo, inputData?: INodeExecutionData[]): Promise<any>; // tslint:disable-line:no-any expression: string,
itemIndex: number,
): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[];
executeWorkflow(
workflowInfo: IExecuteWorkflowInfo,
inputData?: INodeExecutionData[],
): Promise<any>;
getContext(type: string): IContextObject; getContext(type: string): IContextObject;
getCredentials(type: string, itemIndex?: number): Promise<ICredentialDataDecryptedObject | undefined>; getCredentials(
type: string,
itemIndex?: number,
): Promise<ICredentialDataDecryptedObject | undefined>;
getInputData(inputIndex?: number, inputName?: string): INodeExecutionData[]; getInputData(inputIndex?: number, inputName?: string): INodeExecutionData[];
getMode(): WorkflowExecuteMode; getMode(): WorkflowExecuteMode;
getNode(): INode; getNode(): INode;
getNodeParameter(parameterName: string, itemIndex: number, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object; //tslint:disable-line:no-any getNodeParameter(
parameterName: string,
itemIndex: number,
fallbackValue?: any,
): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object;
getWorkflowDataProxy(itemIndex: number): IWorkflowDataProxyData; getWorkflowDataProxy(itemIndex: number): IWorkflowDataProxyData;
getWorkflowStaticData(type: string): IDataObject; getWorkflowStaticData(type: string): IDataObject;
getRestApiUrl(): string; getRestApiUrl(): string;
getTimezone(): string; getTimezone(): string;
getWorkflow(): IWorkflowMetadata; getWorkflow(): IWorkflowMetadata;
prepareOutputData(outputData: INodeExecutionData[], outputIndex?: number): Promise<INodeExecutionData[][]>; prepareOutputData(
outputData: INodeExecutionData[],
outputIndex?: number,
): Promise<INodeExecutionData[][]>;
putExecutionToWait(waitTill: Date): Promise<void>; putExecutionToWait(waitTill: Date): Promise<void>;
sendMessageToUI(message: any): void; // tslint:disable-line:no-any sendMessageToUI(message: any): void; // tslint:disable-line:no-any
helpers: { helpers: {
[key: string]: (...args: any[]) => any //tslint:disable-line:no-any [key: string]: (...args: any[]) => any;
}; };
} }
export interface IExecuteSingleFunctions { export interface IExecuteSingleFunctions {
continueOnFail(): boolean; continueOnFail(): boolean;
evaluateExpression(expression: string, itemIndex: number | undefined): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[]; evaluateExpression(
expression: string,
itemIndex: number | undefined,
): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[];
getContext(type: string): IContextObject; getContext(type: string): IContextObject;
getCredentials(type: string): Promise<ICredentialDataDecryptedObject | undefined>; getCredentials(type: string): Promise<ICredentialDataDecryptedObject | undefined>;
getInputData(inputIndex?: number, inputName?: string): INodeExecutionData; getInputData(inputIndex?: number, inputName?: string): INodeExecutionData;
getMode(): WorkflowExecuteMode; getMode(): WorkflowExecuteMode;
getNode(): INode; getNode(): INode;
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;
getRestApiUrl(): string; getRestApiUrl(): string;
getTimezone(): string; getTimezone(): string;
getWorkflow(): IWorkflowMetadata; getWorkflow(): IWorkflowMetadata;
getWorkflowDataProxy(): IWorkflowDataProxyData; getWorkflowDataProxy(): IWorkflowDataProxyData;
getWorkflowStaticData(type: string): IDataObject; getWorkflowStaticData(type: string): IDataObject;
helpers: { helpers: {
[key: string]: (...args: any[]) => any //tslint:disable-line:no-any [key: string]: (...args: any[]) => any;
}; };
} }
@ -256,13 +345,24 @@ export interface IExecuteWorkflowInfo {
export interface ILoadOptionsFunctions { export interface ILoadOptionsFunctions {
getCredentials(type: string): Promise<ICredentialDataDecryptedObject | undefined>; getCredentials(type: string): Promise<ICredentialDataDecryptedObject | undefined>;
getNode(): INode; getNode(): INode;
getNodeParameter(parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object; //tslint:disable-line:no-any getNodeParameter(
getCurrentNodeParameter(parameterName: string): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object | undefined; parameterName: string,
fallbackValue?: any,
): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object;
getCurrentNodeParameter(
parameterName: string,
):
| NodeParameterValue
| INodeParameters
| NodeParameterValue[]
| INodeParameters[]
| object
| undefined;
getCurrentNodeParameters(): INodeParameters | undefined; getCurrentNodeParameters(): INodeParameters | undefined;
getTimezone(): string; getTimezone(): string;
getRestApiUrl(): string; getRestApiUrl(): string;
helpers: { helpers: {
[key: string]: ((...args: any[]) => any) | undefined; //tslint:disable-line:no-any [key: string]: ((...args: any[]) => any) | undefined;
}; };
} }
@ -272,14 +372,17 @@ export interface IHookFunctions {
getActivationMode(): WorkflowActivateMode; getActivationMode(): WorkflowActivateMode;
getNode(): INode; getNode(): INode;
getNodeWebhookUrl: (name: string) => string | undefined; getNodeWebhookUrl: (name: string) => string | undefined;
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;
getTimezone(): string; getTimezone(): string;
getWebhookDescription(name: string): IWebhookDescription | undefined; getWebhookDescription(name: string): IWebhookDescription | undefined;
getWebhookName(): string; getWebhookName(): string;
getWorkflow(): IWorkflowMetadata; getWorkflow(): IWorkflowMetadata;
getWorkflowStaticData(type: string): IDataObject; getWorkflowStaticData(type: string): IDataObject;
helpers: { helpers: {
[key: string]: (...args: any[]) => any //tslint:disable-line:no-any [key: string]: (...args: any[]) => any;
}; };
} }
@ -289,13 +392,16 @@ export interface IPollFunctions {
getMode(): WorkflowExecuteMode; getMode(): WorkflowExecuteMode;
getActivationMode(): WorkflowActivateMode; getActivationMode(): WorkflowActivateMode;
getNode(): INode; getNode(): INode;
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;
getRestApiUrl(): string; getRestApiUrl(): string;
getTimezone(): string; getTimezone(): string;
getWorkflow(): IWorkflowMetadata; getWorkflow(): IWorkflowMetadata;
getWorkflowStaticData(type: string): IDataObject; getWorkflowStaticData(type: string): IDataObject;
helpers: { helpers: {
[key: string]: (...args: any[]) => any //tslint:disable-line:no-any [key: string]: (...args: any[]) => any;
}; };
} }
@ -305,13 +411,16 @@ export interface ITriggerFunctions {
getMode(): WorkflowExecuteMode; getMode(): WorkflowExecuteMode;
getActivationMode(): WorkflowActivateMode; getActivationMode(): WorkflowActivateMode;
getNode(): INode; getNode(): INode;
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;
getRestApiUrl(): string; getRestApiUrl(): string;
getTimezone(): string; getTimezone(): string;
getWorkflow(): IWorkflowMetadata; getWorkflow(): IWorkflowMetadata;
getWorkflowStaticData(type: string): IDataObject; getWorkflowStaticData(type: string): IDataObject;
helpers: { helpers: {
[key: string]: (...args: any[]) => any //tslint:disable-line:no-any [key: string]: (...args: any[]) => any;
}; };
} }
@ -321,7 +430,10 @@ export interface IWebhookFunctions {
getHeaderData(): object; getHeaderData(): object;
getMode(): WorkflowExecuteMode; getMode(): WorkflowExecuteMode;
getNode(): INode; getNode(): INode;
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;
getNodeWebhookUrl: (name: string) => string | undefined; getNodeWebhookUrl: (name: string) => string | undefined;
getParamsData(): object; getParamsData(): object;
getQueryData(): object; getQueryData(): object;
@ -331,9 +443,12 @@ export interface IWebhookFunctions {
getWebhookName(): string; getWebhookName(): string;
getWorkflowStaticData(type: string): IDataObject; getWorkflowStaticData(type: string): IDataObject;
getWorkflow(): IWorkflowMetadata; getWorkflow(): IWorkflowMetadata;
prepareOutputData(outputData: INodeExecutionData[], outputIndex?: number): Promise<INodeExecutionData[][]>; prepareOutputData(
outputData: INodeExecutionData[],
outputIndex?: number,
): Promise<INodeExecutionData[][]>;
helpers: { helpers: {
[key: string]: (...args: any[]) => any //tslint:disable-line:no-any [key: string]: (...args: any[]) => any;
}; };
} }
@ -360,18 +475,15 @@ export interface INode {
webhookId?: string; webhookId?: string;
} }
export interface INodes { export interface INodes {
[key: string]: INode; [key: string]: INode;
} }
export interface IObservableObject { export interface IObservableObject {
[key: string]: any; // tslint:disable-line:no-any [key: string]: any;
__dataChanged: boolean; __dataChanged: boolean;
} }
export interface IBinaryKeyData { export interface IBinaryKeyData {
[key: string]: IBinaryData; [key: string]: IBinaryData;
} }
@ -385,7 +497,6 @@ export interface INodeExecutionData {
binary?: IBinaryKeyData; binary?: IBinaryKeyData;
} }
export interface INodeExecuteFunctions { export interface INodeExecuteFunctions {
getExecutePollFunctions: IGetExecutePollFunctions; getExecutePollFunctions: IGetExecutePollFunctions;
getExecuteTriggerFunctions: IGetExecuteTriggerFunctions; getExecuteTriggerFunctions: IGetExecuteTriggerFunctions;
@ -395,7 +506,6 @@ export interface INodeExecuteFunctions {
getExecuteWebhookFunctions: IGetExecuteWebhookFunctions; getExecuteWebhookFunctions: IGetExecuteWebhookFunctions;
} }
// The values a node property can have // The values a node property can have
export type NodeParameterValue = string | number | boolean | undefined | null; export type NodeParameterValue = string | number | boolean | undefined | null;
@ -404,7 +514,19 @@ export interface INodeParameters {
[key: string]: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[]; [key: string]: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[];
} }
export type NodePropertyTypes = 'boolean' | 'collection' | 'color' | 'dateTime' | 'fixedCollection' | 'hidden' | 'json' | 'notice' | 'multiOptions' | 'number' | 'options' | 'string'; export type NodePropertyTypes =
| 'boolean'
| 'collection'
| 'color'
| 'dateTime'
| 'fixedCollection'
| 'hidden'
| 'json'
| 'notice'
| 'multiOptions'
| 'number'
| 'options'
| 'string';
export type EditorTypes = 'code'; export type EditorTypes = 'code';
@ -435,7 +557,6 @@ export interface IDisplayOptions {
}; };
} }
export interface INodeProperties { export interface INodeProperties {
displayName: string; displayName: string;
name: string; name: string;
@ -492,7 +613,7 @@ export interface INodeType {
methods?: { methods?: {
loadOptions?: { loadOptions?: {
[key: string]: (this: ILoadOptionsFunctions) => Promise<INodePropertyOptions[]>; [key: string]: (this: ILoadOptionsFunctions) => Promise<INodePropertyOptions[]>;
} };
}; };
webhookMethods?: { webhookMethods?: {
[key: string]: IWebhookSetupMethods; [key: string]: IWebhookSetupMethods;
@ -501,7 +622,6 @@ export interface INodeType {
export type WebhookSetupMethodNames = 'checkExists' | 'create' | 'delete'; export type WebhookSetupMethodNames = 'checkExists' | 'create' | 'delete';
export interface IWebhookSetupMethods { export interface IWebhookSetupMethods {
[key: string]: ((this: IHookFunctions) => Promise<boolean>) | undefined; [key: string]: ((this: IHookFunctions) => Promise<boolean>) | undefined;
checkExists?: (this: IHookFunctions) => Promise<boolean>; checkExists?: (this: IHookFunctions) => Promise<boolean>;
@ -509,7 +629,6 @@ export interface IWebhookSetupMethods {
delete?: (this: IHookFunctions) => Promise<boolean>; delete?: (this: IHookFunctions) => Promise<boolean>;
} }
export interface INodeCredentialDescription { export interface INodeCredentialDescription {
name: string; name: string;
required?: boolean; required?: boolean;
@ -596,17 +715,17 @@ export interface IWebhookDescription {
} }
export interface IWorkflowDataProxyData { export interface IWorkflowDataProxyData {
$binary: any; // tslint:disable-line:no-any $binary: any;
$data: any; // tslint:disable-line:no-any $data: any;
$env: any; // tslint:disable-line:no-any $env: any;
$evaluateExpression: any; // tslint:disable-line:no-any $evaluateExpression: any;
$item: any; // tslint:disable-line:no-any $item: any;
$items: any; // tslint:disable-line:no-any $items: any;
$json: any; // tslint:disable-line:no-any $json: any;
$node: any; // tslint:disable-line:no-any $node: any;
$parameter: any; // tslint:disable-line:no-any $parameter: any;
$position: any; // tslint:disable-line:no-any $position: any;
$workflow: any; // tslint:disable-line:no-any $workflow: any;
} }
export interface IWorkflowDataProxyAdditionalKeys { export interface IWorkflowDataProxyAdditionalKeys {
@ -623,7 +742,7 @@ export type WebhookHttpMethod = 'GET' | 'POST' | 'HEAD' | 'OPTIONS';
export interface IWebhookResponseData { export interface IWebhookResponseData {
workflowData?: INodeExecutionData[][]; workflowData?: INodeExecutionData[][];
webhookResponse?: any; // tslint:disable-line:no-any webhookResponse?: any;
noWebhookResponse?: boolean; noWebhookResponse?: boolean;
} }
@ -637,7 +756,6 @@ export interface INodeTypes {
getByName(nodeType: string): INodeType | undefined; getByName(nodeType: string): INodeType | undefined;
} }
export interface INodeTypeData { export interface INodeTypeData {
[key: string]: { [key: string]: {
type: INodeType; type: INodeType;
@ -654,7 +772,6 @@ export interface IRun {
stoppedAt?: Date; stoppedAt?: Date;
} }
// Contains all the data which is needed to execute a workflow and so also to // Contains all the data which is needed to execute a workflow and so also to
// start restart it again after it did fail. // start restart it again after it did fail.
// The RunData, ExecuteData and WaitForExecution contain often the same data. // The RunData, ExecuteData and WaitForExecution contain often the same data.
@ -676,13 +793,11 @@ export interface IRunExecutionData {
waitTill?: Date; waitTill?: Date;
} }
export interface IRunData { export interface IRunData {
// node-name: result-data // node-name: result-data
[key: string]: ITaskData[]; [key: string]: ITaskData[];
} }
// The data that gets returned when a node runs // The data that gets returned when a node runs
export interface ITaskData { export interface ITaskData {
startTime: number; startTime: number;
@ -691,7 +806,6 @@ export interface ITaskData {
error?: ExecutionError; error?: ExecutionError;
} }
// The data for al the different kind of connectons (like main) and all the indexes // The data for al the different kind of connectons (like main) and all the indexes
export interface ITaskDataConnections { export interface ITaskDataConnections {
// Key for each input type and because there can be multiple inputs of the same type it is an array // Key for each input type and because there can be multiple inputs of the same type it is an array
@ -700,20 +814,17 @@ export interface ITaskDataConnections {
[key: string]: Array<INodeExecutionData[] | null>; [key: string]: Array<INodeExecutionData[] | null>;
} }
// Keeps data while workflow gets executed and allows when provided to restart execution // Keeps data while workflow gets executed and allows when provided to restart execution
export interface IWaitingForExecution { export interface IWaitingForExecution {
// Node name // Node name
[key: string]: { [key: string]: {
// Run index // Run index
[key: number]: ITaskDataConnections [key: number]: ITaskDataConnections;
}; };
} }
export interface IWorkflowBase { export interface IWorkflowBase {
id?: number | string | any; // tslint:disable-line:no-any id?: number | string | any;
name: string; name: string;
active: boolean; active: boolean;
createdAt: Date; createdAt: Date;
@ -732,26 +843,34 @@ export interface IWorkflowCredentials {
}; };
} }
export interface IWorkflowExecuteHooks { export interface IWorkflowExecuteHooks {
[key: string]: Array<((...args: any[]) => Promise<void>)> | undefined; // tslint:disable-line:no-any [key: string]: Array<(...args: any[]) => Promise<void>> | undefined;
nodeExecuteAfter?: Array<((nodeName: string, data: ITaskData, executionData: IRunExecutionData) => Promise<void>)>; nodeExecuteAfter?: Array<
nodeExecuteBefore?: Array<((nodeName: string) => Promise<void>)>; (nodeName: string, data: ITaskData, executionData: IRunExecutionData) => Promise<void>
workflowExecuteAfter?: Array<((data: IRun, newStaticData: IDataObject) => Promise<void>)>; >;
workflowExecuteBefore?: Array<((workflow: Workflow, data: IRunExecutionData) => Promise<void>)>; nodeExecuteBefore?: Array<(nodeName: string) => Promise<void>>;
workflowExecuteAfter?: Array<(data: IRun, newStaticData: IDataObject) => Promise<void>>;
workflowExecuteBefore?: Array<(workflow: Workflow, data: IRunExecutionData) => Promise<void>>;
} }
export interface IWorkflowExecuteAdditionalData { export interface IWorkflowExecuteAdditionalData {
credentialsHelper: ICredentialsHelper; credentialsHelper: ICredentialsHelper;
encryptionKey: string; encryptionKey: string;
executeWorkflow: (workflowInfo: IExecuteWorkflowInfo, additionalData: IWorkflowExecuteAdditionalData, inputData?: INodeExecutionData[], parentExecutionId?: string, loadedWorkflowData?: IWorkflowBase, loadedRunData?: any) => Promise<any>; // tslint:disable-line:no-any executeWorkflow: (
workflowInfo: IExecuteWorkflowInfo,
additionalData: IWorkflowExecuteAdditionalData,
inputData?: INodeExecutionData[],
parentExecutionId?: string,
loadedWorkflowData?: IWorkflowBase,
loadedRunData?: any,
) => Promise<any>;
// hooks?: IWorkflowExecuteHooks; // hooks?: IWorkflowExecuteHooks;
executionId?: string; executionId?: string;
hooks?: WorkflowHooks; hooks?: WorkflowHooks;
httpResponse?: express.Response; httpResponse?: express.Response;
httpRequest?: express.Request; httpRequest?: express.Request;
restApiUrl: string; restApiUrl: string;
sendMessageToUI?: (source: string, message: any) => void; // tslint:disable-line:no-any sendMessageToUI?: (source: string, message: any) => void;
timezone: string; timezone: string;
webhookBaseUrl: string; webhookBaseUrl: string;
webhookWaitingBaseUrl: string; webhookWaitingBaseUrl: string;
@ -760,7 +879,15 @@ export interface IWorkflowExecuteAdditionalData {
executionTimeoutTimestamp?: number; executionTimeoutTimestamp?: number;
} }
export type WorkflowExecuteMode = 'cli' | 'error' | 'integrated' | 'internal' | 'manual' | 'retry' | 'trigger' | 'webhook'; export type WorkflowExecuteMode =
| 'cli'
| 'error'
| 'integrated'
| 'internal'
| 'manual'
| 'retry'
| 'trigger'
| 'webhook';
export type WorkflowActivateMode = 'init' | 'create' | 'update' | 'activate' | 'manual'; export type WorkflowActivateMode = 'init' | 'create' | 'update' | 'activate' | 'manual';
export interface IWorkflowHooksOptionalParameters { export interface IWorkflowHooksOptionalParameters {
@ -790,7 +917,7 @@ export interface IStatusCodeMessages {
export type CodexData = { export type CodexData = {
categories?: string[]; categories?: string[];
subcategories?: {[category: string]: string[]}; subcategories?: { [category: string]: string[] };
alias?: string[]; alias?: string[];
}; };

View file

@ -1,8 +1,6 @@
import { /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
ILogger, // eslint-disable-next-line import/no-cycle
LogTypes, import { ILogger, LogTypes } from './Interfaces';
} from './Interfaces';
let logger: ILogger | undefined; let logger: ILogger | undefined;

View file

@ -1,5 +1,13 @@
import { INode, IStatusCodeMessages, JsonObject} from '.'; /* eslint-disable @typescript-eslint/no-shadow */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable no-param-reassign */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
// eslint-disable-next-line max-classes-per-file
import { parseString } from 'xml2js'; import { parseString } from 'xml2js';
// eslint-disable-next-line import/no-cycle
import { INode, IStatusCodeMessages, JsonObject } from '.';
/** /**
* Top-level properties where an error message can be found in an API response. * Top-level properties where an error message can be found in an API response.
@ -33,7 +41,14 @@ const ERROR_MESSAGE_PROPERTIES = [
/** /**
* Top-level properties where an HTTP error code can be found in an API response. * Top-level properties where an HTTP error code can be found in an API response.
*/ */
const ERROR_STATUS_PROPERTIES = ['statusCode', 'status', 'code', 'status_code', 'errorCode', 'error_code']; const ERROR_STATUS_PROPERTIES = [
'statusCode',
'status',
'code',
'status_code',
'errorCode',
'error_code',
];
/** /**
* Properties where a nested object can be found in an API response. * Properties where a nested object can be found in an API response.
@ -46,8 +61,11 @@ const ERROR_NESTING_PROPERTIES = ['error', 'err', 'response', 'body', 'data'];
*/ */
abstract class NodeError extends Error { abstract class NodeError extends Error {
description: string | null | undefined; description: string | null | undefined;
cause: Error | JsonObject; cause: Error | JsonObject;
node: INode; node: INode;
timestamp: number; timestamp: number;
constructor(node: INode, error: Error | JsonObject) { constructor(node: INode, error: Error | JsonObject) {
@ -95,13 +113,17 @@ abstract class NodeError extends Error {
potentialKeys: string[], potentialKeys: string[],
traversalKeys: string[] = [], traversalKeys: string[] = [],
): string | null { ): string | null {
for(const key of potentialKeys) { // eslint-disable-next-line no-restricted-syntax
for (const key of potentialKeys) {
if (error[key]) { if (error[key]) {
if (typeof error[key] === 'string') return error[key] as string; if (typeof error[key] === 'string') return error[key] as string;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (typeof error[key] === 'number') return error[key]!.toString(); if (typeof error[key] === 'number') return error[key]!.toString();
if (Array.isArray(error[key])) { if (Array.isArray(error[key])) {
// @ts-ignore // @ts-ignore
const resolvedErrors: string[] = error[key].map((error) => { const resolvedErrors: string[] = error[key]
// @ts-ignore
.map((error) => {
if (typeof error === 'string') return error; if (typeof error === 'string') return error;
if (typeof error === 'number') return error.toString(); if (typeof error === 'number') return error.toString();
if (this.isTraversableObject(error)) { if (this.isTraversableObject(error)) {
@ -125,6 +147,7 @@ abstract class NodeError extends Error {
} }
} }
// eslint-disable-next-line no-restricted-syntax
for (const key of traversalKeys) { for (const key of traversalKeys) {
if (this.isTraversableObject(error[key])) { if (this.isTraversableObject(error[key])) {
const property = this.findProperty(error[key] as JsonObject, potentialKeys, traversalKeys); const property = this.findProperty(error[key] as JsonObject, potentialKeys, traversalKeys);
@ -140,8 +163,11 @@ abstract class NodeError extends Error {
/** /**
* Check if a value is an object with at least one key, i.e. it can be traversed. * Check if a value is an object with at least one key, i.e. it can be traversed.
*/ */
protected isTraversableObject(value: any): value is JsonObject { // tslint:disable-line:no-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
return value && typeof value === 'object' && !Array.isArray(value) && !!Object.keys(value).length; protected isTraversableObject(value: any): value is JsonObject {
return (
value && typeof value === 'object' && !Array.isArray(value) && !!Object.keys(value).length
);
} }
/** /**
@ -151,7 +177,10 @@ abstract class NodeError extends Error {
seen.add(obj); seen.add(obj);
Object.entries(obj).forEach(([key, value]) => { Object.entries(obj).forEach(([key, value]) => {
if (this.isTraversableObject(value)) { if (this.isTraversableObject(value)) {
seen.has(value) ? obj[key] = { circularReference: true } : this.removeCircularRefs(value, seen); // eslint-disable-next-line @typescript-eslint/no-unused-expressions
seen.has(value)
? (obj[key] = { circularReference: true })
: this.removeCircularRefs(value, seen);
return; return;
} }
if (Array.isArray(value)) { if (Array.isArray(value)) {
@ -173,7 +202,6 @@ abstract class NodeError extends Error {
* Class for instantiating an operational error, e.g. an invalid credentials error. * Class for instantiating an operational error, e.g. an invalid credentials error.
*/ */
export class NodeOperationError extends NodeError { export class NodeOperationError extends NodeError {
constructor(node: INode, error: Error | string) { constructor(node: INode, error: Error | string) {
if (typeof error === 'string') { if (typeof error === 'string') {
error = new Error(error); error = new Error(error);
@ -211,10 +239,16 @@ export class NodeApiError extends NodeError {
constructor( constructor(
node: INode, node: INode,
error: JsonObject, error: JsonObject,
{ message, description, httpCode, parseXml }: { message?: string, description?: string, httpCode?: string, parseXml?: boolean } = {}, {
message,
description,
httpCode,
parseXml,
}: { message?: string; description?: string; httpCode?: string; parseXml?: boolean } = {},
) { ) {
super(node, error); super(node, error);
if (error.error) { // only for request library error if (error.error) {
// only for request library error
this.removeCircularRefs(error.error as JsonObject); this.removeCircularRefs(error.error as JsonObject);
} }
if (message) { if (message) {
@ -236,11 +270,17 @@ export class NodeApiError extends NodeError {
} }
private setDescriptionFromXml(xml: string) { private setDescriptionFromXml(xml: string) {
// eslint-disable-next-line @typescript-eslint/naming-convention
parseString(xml, { explicitArray: false }, (_, result) => { parseString(xml, { explicitArray: false }, (_, result) => {
if (!result) return; if (!result) return;
const topLevelKey = Object.keys(result)[0]; const topLevelKey = Object.keys(result)[0];
this.description = this.findProperty(result[topLevelKey], ERROR_MESSAGE_PROPERTIES, ['Error'].concat(ERROR_NESTING_PROPERTIES)); this.description = this.findProperty(
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
result[topLevelKey],
ERROR_MESSAGE_PROPERTIES,
['Error'].concat(ERROR_NESTING_PROPERTIES),
);
}); });
} }

View file

@ -1,3 +1,17 @@
/* eslint-disable no-console */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-use-before-define */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
/* eslint-disable no-param-reassign */
/* eslint-disable no-continue */
/* eslint-disable prefer-spread */
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable import/no-cycle */
// eslint-disable-next-line import/no-extraneous-dependencies
import { get, isEqual } from 'lodash';
import { import {
IContextObject, IContextObject,
INode, INode,
@ -17,13 +31,7 @@ import {
WebhookHttpMethod, WebhookHttpMethod,
} from './Interfaces'; } from './Interfaces';
import { import { Workflow } from './Workflow';
Workflow
} from './Workflow';
import { get, isEqual } from 'lodash';
/** /**
* Gets special parameters which should be added to nodeTypes depending * Gets special parameters which should be added to nodeTypes depending
@ -99,12 +107,7 @@ export function getSpecialNodeParameters(nodeType: INodeType) {
}, },
displayOptions: { displayOptions: {
hide: { hide: {
mode: [ mode: ['custom', 'everyHour', 'everyMinute', 'everyX'],
'custom',
'everyHour',
'everyMinute',
'everyX',
],
}, },
}, },
default: 14, default: 14,
@ -120,11 +123,7 @@ export function getSpecialNodeParameters(nodeType: INodeType) {
}, },
displayOptions: { displayOptions: {
hide: { hide: {
mode: [ mode: ['custom', 'everyMinute', 'everyX'],
'custom',
'everyMinute',
'everyX',
],
}, },
}, },
default: 0, default: 0,
@ -136,9 +135,7 @@ export function getSpecialNodeParameters(nodeType: INodeType) {
type: 'number', type: 'number',
displayOptions: { displayOptions: {
show: { show: {
mode: [ mode: ['everyMonth'],
'everyMonth',
],
}, },
}, },
typeOptions: { typeOptions: {
@ -154,9 +151,7 @@ export function getSpecialNodeParameters(nodeType: INodeType) {
type: 'options', type: 'options',
displayOptions: { displayOptions: {
show: { show: {
mode: [ mode: ['everyWeek'],
'everyWeek',
],
}, },
}, },
options: [ options: [
@ -198,13 +193,12 @@ export function getSpecialNodeParameters(nodeType: INodeType) {
type: 'string', type: 'string',
displayOptions: { displayOptions: {
show: { show: {
mode: [ mode: ['custom'],
'custom',
],
}, },
}, },
default: '* * * * * *', default: '* * * * * *',
description: 'Use custom cron expression. Values and ranges as follows:<ul><li>Seconds: 0-59</li><li>Minutes: 0 - 59</li><li>Hours: 0 - 23</li><li>Day of Month: 1 - 31</li><li>Months: 0 - 11 (Jan - Dec)</li><li>Day of Week: 0 - 6 (Sun - Sat)</li></ul>', description:
'Use custom cron expression. Values and ranges as follows:<ul><li>Seconds: 0-59</li><li>Minutes: 0 - 59</li><li>Hours: 0 - 23</li><li>Day of Month: 1 - 31</li><li>Months: 0 - 11 (Jan - Dec)</li><li>Day of Week: 0 - 6 (Sun - Sat)</li></ul>',
}, },
{ {
displayName: 'Value', displayName: 'Value',
@ -216,9 +210,7 @@ export function getSpecialNodeParameters(nodeType: INodeType) {
}, },
displayOptions: { displayOptions: {
show: { show: {
mode: [ mode: ['everyX'],
'everyX',
],
}, },
}, },
default: 2, default: 2,
@ -230,9 +222,7 @@ export function getSpecialNodeParameters(nodeType: INodeType) {
type: 'options', type: 'options',
displayOptions: { displayOptions: {
show: { show: {
mode: [ mode: ['everyX'],
'everyX',
],
}, },
}, },
options: [ options: [
@ -258,7 +248,6 @@ export function getSpecialNodeParameters(nodeType: INodeType) {
return []; return [];
} }
/** /**
* Returns if the parameter should be displayed or not * Returns if the parameter should be displayed or not
* *
@ -269,7 +258,11 @@ export function getSpecialNodeParameters(nodeType: INodeType) {
* @param {INodeParameters} [nodeValuesRoot] The root node-parameter-data * @param {INodeParameters} [nodeValuesRoot] The root node-parameter-data
* @returns * @returns
*/ */
export function displayParameter(nodeValues: INodeParameters, parameter: INodeProperties | INodeCredentialDescription, nodeValuesRoot?: INodeParameters) { export function displayParameter(
nodeValues: INodeParameters,
parameter: INodeProperties | INodeCredentialDescription,
nodeValuesRoot?: INodeParameters,
) {
if (!parameter.displayOptions) { if (!parameter.displayOptions) {
return true; return true;
} }
@ -277,7 +270,8 @@ export function displayParameter(nodeValues: INodeParameters, parameter: INodePr
nodeValuesRoot = nodeValuesRoot || nodeValues; nodeValuesRoot = nodeValuesRoot || nodeValues;
let value; let value;
const values: any[] = []; // tslint:disable-line:no-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const values: any[] = [];
if (parameter.displayOptions.show) { if (parameter.displayOptions.show) {
// All the defined rules have to match to display parameter // All the defined rules have to match to display parameter
for (const propertyName of Object.keys(parameter.displayOptions.show)) { for (const propertyName of Object.keys(parameter.displayOptions.show)) {
@ -296,11 +290,14 @@ export function displayParameter(nodeValues: INodeParameters, parameter: INodePr
values.push.apply(values, value); values.push.apply(values, value);
} }
if (values.some(v => (typeof v) === 'string' && (v as string).charAt(0) === '=')) { if (values.some((v) => typeof v === 'string' && v.charAt(0) === '=')) {
return true; return true;
} }
if (values.length === 0 || !parameter.displayOptions.show[propertyName].some(v => values.includes(v))) { if (
values.length === 0 ||
!parameter.displayOptions.show[propertyName].some((v) => values.includes(v))
) {
return false; return false;
} }
} }
@ -324,7 +321,10 @@ export function displayParameter(nodeValues: INodeParameters, parameter: INodePr
values.push.apply(values, value); values.push.apply(values, value);
} }
if (values.length !== 0 && parameter.displayOptions.hide[propertyName].some(v => values.includes(v))) { if (
values.length !== 0 &&
parameter.displayOptions.hide[propertyName].some((v) => values.includes(v))
) {
return false; return false;
} }
} }
@ -333,7 +333,6 @@ export function displayParameter(nodeValues: INodeParameters, parameter: INodePr
return true; return true;
} }
/** /**
* Returns if the given parameter should be displayed or not considering the path * Returns if the given parameter should be displayed or not considering the path
* to the properties * to the properties
@ -345,28 +344,25 @@ export function displayParameter(nodeValues: INodeParameters, parameter: INodePr
* @param {string} path The path to the property * @param {string} path The path to the property
* @returns * @returns
*/ */
export function displayParameterPath(nodeValues: INodeParameters, parameter: INodeProperties | INodeCredentialDescription, path: string) { export function displayParameterPath(
nodeValues: INodeParameters,
parameter: INodeProperties | INodeCredentialDescription,
path: string,
) {
let resolvedNodeValues = nodeValues; let resolvedNodeValues = nodeValues;
if (path !== '') { if (path !== '') {
resolvedNodeValues = get( resolvedNodeValues = get(nodeValues, path) as INodeParameters;
nodeValues,
path,
) as INodeParameters;
} }
// Get the root parameter data // Get the root parameter data
let nodeValuesRoot = nodeValues; let nodeValuesRoot = nodeValues;
if (path && path.split('.').indexOf('parameters') === 0) { if (path && path.split('.').indexOf('parameters') === 0) {
nodeValuesRoot = get( nodeValuesRoot = get(nodeValues, 'parameters') as INodeParameters;
nodeValues,
'parameters',
) as INodeParameters;
} }
return displayParameter(resolvedNodeValues, parameter, nodeValuesRoot); return displayParameter(resolvedNodeValues, parameter, nodeValuesRoot);
} }
/** /**
* Returns the context data * Returns the context data
* *
@ -376,7 +372,11 @@ export function displayParameterPath(nodeValues: INodeParameters, parameter: INo
* @param {INode} [node] If type "node" is set the node to return the context of has to be supplied * @param {INode} [node] If type "node" is set the node to return the context of has to be supplied
* @returns {IContextObject} * @returns {IContextObject}
*/ */
export function getContext(runExecutionData: IRunExecutionData, type: string, node?: INode): IContextObject { export function getContext(
runExecutionData: IRunExecutionData,
type: string,
node?: INode,
): IContextObject {
if (runExecutionData.executionData === undefined) { if (runExecutionData.executionData === undefined) {
// TODO: Should not happen leave it for test now // TODO: Should not happen leave it for test now
throw new Error('The "executionData" is not initialized!'); throw new Error('The "executionData" is not initialized!');
@ -395,13 +395,13 @@ export function getContext(runExecutionData: IRunExecutionData, type: string, no
} }
if (runExecutionData.executionData.contextData[key] === undefined) { if (runExecutionData.executionData.contextData[key] === undefined) {
// eslint-disable-next-line no-param-reassign
runExecutionData.executionData.contextData[key] = {}; runExecutionData.executionData.contextData[key] = {};
} }
return runExecutionData.executionData.contextData[key]; return runExecutionData.executionData.contextData[key];
} }
/** /**
* Returns which parameters are dependent on which * Returns which parameters are dependent on which
* *
@ -409,7 +409,9 @@ export function getContext(runExecutionData: IRunExecutionData, type: string, no
* @param {INodeProperties[]} nodePropertiesArray * @param {INodeProperties[]} nodePropertiesArray
* @returns {IParameterDependencies} * @returns {IParameterDependencies}
*/ */
export function getParamterDependencies(nodePropertiesArray: INodeProperties[]): IParameterDependencies { export function getParamterDependencies(
nodePropertiesArray: INodeProperties[],
): IParameterDependencies {
const dependencies: IParameterDependencies = {}; const dependencies: IParameterDependencies = {};
let displayRule: string; let displayRule: string;
@ -436,7 +438,6 @@ export function getParamterDependencies(nodePropertiesArray: INodeProperties[]):
return dependencies; return dependencies;
} }
/** /**
* Returns in which order the parameters should be resolved * Returns in which order the parameters should be resolved
* to have the parameters available they depend on * to have the parameters available they depend on
@ -446,7 +447,10 @@ export function getParamterDependencies(nodePropertiesArray: INodeProperties[]):
* @param {IParameterDependencies} parameterDependencies * @param {IParameterDependencies} parameterDependencies
* @returns {number[]} * @returns {number[]}
*/ */
export function getParameterResolveOrder(nodePropertiesArray: INodeProperties[], parameterDependencies: IParameterDependencies): number[] { export function getParamterResolveOrder(
nodePropertiesArray: INodeProperties[],
parameterDependencies: IParameterDependencies,
): number[] {
const executionOrder: number[] = []; const executionOrder: number[] = [];
const indexToResolve = Array.from({ length: nodePropertiesArray.length }, (v, k) => k); const indexToResolve = Array.from({ length: nodePropertiesArray.length }, (v, k) => k);
const resolvedParamters: string[] = []; const resolvedParamters: string[] = [];
@ -457,7 +461,7 @@ export function getParameterResolveOrder(nodePropertiesArray: INodeProperties[],
let lastIndexLength = indexToResolve.length; let lastIndexLength = indexToResolve.length;
let lastIndexReduction = -1; let lastIndexReduction = -1;
let iterations = 0 ; let iterations = 0;
while (indexToResolve.length !== 0) { while (indexToResolve.length !== 0) {
iterations += 1; iterations += 1;
@ -495,7 +499,9 @@ export function getParameterResolveOrder(nodePropertiesArray: INodeProperties[],
} }
if (iterations > lastIndexReduction + nodePropertiesArray.length) { if (iterations > lastIndexReduction + nodePropertiesArray.length) {
throw new Error('Could not resolve parameter depenencies. Max iterations reached! Hint: If `displayOptions` are specified in any child parameter of a parent `collection` or `fixedCollection`, remove the `displayOptions` from the child parameter.'); throw new Error(
'Could not resolve parameter depenencies. Max iterations reached! Hint: If `displayOptions` are specified in any child parameter of a parent `collection` or `fixedCollection`, remove the `displayOptions` from the child parameter.',
);
} }
lastIndexLength = indexToResolve.length; lastIndexLength = indexToResolve.length;
} }
@ -503,7 +509,6 @@ export function getParameterResolveOrder(nodePropertiesArray: INodeProperties[],
return executionOrder; return executionOrder;
} }
/** /**
* Returns the node parameter values. Depending on the settings it either just returns the none * Returns the node parameter values. Depending on the settings it either just returns the none
* default values or it applies all the default values. * default values or it applies all the default values.
@ -518,7 +523,17 @@ export function getParameterResolveOrder(nodePropertiesArray: INodeProperties[],
* @param {INodeParameters} [nodeValuesRoot] The root node-parameter-data * @param {INodeParameters} [nodeValuesRoot] The root node-parameter-data
* @returns {(INodeParameters | null)} * @returns {(INodeParameters | null)}
*/ */
export function getNodeParameters(nodePropertiesArray: INodeProperties[], nodeValues: INodeParameters, returnDefaults: boolean, returnNoneDisplayed: boolean, onlySimpleTypes = false, dataIsResolved = false, nodeValuesRoot?: INodeParameters, parentType?: string, parameterDependencies?: IParameterDependencies): INodeParameters | null { export function getNodeParameters(
nodePropertiesArray: INodeProperties[],
nodeValues: INodeParameters,
returnDefaults: boolean,
returnNoneDisplayed: boolean,
onlySimpleTypes = false,
dataIsResolved = false,
nodeValuesRoot?: INodeParameters,
parentType?: string,
parameterDependencies?: IParameterDependencies,
): INodeParameters | null {
if (parameterDependencies === undefined) { if (parameterDependencies === undefined) {
parameterDependencies = getParamterDependencies(nodePropertiesArray); parameterDependencies = getParamterDependencies(nodePropertiesArray);
} }
@ -541,27 +556,43 @@ export function getNodeParameters(nodePropertiesArray: INodeProperties[], nodeVa
const nodeParametersFull: INodeParameters = {}; const nodeParametersFull: INodeParameters = {};
let nodeValuesDisplayCheck = nodeParametersFull; let nodeValuesDisplayCheck = nodeParametersFull;
if (dataIsResolved !== true && returnNoneDisplayed === false) { if (!dataIsResolved && !returnNoneDisplayed) {
nodeValuesDisplayCheck = getNodeParameters(nodePropertiesArray, nodeValues, true, true, true, true, nodeValuesRoot, parentType, parameterDependencies) as INodeParameters; nodeValuesDisplayCheck = getNodeParameters(
nodePropertiesArray,
nodeValues,
true,
true,
true,
true,
nodeValuesRoot,
parentType,
parameterDependencies,
) as INodeParameters;
} }
nodeValuesRoot = nodeValuesRoot || nodeValuesDisplayCheck; nodeValuesRoot = nodeValuesRoot || nodeValuesDisplayCheck;
// Go through the parameters in order of their dependencies // Go through the parameters in order of their dependencies
const parameterItterationOrderIndex = getParameterResolveOrder(nodePropertiesArray, parameterDependencies); const parameterItterationOrderIndex = getParamterResolveOrder(
nodePropertiesArray,
parameterDependencies,
);
for (const parameterIndex of parameterItterationOrderIndex) { for (const parameterIndex of parameterItterationOrderIndex) {
const nodeProperties = nodePropertiesArray[parameterIndex]; const nodeProperties = nodePropertiesArray[parameterIndex];
if (nodeValues[nodeProperties.name] === undefined && (returnDefaults === false || parentType === 'collection')) { if (
nodeValues[nodeProperties.name] === undefined &&
(!returnDefaults || parentType === 'collection')
) {
// The value is not defined so go to the next // The value is not defined so go to the next
continue; continue;
} }
if (returnNoneDisplayed === false && !displayParameter(nodeValuesDisplayCheck, nodeProperties, nodeValuesRoot)) { if (
if (returnNoneDisplayed === false) { !returnNoneDisplayed &&
continue; !displayParameter(nodeValuesDisplayCheck, nodeProperties, nodeValuesRoot)
} ) {
if (returnDefaults === false) { if (!returnNoneDisplayed || !returnDefaults) {
continue; continue;
} }
} }
@ -575,19 +606,27 @@ export function getNodeParameters(nodePropertiesArray: INodeProperties[], nodeVa
} }
} }
if (returnDefaults === true) { if (returnDefaults) {
// Set also when it has the default value // Set also when it has the default value
if (['boolean', 'number', 'options'].includes(nodeProperties.type)) { if (['boolean', 'number', 'options'].includes(nodeProperties.type)) {
// Boolean, numbers and options are special as false and 0 are valid values // Boolean, numbers and options are special as false and 0 are valid values
// and should not be replaced with default value // and should not be replaced with default value
nodeParameters[nodeProperties.name] = nodeValues[nodeProperties.name] !== undefined ? nodeValues[nodeProperties.name] : nodeProperties.default; nodeParameters[nodeProperties.name] =
nodeValues[nodeProperties.name] !== undefined
? nodeValues[nodeProperties.name]
: nodeProperties.default;
} else { } else {
nodeParameters[nodeProperties.name] = nodeValues[nodeProperties.name] || nodeProperties.default; nodeParameters[nodeProperties.name] =
nodeValues[nodeProperties.name] || nodeProperties.default;
} }
nodeParametersFull[nodeProperties.name] = nodeParameters[nodeProperties.name]; nodeParametersFull[nodeProperties.name] = nodeParameters[nodeProperties.name];
} else if ((nodeValues[nodeProperties.name] !== nodeProperties.default && typeof nodeValues[nodeProperties.name] !== 'object') || } else if (
(typeof nodeValues[nodeProperties.name] === 'object' && !isEqual(nodeValues[nodeProperties.name], nodeProperties.default)) || (nodeValues[nodeProperties.name] !== nodeProperties.default &&
(nodeValues[nodeProperties.name] !== undefined && parentType === 'collection')) { typeof nodeValues[nodeProperties.name] !== 'object') ||
(typeof nodeValues[nodeProperties.name] === 'object' &&
!isEqual(nodeValues[nodeProperties.name], nodeProperties.default)) ||
(nodeValues[nodeProperties.name] !== undefined && parentType === 'collection')
) {
// Set only if it is different to the default value // Set only if it is different to the default value
nodeParameters[nodeProperties.name] = nodeValues[nodeProperties.name]; nodeParameters[nodeProperties.name] = nodeValues[nodeProperties.name];
nodeParametersFull[nodeProperties.name] = nodeParameters[nodeProperties.name]; nodeParametersFull[nodeProperties.name] = nodeParameters[nodeProperties.name];
@ -595,7 +634,7 @@ export function getNodeParameters(nodePropertiesArray: INodeProperties[], nodeVa
} }
} }
if (onlySimpleTypes === true) { if (onlySimpleTypes) {
// It is only supposed to resolve the simple types. So continue. // It is only supposed to resolve the simple types. So continue.
continue; continue;
} }
@ -605,16 +644,21 @@ export function getNodeParameters(nodePropertiesArray: INodeProperties[], nodeVa
if (nodeProperties.type === 'collection') { if (nodeProperties.type === 'collection') {
// Is collection // Is collection
if (nodeProperties.typeOptions !== undefined && nodeProperties.typeOptions.multipleValues === true) { if (
nodeProperties.typeOptions !== undefined &&
nodeProperties.typeOptions.multipleValues === true
) {
// Multiple can be set so will be an array // Multiple can be set so will be an array
// Return directly the values like they are // Return directly the values like they are
if (nodeValues[nodeProperties.name] !== undefined) { if (nodeValues[nodeProperties.name] !== undefined) {
nodeParameters[nodeProperties.name] = nodeValues[nodeProperties.name]; nodeParameters[nodeProperties.name] = nodeValues[nodeProperties.name];
} else if (returnDefaults === true) { } else if (returnDefaults) {
// Does not have values defined but defaults should be returned // Does not have values defined but defaults should be returned
if (Array.isArray(nodeProperties.default)) { if (Array.isArray(nodeProperties.default)) {
nodeParameters[nodeProperties.name] = JSON.parse(JSON.stringify(nodeProperties.default)); nodeParameters[nodeProperties.name] = JSON.parse(
JSON.stringify(nodeProperties.default),
);
} else { } else {
// As it is probably wrong for many nodes, do we keep on returning an empty array if // As it is probably wrong for many nodes, do we keep on returning an empty array if
// anything else than an array is set as default // anything else than an array is set as default
@ -622,21 +666,28 @@ export function getNodeParameters(nodePropertiesArray: INodeProperties[], nodeVa
} }
} }
nodeParametersFull[nodeProperties.name] = nodeParameters[nodeProperties.name]; nodeParametersFull[nodeProperties.name] = nodeParameters[nodeProperties.name];
} else { } else if (nodeValues[nodeProperties.name] !== undefined) {
if (nodeValues[nodeProperties.name] !== undefined) {
// Has values defined so get them // Has values defined so get them
const tempNodeParameters = getNodeParameters(nodeProperties.options as INodeProperties[], nodeValues[nodeProperties.name] as INodeParameters, returnDefaults, returnNoneDisplayed, false, false, nodeValuesRoot, nodeProperties.type); const tempNodeParameters = getNodeParameters(
nodeProperties.options as INodeProperties[],
nodeValues[nodeProperties.name] as INodeParameters,
returnDefaults,
returnNoneDisplayed,
false,
false,
nodeValuesRoot,
nodeProperties.type,
);
if (tempNodeParameters !== null) { if (tempNodeParameters !== null) {
nodeParameters[nodeProperties.name] = tempNodeParameters; nodeParameters[nodeProperties.name] = tempNodeParameters;
nodeParametersFull[nodeProperties.name] = nodeParameters[nodeProperties.name]; nodeParametersFull[nodeProperties.name] = nodeParameters[nodeProperties.name];
} }
} else if (returnDefaults === true) { } else if (returnDefaults) {
// Does not have values defined but defaults should be returned // Does not have values defined but defaults should be returned
nodeParameters[nodeProperties.name] = JSON.parse(JSON.stringify(nodeProperties.default)); nodeParameters[nodeProperties.name] = JSON.parse(JSON.stringify(nodeProperties.default));
nodeParametersFull[nodeProperties.name] = nodeParameters[nodeProperties.name]; nodeParametersFull[nodeProperties.name] = nodeParameters[nodeProperties.name];
} }
}
} else if (nodeProperties.type === 'fixedCollection') { } else if (nodeProperties.type === 'fixedCollection') {
// Is fixedCollection // Is fixedCollection
@ -646,7 +697,7 @@ export function getNodeParameters(nodePropertiesArray: INodeProperties[], nodeVa
let nodePropertyOptions: INodePropertyCollection | undefined; let nodePropertyOptions: INodePropertyCollection | undefined;
let propertyValues = nodeValues[nodeProperties.name]; let propertyValues = nodeValues[nodeProperties.name];
if (returnDefaults === true) { if (returnDefaults) {
if (propertyValues === undefined) { if (propertyValues === undefined) {
propertyValues = JSON.parse(JSON.stringify(nodeProperties.default)); propertyValues = JSON.parse(JSON.stringify(nodeProperties.default));
} }
@ -654,20 +705,39 @@ export function getNodeParameters(nodePropertiesArray: INodeProperties[], nodeVa
// Iterate over all collections // Iterate over all collections
for (const itemName of Object.keys(propertyValues || {})) { for (const itemName of Object.keys(propertyValues || {})) {
if (nodeProperties.typeOptions !== undefined && nodeProperties.typeOptions.multipleValues === true) { if (
nodeProperties.typeOptions !== undefined &&
nodeProperties.typeOptions.multipleValues === true
) {
// Multiple can be set so will be an array // Multiple can be set so will be an array
const tempArrayValue: INodeParameters[] = []; const tempArrayValue: INodeParameters[] = [];
// Iterate over all items as it contains multiple ones // Iterate over all items as it contains multiple ones
for (const nodeValue of (propertyValues as INodeParameters)[itemName] as INodeParameters[]) { for (const nodeValue of (propertyValues as INodeParameters)[
nodePropertyOptions = nodeProperties!.options!.find((nodePropertyOptions) => nodePropertyOptions.name === itemName) as INodePropertyCollection; itemName
] as INodeParameters[]) {
nodePropertyOptions = nodeProperties.options!.find(
// eslint-disable-next-line @typescript-eslint/no-shadow
(nodePropertyOptions) => nodePropertyOptions.name === itemName,
) as INodePropertyCollection;
if (nodePropertyOptions === undefined) { if (nodePropertyOptions === undefined) {
throw new Error(`Could not find property option "${itemName}" for "${nodeProperties.name}"`); throw new Error(
`Could not find property option "${itemName}" for "${nodeProperties.name}"`,
);
} }
tempNodePropertiesArray = (nodePropertyOptions as INodePropertyCollection).values!; tempNodePropertiesArray = nodePropertyOptions.values!;
tempValue = getNodeParameters(tempNodePropertiesArray, nodeValue as INodeParameters, returnDefaults, returnNoneDisplayed, false, false, nodeValuesRoot, nodeProperties.type); tempValue = getNodeParameters(
tempNodePropertiesArray,
nodeValue,
returnDefaults,
returnNoneDisplayed,
false,
false,
nodeValuesRoot,
nodeProperties.type,
);
if (tempValue !== null) { if (tempValue !== null) {
tempArrayValue.push(tempValue); tempArrayValue.push(tempValue);
} }
@ -678,11 +748,23 @@ export function getNodeParameters(nodePropertiesArray: INodeProperties[], nodeVa
tempNodeParameters = {}; tempNodeParameters = {};
// Get the options of the current item // Get the options of the current item
const nodePropertyOptions = nodeProperties!.options!.find((data) => data.name === itemName); // eslint-disable-next-line @typescript-eslint/no-shadow
const nodePropertyOptions = nodeProperties.options!.find(
(data) => data.name === itemName,
);
if (nodePropertyOptions !== undefined) { if (nodePropertyOptions !== undefined) {
tempNodePropertiesArray = (nodePropertyOptions as INodePropertyCollection).values!; tempNodePropertiesArray = (nodePropertyOptions as INodePropertyCollection).values!;
tempValue = getNodeParameters(tempNodePropertiesArray, (nodeValues[nodeProperties.name] as INodeParameters)[itemName] as INodeParameters, returnDefaults, returnNoneDisplayed, false, false, nodeValuesRoot, nodeProperties.type); tempValue = getNodeParameters(
tempNodePropertiesArray,
(nodeValues[nodeProperties.name] as INodeParameters)[itemName] as INodeParameters,
returnDefaults,
returnNoneDisplayed,
false,
false,
nodeValuesRoot,
nodeProperties.type,
);
if (tempValue !== null) { if (tempValue !== null) {
Object.assign(tempNodeParameters, tempValue); Object.assign(tempNodeParameters, tempValue);
} }
@ -694,13 +776,15 @@ export function getNodeParameters(nodePropertiesArray: INodeProperties[], nodeVa
} }
} }
if (Object.keys(collectionValues).length !== 0 || returnDefaults === true) { if (Object.keys(collectionValues).length !== 0 || returnDefaults) {
// Set only if value got found // Set only if value got found
if (returnDefaults === true) { if (returnDefaults) {
// Set also when it has the default value // Set also when it has the default value
if (collectionValues === undefined) { if (collectionValues === undefined) {
nodeParameters[nodeProperties.name] = JSON.parse(JSON.stringify(nodeProperties.default)); nodeParameters[nodeProperties.name] = JSON.parse(
JSON.stringify(nodeProperties.default),
);
} else { } else {
nodeParameters[nodeProperties.name] = collectionValues; nodeParameters[nodeProperties.name] = collectionValues;
} }
@ -717,7 +801,6 @@ export function getNodeParameters(nodePropertiesArray: INodeProperties[], nodeVa
return nodeParameters; return nodeParameters;
} }
/** /**
* Brings the output data in a format that can be returned from a node * Brings the output data in a format that can be returned from a node
* *
@ -726,7 +809,10 @@ export function getNodeParameters(nodePropertiesArray: INodeProperties[], nodeVa
* @param {number} [outputIndex=0] * @param {number} [outputIndex=0]
* @returns {Promise<INodeExecutionData[][]>} * @returns {Promise<INodeExecutionData[][]>}
*/ */
export async function prepareOutputData(outputData: INodeExecutionData[], outputIndex = 0): Promise<INodeExecutionData[][]> { export async function prepareOutputData(
outputData: INodeExecutionData[],
outputIndex = 0,
): Promise<INodeExecutionData[][]> {
// TODO: Check if node has output with that index // TODO: Check if node has output with that index
const returnData = []; const returnData = [];
@ -739,8 +825,6 @@ export async function prepareOutputData(outputData: INodeExecutionData[], output
return returnData; return returnData;
} }
/** /**
* Returns all the webhooks which should be created for the give node * Returns all the webhooks which should be created for the give node
* *
@ -749,7 +833,12 @@ export async function prepareOutputData(outputData: INodeExecutionData[], output
* @param {INode} node * @param {INode} node
* @returns {IWebhookData[]} * @returns {IWebhookData[]}
*/ */
export function getNodeWebhooks(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, ignoreRestartWehbooks = false): IWebhookData[] { export function getNodeWebhooks(
workflow: Workflow,
node: INode,
additionalData: IWorkflowExecuteAdditionalData,
ignoreRestartWehbooks = false,
): IWebhookData[] {
if (node.disabled === true) { if (node.disabled === true) {
// Node is disabled so webhooks will also not be enabled // Node is disabled so webhooks will also not be enabled
return []; return [];
@ -767,15 +856,21 @@ export function getNodeWebhooks(workflow: Workflow, node: INode, additionalData:
const returnData: IWebhookData[] = []; const returnData: IWebhookData[] = [];
for (const webhookDescription of nodeType.description.webhooks) { for (const webhookDescription of nodeType.description.webhooks) {
if (ignoreRestartWehbooks && webhookDescription.restartWebhook === true) {
if (ignoreRestartWehbooks === true && webhookDescription.restartWebhook === true) {
continue; continue;
} }
let nodeWebhookPath = workflow.expression.getSimpleParameterValue(node, webhookDescription['path'], mode, {}); let nodeWebhookPath = workflow.expression.getSimpleParameterValue(
node,
webhookDescription.path,
mode,
{},
);
if (nodeWebhookPath === undefined) { if (nodeWebhookPath === undefined) {
// TODO: Use a proper logger // TODO: Use a proper logger
console.error(`No webhook path could be found for node "${node.name}" in workflow "${workflowId}".`); console.error(
`No webhook path could be found for node "${node.name}" in workflow "${workflowId}".`,
);
continue; continue;
} }
@ -788,15 +883,35 @@ export function getNodeWebhooks(workflow: Workflow, node: INode, additionalData:
nodeWebhookPath = nodeWebhookPath.slice(0, -1); nodeWebhookPath = nodeWebhookPath.slice(0, -1);
} }
const isFullPath: boolean = workflow.expression.getSimpleParameterValue(node, webhookDescription['isFullPath'], 'internal', {}, false) as boolean; const isFullPath: boolean = workflow.expression.getSimpleParameterValue(
const restartWebhook: boolean = workflow.expression.getSimpleParameterValue(node, webhookDescription['restartWebhook'], 'internal', {}, false) as boolean; node,
webhookDescription.isFullPath,
'internal',
{},
false,
) as boolean;
const restartWebhook: boolean = workflow.expression.getSimpleParameterValue(
node,
webhookDescription.restartWebhook,
'internal',
{},
false,
) as boolean;
const path = getNodeWebhookPath(workflowId, node, nodeWebhookPath, isFullPath, restartWebhook); const path = getNodeWebhookPath(workflowId, node, nodeWebhookPath, isFullPath, restartWebhook);
const httpMethod = workflow.expression.getSimpleParameterValue(node, webhookDescription['httpMethod'], mode, {}, 'GET'); const httpMethod = workflow.expression.getSimpleParameterValue(
node,
webhookDescription.httpMethod,
mode,
{},
'GET',
);
if (httpMethod === undefined) { if (httpMethod === undefined) {
// TODO: Use a proper logger // TODO: Use a proper logger
console.error(`The webhook "${path}" for node "${node.name}" in workflow "${workflowId}" could not be added because the httpMethod is not defined.`); console.error(
`The webhook "${path}" for node "${node.name}" in workflow "${workflowId}" could not be added because the httpMethod is not defined.`,
);
continue; continue;
} }
@ -838,10 +953,17 @@ export function getNodeWebhooksBasic(workflow: Workflow, node: INode): IWebhookD
const returnData: IWebhookData[] = []; const returnData: IWebhookData[] = [];
for (const webhookDescription of nodeType.description.webhooks) { for (const webhookDescription of nodeType.description.webhooks) {
let nodeWebhookPath = workflow.expression.getSimpleParameterValue(node, webhookDescription['path'], mode, {}); let nodeWebhookPath = workflow.expression.getSimpleParameterValue(
node,
webhookDescription.path,
mode,
{},
);
if (nodeWebhookPath === undefined) { if (nodeWebhookPath === undefined) {
// TODO: Use a proper logger // TODO: Use a proper logger
console.error(`No webhook path could be found for node "${node.name}" in workflow "${workflowId}".`); console.error(
`No webhook path could be found for node "${node.name}" in workflow "${workflowId}".`,
);
continue; continue;
} }
@ -854,19 +976,32 @@ export function getNodeWebhooksBasic(workflow: Workflow, node: INode): IWebhookD
nodeWebhookPath = nodeWebhookPath.slice(0, -1); nodeWebhookPath = nodeWebhookPath.slice(0, -1);
} }
const isFullPath: boolean = workflow.expression.getSimpleParameterValue(node, webhookDescription['isFullPath'], mode, {}, false) as boolean; const isFullPath: boolean = workflow.expression.getSimpleParameterValue(
node,
webhookDescription.isFullPath,
mode,
{},
false,
) as boolean;
const path = getNodeWebhookPath(workflowId, node, nodeWebhookPath, isFullPath); const path = getNodeWebhookPath(workflowId, node, nodeWebhookPath, isFullPath);
const httpMethod = workflow.expression.getSimpleParameterValue(node, webhookDescription['httpMethod'], mode, {}); const httpMethod = workflow.expression.getSimpleParameterValue(
node,
webhookDescription.httpMethod,
mode,
{},
);
if (httpMethod === undefined) { if (httpMethod === undefined) {
// TODO: Use a proper logger // TODO: Use a proper logger
console.error(`The webhook "${path}" for node "${node.name}" in workflow "${workflowId}" could not be added because the httpMethod is not defined.`); console.error(
`The webhook "${path}" for node "${node.name}" in workflow "${workflowId}" could not be added because the httpMethod is not defined.`,
);
continue; continue;
} }
//@ts-ignore // @ts-ignore
returnData.push({ returnData.push({
httpMethod: httpMethod.toString() as WebhookHttpMethod, httpMethod: httpMethod.toString() as WebhookHttpMethod,
node: node.name, node: node.name,
@ -879,7 +1014,6 @@ export function getNodeWebhooksBasic(workflow: Workflow, node: INode): IWebhookD
return returnData; return returnData;
} }
/** /**
* Returns the webhook path * Returns the webhook path
* *
@ -889,11 +1023,18 @@ export function getNodeWebhooksBasic(workflow: Workflow, node: INode): IWebhookD
* @param {string} path * @param {string} path
* @returns {string} * @returns {string}
*/ */
export function getNodeWebhookPath(workflowId: string, node: INode, path: string, isFullPath?: boolean, restartWebhook?: boolean): string { export function getNodeWebhookPath(
workflowId: string,
node: INode,
path: string,
isFullPath?: boolean,
restartWebhook?: boolean,
): string {
let webhookPath = ''; let webhookPath = '';
if (restartWebhook === true) { if (restartWebhook === true) {
return path; return path;
} else if (node.webhookId === undefined) { }
if (node.webhookId === undefined) {
webhookPath = `${workflowId}/${encodeURIComponent(node.name.toLowerCase())}/${path}`; webhookPath = `${workflowId}/${encodeURIComponent(node.name.toLowerCase())}/${path}`;
} else { } else {
if (isFullPath === true) { if (isFullPath === true) {
@ -904,7 +1045,6 @@ export function getNodeWebhookPath(workflowId: string, node: INode, path: string
return webhookPath; return webhookPath;
} }
/** /**
* Returns the webhook URL * Returns the webhook URL
* *
@ -916,7 +1056,13 @@ export function getNodeWebhookPath(workflowId: string, node: INode, path: string
* @param {boolean} isFullPath * @param {boolean} isFullPath
* @returns {string} * @returns {string}
*/ */
export function getNodeWebhookUrl(baseUrl: string, workflowId: string, node: INode, path: string, isFullPath?: boolean): string { export function getNodeWebhookUrl(
baseUrl: string,
workflowId: string,
node: INode,
path: string,
isFullPath?: boolean,
): string {
if ((path.startsWith(':') || path.includes('/:')) && node.webhookId) { if ((path.startsWith(':') || path.includes('/:')) && node.webhookId) {
// setting this to false to prefix the webhookId // setting this to false to prefix the webhookId
isFullPath = false; isFullPath = false;
@ -927,7 +1073,6 @@ export function getNodeWebhookUrl(baseUrl: string, workflowId: string, node: INo
return `${baseUrl}/${getNodeWebhookPath(workflowId, node, path, isFullPath)}`; return `${baseUrl}/${getNodeWebhookPath(workflowId, node, path, isFullPath)}`;
} }
/** /**
* Returns all the parameter-issues of the node * Returns all the parameter-issues of the node
* *
@ -936,7 +1081,10 @@ export function getNodeWebhookUrl(baseUrl: string, workflowId: string, node: INo
* @param {INode} node The data of the node * @param {INode} node The data of the node
* @returns {(INodeIssues | null)} * @returns {(INodeIssues | null)}
*/ */
export function getNodeParametersIssues(nodePropertiesArray: INodeProperties[], node: INode): INodeIssues | null { export function getNodeParametersIssues(
nodePropertiesArray: INodeProperties[],
node: INode,
): INodeIssues | null {
const foundIssues: INodeIssues = {}; const foundIssues: INodeIssues = {};
let propertyIssues: INodeIssues; let propertyIssues: INodeIssues;
@ -957,7 +1105,6 @@ export function getNodeParametersIssues(nodePropertiesArray: INodeProperties[],
return foundIssues; return foundIssues;
} }
/** /**
* Returns the issues of the node as string * Returns the issues of the node as string
* *
@ -973,12 +1120,10 @@ export function nodeIssuesToString(issues: INodeIssues, node?: INode): string[]
nodeIssues.push(`Execution Error.`); nodeIssues.push(`Execution Error.`);
} }
const objectProperties = [ const objectProperties = ['parameters', 'credentials'];
'parameters',
'credentials',
];
let issueText: string, parameterName: string; let issueText: string;
let parameterName: string;
for (const propertyName of objectProperties) { for (const propertyName of objectProperties) {
if (issues[propertyName] !== undefined) { if (issues[propertyName] !== undefined) {
for (parameterName of Object.keys(issues[propertyName] as object)) { for (parameterName of Object.keys(issues[propertyName] as object)) {
@ -1000,7 +1145,6 @@ export function nodeIssuesToString(issues: INodeIssues, node?: INode): string[]
return nodeIssues; return nodeIssues;
} }
/** /**
* Adds an issue if the parameter is not defined * Adds an issue if the parameter is not defined
* *
@ -1009,11 +1153,17 @@ export function nodeIssuesToString(issues: INodeIssues, node?: INode): string[]
* @param {INodeProperties} nodeProperties The properties of the node * @param {INodeProperties} nodeProperties The properties of the node
* @param {NodeParameterValue} value The value of the parameter * @param {NodeParameterValue} value The value of the parameter
*/ */
export function addToIssuesIfMissing(foundIssues: INodeIssues, nodeProperties: INodeProperties, value: NodeParameterValue) { export function addToIssuesIfMissing(
foundIssues: INodeIssues,
nodeProperties: INodeProperties,
value: NodeParameterValue,
) {
// TODO: Check what it really has when undefined // TODO: Check what it really has when undefined
if ((nodeProperties.type === 'string' && (value === '' || value === undefined)) || if (
(nodeProperties.type === 'string' && (value === '' || value === undefined)) ||
(nodeProperties.type === 'multiOptions' && Array.isArray(value) && value.length === 0) || (nodeProperties.type === 'multiOptions' && Array.isArray(value) && value.length === 0) ||
(nodeProperties.type === 'dateTime' && value === undefined)) { (nodeProperties.type === 'dateTime' && value === undefined)
) {
// Parameter is requried but empty // Parameter is requried but empty
if (foundIssues.parameters === undefined) { if (foundIssues.parameters === undefined) {
foundIssues.parameters = {}; foundIssues.parameters = {};
@ -1022,11 +1172,12 @@ export function addToIssuesIfMissing(foundIssues: INodeIssues, nodeProperties: I
foundIssues.parameters[nodeProperties.name] = []; foundIssues.parameters[nodeProperties.name] = [];
} }
foundIssues.parameters[nodeProperties.name].push(`Parameter "${nodeProperties.displayName}" is required.`); foundIssues.parameters[nodeProperties.name].push(
`Parameter "${nodeProperties.displayName}" is required.`,
);
} }
} }
/** /**
* Returns the parameter value * Returns the parameter value
* *
@ -1036,15 +1187,14 @@ export function addToIssuesIfMissing(foundIssues: INodeIssues, nodeProperties: I
* @param {string} path The path to the properties * @param {string} path The path to the properties
* @returns * @returns
*/ */
export function getParameterValueByPath(nodeValues: INodeParameters, parameterName: string, path: string) { export function getParameterValueByPath(
return get( nodeValues: INodeParameters,
nodeValues, parameterName: string,
path ? path + '.' + parameterName : parameterName, path: string,
); ) {
return get(nodeValues, path ? `${path}.${parameterName}` : parameterName);
} }
/** /**
* Returns all the issues with the given node-values * Returns all the issues with the given node-values
* *
@ -1054,7 +1204,11 @@ export function getParameterValueByPath(nodeValues: INodeParameters, parameterNa
* @param {string} path The path to the properties * @param {string} path The path to the properties
* @returns {INodeIssues} * @returns {INodeIssues}
*/ */
export function getParameterIssues(nodeProperties: INodeProperties, nodeValues: INodeParameters, path: string): INodeIssues { export function getParameterIssues(
nodeProperties: INodeProperties,
nodeValues: INodeParameters,
path: string,
): INodeIssues {
const foundIssues: INodeIssues = {}; const foundIssues: INodeIssues = {};
let value; let value;
@ -1062,11 +1216,15 @@ export function getParameterIssues(nodeProperties: INodeProperties, nodeValues:
if (displayParameterPath(nodeValues, nodeProperties, path)) { if (displayParameterPath(nodeValues, nodeProperties, path)) {
value = getParameterValueByPath(nodeValues, nodeProperties.name, path); value = getParameterValueByPath(nodeValues, nodeProperties.name, path);
if (nodeProperties.typeOptions !== undefined && nodeProperties.typeOptions.multipleValues !== undefined) { if (
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
nodeProperties.typeOptions !== undefined &&
nodeProperties.typeOptions.multipleValues !== undefined
) {
// Multiple can be set so will be an array // Multiple can be set so will be an array
if (Array.isArray(value)) { if (Array.isArray(value)) {
for (const singleValue of value as NodeParameterValue[]) { for (const singleValue of value as NodeParameterValue[]) {
addToIssuesIfMissing(foundIssues, nodeProperties, singleValue as NodeParameterValue); addToIssuesIfMissing(foundIssues, nodeProperties, singleValue);
} }
} }
} else { } else {
@ -1106,7 +1264,7 @@ export function getParameterIssues(nodeProperties: INodeProperties, nodeValues:
}); });
} }
} else if (nodeProperties.type === 'fixedCollection') { } else if (nodeProperties.type === 'fixedCollection') {
basePath = basePath ? `${basePath}.` : '' + nodeProperties.name + '.'; basePath = basePath ? `${basePath}.` : `${nodeProperties.name}.`;
let propertyOptions: INodePropertyCollection; let propertyOptions: INodePropertyCollection;
for (propertyOptions of nodeProperties.options as INodePropertyCollection[]) { for (propertyOptions of nodeProperties.options as INodePropertyCollection[]) {
@ -1116,14 +1274,18 @@ export function getParameterIssues(nodeProperties: INodeProperties, nodeValues:
continue; continue;
} }
if (nodeProperties.typeOptions !== undefined && nodeProperties.typeOptions.multipleValues !== undefined) { if (
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
nodeProperties.typeOptions !== undefined &&
nodeProperties.typeOptions.multipleValues !== undefined
) {
// Multiple can be set so will be an array of objects // Multiple can be set so will be an array of objects
if (Array.isArray(value)) { if (Array.isArray(value)) {
for (let i = 0; i < (value as INodeParameters[]).length; i++) { for (let i = 0; i < (value as INodeParameters[]).length; i++) {
for (const option of propertyOptions.values) { for (const option of propertyOptions.values) {
checkChildNodeProperties.push({ checkChildNodeProperties.push({
basePath: `${basePath}${propertyOptions.name}[${i}]`, basePath: `${basePath}${propertyOptions.name}[${i}]`,
data: option as INodeProperties, data: option,
}); });
} }
} }
@ -1133,7 +1295,7 @@ export function getParameterIssues(nodeProperties: INodeProperties, nodeValues:
for (const option of propertyOptions.values) { for (const option of propertyOptions.values) {
checkChildNodeProperties.push({ checkChildNodeProperties.push({
basePath: basePath + propertyOptions.name, basePath: basePath + propertyOptions.name,
data: option as INodeProperties, data: option,
}); });
} }
} }
@ -1146,14 +1308,13 @@ export function getParameterIssues(nodeProperties: INodeProperties, nodeValues:
let propertyIssues; let propertyIssues;
for (const optionData of checkChildNodeProperties) { for (const optionData of checkChildNodeProperties) {
propertyIssues = getParameterIssues(optionData.data as INodeProperties, nodeValues, optionData.basePath); propertyIssues = getParameterIssues(optionData.data, nodeValues, optionData.basePath);
mergeIssues(foundIssues, propertyIssues); mergeIssues(foundIssues, propertyIssues);
} }
return foundIssues; return foundIssues;
} }
/** /**
* Merges multiple NodeIssues together * Merges multiple NodeIssues together
* *
@ -1172,10 +1333,7 @@ export function mergeIssues(destination: INodeIssues, source: INodeIssues | null
destination.execution = true; destination.execution = true;
} }
const objectProperties = [ const objectProperties = ['parameters', 'credentials'];
'parameters',
'credentials',
];
let destinationProperty: INodeIssueObjectProperty; let destinationProperty: INodeIssueObjectProperty;
for (const propertyName of objectProperties) { for (const propertyName of objectProperties) {
@ -1190,7 +1348,10 @@ export function mergeIssues(destination: INodeIssues, source: INodeIssues | null
if (destinationProperty[parameterName] === undefined) { if (destinationProperty[parameterName] === undefined) {
destinationProperty[parameterName] = []; destinationProperty[parameterName] = [];
} }
destinationProperty[parameterName].push.apply(destinationProperty[parameterName], (source[propertyName] as INodeIssueObjectProperty)[parameterName]); destinationProperty[parameterName].push.apply(
destinationProperty[parameterName],
(source[propertyName] as INodeIssueObjectProperty)[parameterName],
);
} }
} }
} }
@ -1200,8 +1361,6 @@ export function mergeIssues(destination: INodeIssues, source: INodeIssues | null
} }
} }
/** /**
* Merges the given node properties * Merges the given node properties
* *
@ -1209,10 +1368,13 @@ export function mergeIssues(destination: INodeIssues, source: INodeIssues | null
* @param {INodeProperties[]} mainProperties * @param {INodeProperties[]} mainProperties
* @param {INodeProperties[]} addProperties * @param {INodeProperties[]} addProperties
*/ */
export function mergeNodeProperties(mainProperties: INodeProperties[], addProperties: INodeProperties[]): void { export function mergeNodeProperties(
mainProperties: INodeProperties[],
addProperties: INodeProperties[],
): void {
let existingIndex: number; let existingIndex: number;
for (const property of addProperties) { for (const property of addProperties) {
existingIndex = mainProperties.findIndex(element => element.name === property.name); existingIndex = mainProperties.findIndex((element) => element.name === property.name);
if (existingIndex === -1) { if (existingIndex === -1) {
// Property does not exist yet, so add // Property does not exist yet, so add

View file

@ -1,20 +1,35 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { /* eslint-disable @typescript-eslint/no-shadow */
IDataObject, /* eslint-disable no-param-reassign */
IObservableObject, /* eslint-disable no-underscore-dangle */
} from './'; // eslint-disable-next-line import/no-cycle
import { IDataObject, IObservableObject } from '.';
export interface IObservableOptions { export interface IObservableOptions {
ignoreEmptyOnFirstChild?: boolean; ignoreEmptyOnFirstChild?: boolean;
} }
export function create(target: IDataObject, parent?: IObservableObject, option?: IObservableOptions, depth?: number): IDataObject { export function create(
target: IDataObject,
parent?: IObservableObject,
option?: IObservableOptions,
depth?: number,
): IDataObject {
// eslint-disable-next-line no-param-reassign, @typescript-eslint/prefer-nullish-coalescing
depth = depth || 0; depth = depth || 0;
// Make all the children of target also observeable // Make all the children of target also observeable
// eslint-disable-next-line no-restricted-syntax
for (const key in target) { for (const key in target) {
if (typeof target[key] === 'object' && target[key] !== null) { if (typeof target[key] === 'object' && target[key] !== null) {
target[key] = create(target[key] as IDataObject, (parent || target) as IObservableObject, option, depth + 1); // eslint-disable-next-line no-param-reassign
target[key] = create(
target[key] as IDataObject,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
(parent || target) as IObservableObject,
option,
depth + 1,
);
} }
} }
@ -23,6 +38,7 @@ export function create(target: IDataObject, parent?: IObservableObject, option?:
writable: true, writable: true,
}); });
return new Proxy(target, { return new Proxy(target, {
// eslint-disable-next-line @typescript-eslint/no-shadow
deleteProperty(target, name) { deleteProperty(target, name) {
if (parent === undefined) { if (parent === undefined) {
// If no parent is given mark current data as changed // If no parent is given mark current data as changed
@ -39,8 +55,15 @@ export function create(target: IDataObject, parent?: IObservableObject, option?:
set(target, name, value) { set(target, name, value) {
if (parent === undefined) { if (parent === undefined) {
// If no parent is given mark current data as changed // If no parent is given mark current data as changed
if (option !== undefined && option.ignoreEmptyOnFirstChild === true && depth === 0 if (
&& target[name.toString()] === undefined && typeof value === 'object' && Object.keys(value).length === 0) { option !== undefined &&
option.ignoreEmptyOnFirstChild === true &&
depth === 0 &&
target[name.toString()] === undefined &&
typeof value === 'object' &&
Object.keys(value).length === 0
// eslint-disable-next-line no-empty
) {
} else { } else {
(target as IObservableObject).__dataChanged = true; (target as IObservableObject).__dataChanged = true;
} }

View file

@ -1,4 +1,15 @@
/* eslint-disable no-param-reassign */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-for-in-array */
/* eslint-disable no-prototype-builtins */
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
/* eslint-disable no-underscore-dangle */
/* eslint-disable no-continue */
/* eslint-disable no-restricted-syntax */
/* eslint-disable import/no-cycle */
// eslint-disable-next-line import/no-cycle
import { import {
Expression, Expression,
IConnections, IConnections,
@ -26,20 +37,27 @@ import {
WebhookSetupMethodNames, WebhookSetupMethodNames,
WorkflowActivateMode, WorkflowActivateMode,
WorkflowExecuteMode, WorkflowExecuteMode,
} from './'; } from '.';
import { IConnection, IDataObject, IObservableObject } from './Interfaces'; import { IConnection, IDataObject, IObservableObject } from './Interfaces';
export class Workflow { export class Workflow {
id: string | undefined; id: string | undefined;
name: string | undefined; name: string | undefined;
nodes: INodes = {}; nodes: INodes = {};
connectionsBySourceNode: IConnections; connectionsBySourceNode: IConnections;
connectionsByDestinationNode: IConnections; connectionsByDestinationNode: IConnections;
nodeTypes: INodeTypes; nodeTypes: INodeTypes;
expression: Expression; expression: Expression;
active: boolean; active: boolean;
settings: IWorkflowSettings; settings: IWorkflowSettings;
// To save workflow specific static data like for example // To save workflow specific static data like for example
@ -47,7 +65,16 @@ export class Workflow {
staticData: IDataObject; staticData: IDataObject;
// constructor(id: string | undefined, nodes: INode[], connections: IConnections, active: boolean, nodeTypes: INodeTypes, staticData?: IDataObject, settings?: IWorkflowSettings) { // constructor(id: string | undefined, nodes: INode[], connections: IConnections, active: boolean, nodeTypes: INodeTypes, staticData?: IDataObject, settings?: IWorkflowSettings) {
constructor(parameters: {id?: string, name?: string, nodes: INode[], connections: IConnections, active: boolean, nodeTypes: INodeTypes, staticData?: IDataObject, settings?: IWorkflowSettings}) { constructor(parameters: {
id?: string;
name?: string;
nodes: INode[];
connections: IConnections;
active: boolean;
nodeTypes: INodeTypes;
staticData?: IDataObject;
settings?: IWorkflowSettings;
}) {
this.id = parameters.id; this.id = parameters.id;
this.name = parameters.name; this.name = parameters.name;
this.nodeTypes = parameters.nodeTypes; this.nodeTypes = parameters.nodeTypes;
@ -70,7 +97,12 @@ export class Workflow {
} }
// Add default values // Add default values
const nodeParameters = NodeHelpers.getNodeParameters(nodeType.description.properties, node.parameters, true, false); const nodeParameters = NodeHelpers.getNodeParameters(
nodeType.description.properties,
node.parameters,
true,
false,
);
node.parameters = nodeParameters !== null ? nodeParameters : {}; node.parameters = nodeParameters !== null ? nodeParameters : {};
} }
this.connectionsBySourceNode = parameters.connections; this.connectionsBySourceNode = parameters.connections;
@ -80,15 +112,15 @@ export class Workflow {
this.active = parameters.active || false; this.active = parameters.active || false;
this.staticData = ObservableObject.create(parameters.staticData || {}, undefined, { ignoreEmptyOnFirstChild: true }); this.staticData = ObservableObject.create(parameters.staticData || {}, undefined, {
ignoreEmptyOnFirstChild: true,
});
this.settings = parameters.settings || {}; this.settings = parameters.settings || {};
this.expression = new Expression(this); this.expression = new Expression(this);
} }
/** /**
* The default connections are by source node. This function rewrites them by destination nodes * The default connections are by source node. This function rewrites them by destination nodes
* to easily find parent nodes. * to easily find parent nodes.
@ -140,8 +172,6 @@ export class Workflow {
return returnConnection; return returnConnection;
} }
/** /**
* A workflow can only be activated if it has a node which has either triggers * A workflow can only be activated if it has a node which has either triggers
* or webhooks defined. * or webhooks defined.
@ -162,6 +192,7 @@ export class Workflow {
continue; continue;
} }
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
if (ignoreNodeTypes !== undefined && ignoreNodeTypes.includes(node.type)) { if (ignoreNodeTypes !== undefined && ignoreNodeTypes.includes(node.type)) {
continue; continue;
} }
@ -173,7 +204,11 @@ export class Workflow {
continue; continue;
} }
if (nodeType.poll !== undefined || nodeType.trigger !== undefined || nodeType.webhook !== undefined) { if (
nodeType.poll !== undefined ||
nodeType.trigger !== undefined ||
nodeType.webhook !== undefined
) {
// Is a trigger node. So workflow can be activated. // Is a trigger node. So workflow can be activated.
return true; return true;
} }
@ -182,8 +217,6 @@ export class Workflow {
return false; return false;
} }
/** /**
* Checks if everything in the workflow is complete * Checks if everything in the workflow is complete
* and ready to be executed. If it returns null everything * and ready to be executed. If it returns null everything
@ -216,7 +249,7 @@ export class Workflow {
typeUnknown: true, typeUnknown: true,
}; };
} else { } else {
nodeIssues = NodeHelpers.getNodeParametersIssues(nodeType.description.properties!, node); nodeIssues = NodeHelpers.getNodeParametersIssues(nodeType.description.properties, node);
} }
if (nodeIssues !== null) { if (nodeIssues !== null) {
@ -231,8 +264,6 @@ export class Workflow {
return workflowIssues; return workflowIssues;
} }
/** /**
* Returns the static data of the workflow. * Returns the static data of the workflow.
* It gets saved with the workflow and will be the same for * It gets saved with the workflow and will be the same for
@ -249,11 +280,15 @@ export class Workflow {
key = 'global'; key = 'global';
} else if (type === 'node') { } else if (type === 'node') {
if (node === undefined) { if (node === undefined) {
throw new Error(`The request data of context type "node" the node parameter has to be set!`); throw new Error(
`The request data of context type "node" the node parameter has to be set!`,
);
} }
key = `node:${node.name}`; key = `node:${node.name}`;
} else { } else {
throw new Error(`The context type "${type}" is not know. Only "global" and node" are supported!`); throw new Error(
`The context type "${type}" is not know. Only "global" and node" are supported!`,
);
} }
if (this.staticData[key] === undefined) { if (this.staticData[key] === undefined) {
@ -265,8 +300,6 @@ export class Workflow {
return this.staticData[key] as IDataObject; return this.staticData[key] as IDataObject;
} }
/** /**
* Returns all the trigger nodes in the workflow. * Returns all the trigger nodes in the workflow.
* *
@ -274,10 +307,9 @@ export class Workflow {
* @memberof Workflow * @memberof Workflow
*/ */
getTriggerNodes(): INode[] { getTriggerNodes(): INode[] {
return this.queryNodes((nodeType: INodeType) => !!nodeType.trigger ); return this.queryNodes((nodeType: INodeType) => !!nodeType.trigger);
} }
/** /**
* Returns all the poll nodes in the workflow * Returns all the poll nodes in the workflow
* *
@ -285,10 +317,9 @@ export class Workflow {
* @memberof Workflow * @memberof Workflow
*/ */
getPollNodes(): INode[] { getPollNodes(): INode[] {
return this.queryNodes((nodeType: INodeType) => !!nodeType.poll ); return this.queryNodes((nodeType: INodeType) => !!nodeType.poll);
} }
/** /**
* Returns all the nodes in the workflow for which the given * Returns all the nodes in the workflow for which the given
* checkFunction return true * checkFunction return true
@ -321,8 +352,6 @@ export class Workflow {
return returnNodes; return returnNodes;
} }
/** /**
* Returns the node with the given name if it exists else null * Returns the node with the given name if it exists else null
* *
@ -338,7 +367,6 @@ export class Workflow {
return null; return null;
} }
/** /**
* Renames nodes in expressions * Renames nodes in expressions
* *
@ -348,7 +376,11 @@ export class Workflow {
* @returns {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[])} * @returns {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[])}
* @memberof Workflow * @memberof Workflow
*/ */
renameNodeInExpressions(parameterValue: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[], currentName: string, newName: string): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] { renameNodeInExpressions(
parameterValue: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[],
currentName: string,
newName: string,
): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] {
if (typeof parameterValue !== 'object') { if (typeof parameterValue !== 'object') {
// Reached the actual value // Reached the actual value
if (typeof parameterValue === 'string' && parameterValue.charAt(0) === '=') { if (typeof parameterValue === 'string' && parameterValue.charAt(0) === '=') {
@ -362,7 +394,11 @@ export class Workflow {
// In case some special characters are used in name escape them // In case some special characters are used in name escape them
const currentNameEscaped = currentName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const currentNameEscaped = currentName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
parameterValue = parameterValue.replace(new RegExp(`(\\$node(\.|\\["|\\[\'))${currentNameEscaped}((\.|"\\]|\'\\]))`, 'g'), `$1${newName}$3`); parameterValue = parameterValue.replace(
// eslint-disable-next-line no-useless-escape
new RegExp(`(\\$node(\.|\\["|\\[\'))${currentNameEscaped}((\.|"\\]|\'\\]))`, 'g'),
`$1${newName}$3`,
);
} }
} }
@ -370,7 +406,8 @@ export class Workflow {
} }
if (Array.isArray(parameterValue)) { if (Array.isArray(parameterValue)) {
const returnArray: any[] = []; // tslint:disable-line:no-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const returnArray: any[] = [];
for (const currentValue of parameterValue) { for (const currentValue of parameterValue) {
returnArray.push(this.renameNodeInExpressions(currentValue, currentName, newName)); returnArray.push(this.renameNodeInExpressions(currentValue, currentName, newName));
@ -379,17 +416,21 @@ export class Workflow {
return returnArray; return returnArray;
} }
const returnData: any = {}; // tslint:disable-line:no-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const returnData: any = {};
for (const parameterName of Object.keys(parameterValue || {})) { for (const parameterName of Object.keys(parameterValue || {})) {
returnData[parameterName] = this.renameNodeInExpressions(parameterValue![parameterName], currentName, newName); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
returnData[parameterName] = this.renameNodeInExpressions(
parameterValue![parameterName],
currentName,
newName,
);
} }
return returnData; return returnData;
} }
/** /**
* Rename a node in the workflow * Rename a node in the workflow
* *
@ -398,7 +439,6 @@ export class Workflow {
* @memberof Workflow * @memberof Workflow
*/ */
renameNode(currentName: string, newName: string) { renameNode(currentName: string, newName: string) {
// Rename the node itself // Rename the node itself
if (this.nodes[currentName] !== undefined) { if (this.nodes[currentName] !== undefined) {
this.nodes[newName] = this.nodes[currentName]; this.nodes[newName] = this.nodes[currentName];
@ -409,7 +449,11 @@ export class Workflow {
// Update the expressions which reference the node // Update the expressions which reference the node
// with its old name // with its old name
for (const node of Object.values(this.nodes)) { for (const node of Object.values(this.nodes)) {
node.parameters = this.renameNodeInExpressions(node.parameters, currentName, newName) as INodeParameters; node.parameters = this.renameNodeInExpressions(
node.parameters,
currentName,
newName,
) as INodeParameters;
} }
// Change all source connections // Change all source connections
@ -419,12 +463,21 @@ export class Workflow {
} }
// Change all destination connections // Change all destination connections
let sourceNode: string, type: string, sourceIndex: string, connectionIndex: string, connectionData: IConnection; let sourceNode: string;
let type: string;
let sourceIndex: string;
let connectionIndex: string;
let connectionData: IConnection;
for (sourceNode of Object.keys(this.connectionsBySourceNode)) { for (sourceNode of Object.keys(this.connectionsBySourceNode)) {
for (type of Object.keys(this.connectionsBySourceNode[sourceNode])) { for (type of Object.keys(this.connectionsBySourceNode[sourceNode])) {
for (sourceIndex of Object.keys(this.connectionsBySourceNode[sourceNode][type])) { for (sourceIndex of Object.keys(this.connectionsBySourceNode[sourceNode][type])) {
for (connectionIndex of Object.keys(this.connectionsBySourceNode[sourceNode][type][parseInt(sourceIndex, 10)])) { for (connectionIndex of Object.keys(
connectionData = this.connectionsBySourceNode[sourceNode][type][parseInt(sourceIndex, 10)][parseInt(connectionIndex, 10)]; this.connectionsBySourceNode[sourceNode][type][parseInt(sourceIndex, 10)],
)) {
connectionData =
this.connectionsBySourceNode[sourceNode][type][parseInt(sourceIndex, 10)][
parseInt(connectionIndex, 10)
];
if (connectionData.node === currentName) { if (connectionData.node === currentName) {
connectionData.node = newName; connectionData.node = newName;
} }
@ -434,11 +487,11 @@ export class Workflow {
} }
// Use the updated connections to create updated connections by destionation nodes // Use the updated connections to create updated connections by destionation nodes
this.connectionsByDestinationNode = this.__getConnectionsByDestination(this.connectionsBySourceNode); this.connectionsByDestinationNode = this.__getConnectionsByDestination(
this.connectionsBySourceNode,
);
} }
/** /**
* Finds the highest parent nodes of the node with the given name * Finds the highest parent nodes of the node with the given name
* *
@ -448,7 +501,12 @@ export class Workflow {
* @returns {string[]} * @returns {string[]}
* @memberof Workflow * @memberof Workflow
*/ */
getHighestNode(nodeName: string, type = 'main', nodeConnectionIndex?:number, checkedNodes?: string[]): string[] { getHighestNode(
nodeName: string,
type = 'main',
nodeConnectionIndex?: number,
checkedNodes?: string[],
): string[] {
const currentHighest: string[] = []; const currentHighest: string[] = [];
if (this.nodes[nodeName].disabled === false) { if (this.nodes[nodeName].disabled === false) {
// If the current node is not disabled itself is the highest // If the current node is not disabled itself is the highest
@ -467,23 +525,28 @@ export class Workflow {
checkedNodes = checkedNodes || []; checkedNodes = checkedNodes || [];
if (checkedNodes!.includes(nodeName)) { if (checkedNodes.includes(nodeName)) {
// Node got checked already before // Node got checked already before
return currentHighest; return currentHighest;
} }
checkedNodes!.push(nodeName); checkedNodes.push(nodeName);
const returnNodes: string[] = []; const returnNodes: string[] = [];
let addNodes: string[]; let addNodes: string[];
let connectionsByIndex: IConnection[]; let connectionsByIndex: IConnection[];
for (let connectionIndex = 0; connectionIndex < this.connectionsByDestinationNode[nodeName][type].length; connectionIndex++) { for (
let connectionIndex = 0;
connectionIndex < this.connectionsByDestinationNode[nodeName][type].length;
connectionIndex++
) {
if (nodeConnectionIndex !== undefined && nodeConnectionIndex !== connectionIndex) { if (nodeConnectionIndex !== undefined && nodeConnectionIndex !== connectionIndex) {
// If a connection-index is given ignore all other ones // If a connection-index is given ignore all other ones
continue; continue;
} }
connectionsByIndex = this.connectionsByDestinationNode[nodeName][type][connectionIndex]; connectionsByIndex = this.connectionsByDestinationNode[nodeName][type][connectionIndex];
// eslint-disable-next-line @typescript-eslint/no-loop-func
connectionsByIndex.forEach((connection) => { connectionsByIndex.forEach((connection) => {
if (checkedNodes!.includes(connection.node)) { if (checkedNodes!.includes(connection.node)) {
// Node got checked already before // Node got checked already before
@ -512,8 +575,6 @@ export class Workflow {
return returnNodes; return returnNodes;
} }
/** /**
* Returns all the after the given one * Returns all the after the given one
* *
@ -527,8 +588,6 @@ export class Workflow {
return this.getConnectedNodes(this.connectionsBySourceNode, nodeName, type, depth); return this.getConnectedNodes(this.connectionsBySourceNode, nodeName, type, depth);
} }
/** /**
* Returns all the nodes before the given one * Returns all the nodes before the given one
* *
@ -542,8 +601,6 @@ export class Workflow {
return this.getConnectedNodes(this.connectionsByDestinationNode, nodeName, type, depth); return this.getConnectedNodes(this.connectionsByDestinationNode, nodeName, type, depth);
} }
/** /**
* Gets all the nodes which are connected nodes starting from * Gets all the nodes which are connected nodes starting from
* the given one * the given one
@ -556,7 +613,13 @@ export class Workflow {
* @returns {string[]} * @returns {string[]}
* @memberof Workflow * @memberof Workflow
*/ */
getConnectedNodes(connections: IConnections, nodeName: string, type = 'main', depth = -1, checkedNodes?: string[]): string[] { getConnectedNodes(
connections: IConnections,
nodeName: string,
type = 'main',
depth = -1,
checkedNodes?: string[],
): string[] {
depth = depth === -1 ? -1 : depth; depth = depth === -1 ? -1 : depth;
const newDepth = depth === -1 ? depth : depth - 1; const newDepth = depth === -1 ? depth : depth - 1;
if (depth === 0) { if (depth === 0) {
@ -576,12 +639,12 @@ export class Workflow {
checkedNodes = checkedNodes || []; checkedNodes = checkedNodes || [];
if (checkedNodes!.includes(nodeName)) { if (checkedNodes.includes(nodeName)) {
// Node got checked already before // Node got checked already before
return []; return [];
} }
checkedNodes!.push(nodeName); checkedNodes.push(nodeName);
const returnNodes: string[] = []; const returnNodes: string[] = [];
let addNodes: string[]; let addNodes: string[];
@ -597,7 +660,13 @@ export class Workflow {
returnNodes.unshift(connection.node); returnNodes.unshift(connection.node);
addNodes = this.getConnectedNodes(connections, connection.node, type, newDepth, checkedNodes); addNodes = this.getConnectedNodes(
connections,
connection.node,
type,
newDepth,
checkedNodes,
);
for (i = addNodes.length; i--; i > 0) { for (i = addNodes.length; i--; i > 0) {
// Because nodes can have multiple parents it is possible that // Because nodes can have multiple parents it is possible that
@ -620,8 +689,6 @@ export class Workflow {
return returnNodes; return returnNodes;
} }
/** /**
* Returns via which output of the parent-node the node * Returns via which output of the parent-node the node
* is connected to. * is connected to.
@ -634,7 +701,13 @@ export class Workflow {
* @returns {(number | undefined)} * @returns {(number | undefined)}
* @memberof Workflow * @memberof Workflow
*/ */
getNodeConnectionOutputIndex(nodeName: string, parentNodeName: string, type = 'main', depth = -1, checkedNodes?: string[]): number | undefined { getNodeConnectionOutputIndex(
nodeName: string,
parentNodeName: string,
type = 'main',
depth = -1,
checkedNodes?: string[],
): number | undefined {
const node = this.getNode(parentNodeName); const node = this.getNode(parentNodeName);
if (node === null) { if (node === null) {
return undefined; return undefined;
@ -665,12 +738,12 @@ export class Workflow {
checkedNodes = checkedNodes || []; checkedNodes = checkedNodes || [];
if (checkedNodes!.includes(nodeName)) { if (checkedNodes.includes(nodeName)) {
// Node got checked already before // Node got checked already before
return undefined; return undefined;
} }
checkedNodes!.push(nodeName); checkedNodes.push(nodeName);
let outputIndex: number | undefined; let outputIndex: number | undefined;
for (const connectionsByIndex of this.connectionsByDestinationNode[nodeName][type]) { for (const connectionsByIndex of this.connectionsByDestinationNode[nodeName][type]) {
@ -679,12 +752,18 @@ export class Workflow {
return connection.index; return connection.index;
} }
if (checkedNodes!.includes(connection.node)) { if (checkedNodes.includes(connection.node)) {
// Node got checked already before so continue with the next one // Node got checked already before so continue with the next one
continue; continue;
} }
outputIndex = this.getNodeConnectionOutputIndex(connection.node, parentNodeName, type, newDepth, checkedNodes); outputIndex = this.getNodeConnectionOutputIndex(
connection.node,
parentNodeName,
type,
newDepth,
checkedNodes,
);
if (outputIndex !== undefined) { if (outputIndex !== undefined) {
return outputIndex; return outputIndex;
@ -695,9 +774,6 @@ export class Workflow {
return undefined; return undefined;
} }
/** /**
* Returns from which of the given nodes the workflow should get started from * Returns from which of the given nodes the workflow should get started from
* *
@ -713,7 +789,6 @@ export class Workflow {
node = this.nodes[nodeName]; node = this.nodes[nodeName];
nodeType = this.nodeTypes.getByName(node.type) as INodeType; nodeType = this.nodeTypes.getByName(node.type) as INodeType;
if (nodeType.trigger !== undefined || nodeType.poll !== undefined) { if (nodeType.trigger !== undefined || nodeType.poll !== undefined) {
if (node.disabled === true) { if (node.disabled === true) {
continue; continue;
@ -734,8 +809,6 @@ export class Workflow {
return undefined; return undefined;
} }
/** /**
* Returns the start node to start the worfklow from * Returns the start node to start the worfklow from
* *
@ -744,7 +817,6 @@ export class Workflow {
* @memberof Workflow * @memberof Workflow
*/ */
getStartNode(destinationNode?: string): INode | undefined { getStartNode(destinationNode?: string): INode | undefined {
if (destinationNode) { if (destinationNode) {
// Find the highest parent nodes of the given one // Find the highest parent nodes of the given one
const nodeNames = this.getHighestNode(destinationNode); const nodeNames = this.getHighestNode(destinationNode);
@ -769,8 +841,6 @@ export class Workflow {
return this.__getStartNode(Object.keys(this.nodes)); return this.__getStartNode(Object.keys(this.nodes));
} }
/** /**
* Executes the Webhooks method of the node * Executes the Webhooks method of the node
* *
@ -781,11 +851,17 @@ export class Workflow {
* @returns {(Promise<boolean | undefined>)} * @returns {(Promise<boolean | undefined>)}
* @memberof Workflow * @memberof Workflow
*/ */
async runWebhookMethod(method: WebhookSetupMethodNames, webhookData: IWebhookData, nodeExecuteFunctions: INodeExecuteFunctions, mode: WorkflowExecuteMode, activation: WorkflowActivateMode, isTest?: boolean): Promise<boolean | undefined> { async runWebhookMethod(
method: WebhookSetupMethodNames,
webhookData: IWebhookData,
nodeExecuteFunctions: INodeExecuteFunctions,
mode: WorkflowExecuteMode,
activation: WorkflowActivateMode,
isTest?: boolean,
): Promise<boolean | undefined> {
const node = this.getNode(webhookData.node) as INode; const node = this.getNode(webhookData.node) as INode;
const nodeType = this.nodeTypes.getByName(node.type) as INodeType; const nodeType = this.nodeTypes.getByName(node.type) as INodeType;
if (nodeType.webhookMethods === undefined) { if (nodeType.webhookMethods === undefined) {
return; return;
} }
@ -798,11 +874,19 @@ export class Workflow {
return; return;
} }
const thisArgs = nodeExecuteFunctions.getExecuteHookFunctions(this, node, webhookData.workflowExecuteAdditionalData, mode, activation, isTest, webhookData); const thisArgs = nodeExecuteFunctions.getExecuteHookFunctions(
this,
node,
webhookData.workflowExecuteAdditionalData,
mode,
activation,
isTest,
webhookData,
);
// eslint-disable-next-line consistent-return
return nodeType.webhookMethods[webhookData.webhookDescription.name][method]!.call(thisArgs); return nodeType.webhookMethods[webhookData.webhookDescription.name][method]!.call(thisArgs);
} }
/** /**
* Runs the given trigger node so that it can trigger the workflow * Runs the given trigger node so that it can trigger the workflow
* when the node has data. * when the node has data.
@ -814,7 +898,13 @@ export class Workflow {
* @returns {(Promise<ITriggerResponse | undefined>)} * @returns {(Promise<ITriggerResponse | undefined>)}
* @memberof Workflow * @memberof Workflow
*/ */
async runTrigger(node: INode, getTriggerFunctions: IGetExecuteTriggerFunctions, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): Promise<ITriggerResponse | undefined> { async runTrigger(
node: INode,
getTriggerFunctions: IGetExecuteTriggerFunctions,
additionalData: IWorkflowExecuteAdditionalData,
mode: WorkflowExecuteMode,
activation: WorkflowActivateMode,
): Promise<ITriggerResponse | undefined> {
const triggerFunctions = getTriggerFunctions(this, node, additionalData, mode, activation); const triggerFunctions = getTriggerFunctions(this, node, additionalData, mode, activation);
const nodeType = this.nodeTypes.getByName(node.type); const nodeType = this.nodeTypes.getByName(node.type);
@ -824,28 +914,29 @@ export class Workflow {
} }
if (!nodeType.trigger) { if (!nodeType.trigger) {
throw new Error(`The node type "${node.type}" of node "${node.name}" does not have a trigger function defined.`); throw new Error(
`The node type "${node.type}" of node "${node.name}" does not have a trigger function defined.`,
);
} }
if (mode === 'manual') { if (mode === 'manual') {
// In manual mode we do not just start the trigger function we also // In manual mode we do not just start the trigger function we also
// want to be able to get informed as soon as the first data got emitted // want to be able to get informed as soon as the first data got emitted
const triggerResponse = await nodeType.trigger!.call(triggerFunctions); const triggerResponse = await nodeType.trigger.call(triggerFunctions);
// Add the manual trigger response which resolves when the first time data got emitted // Add the manual trigger response which resolves when the first time data got emitted
triggerResponse!.manualTriggerResponse = new Promise((resolve) => { triggerResponse!.manualTriggerResponse = new Promise((resolve) => {
// eslint-disable-next-line @typescript-eslint/no-shadow
triggerFunctions.emit = ((resolve) => (data: INodeExecutionData[][]) => { triggerFunctions.emit = ((resolve) => (data: INodeExecutionData[][]) => {
resolve(data); resolve(data);
})(resolve); })(resolve);
}); });
return triggerResponse; return triggerResponse;
} else { }
// In all other modes simply start the trigger // In all other modes simply start the trigger
return nodeType.trigger!.call(triggerFunctions); return nodeType.trigger.call(triggerFunctions);
} }
}
/** /**
* Runs the given trigger node so that it can trigger the workflow * Runs the given trigger node so that it can trigger the workflow
@ -856,7 +947,10 @@ export class Workflow {
* @returns * @returns
* @memberof Workflow * @memberof Workflow
*/ */
async runPoll(node: INode, pollFunctions: IPollFunctions): Promise<INodeExecutionData[][] | null> { async runPoll(
node: INode,
pollFunctions: IPollFunctions,
): Promise<INodeExecutionData[][] | null> {
const nodeType = this.nodeTypes.getByName(node.type); const nodeType = this.nodeTypes.getByName(node.type);
if (nodeType === undefined) { if (nodeType === undefined) {
@ -864,13 +958,14 @@ export class Workflow {
} }
if (!nodeType.poll) { if (!nodeType.poll) {
throw new Error(`The node type "${node.type}" of node "${node.name}" does not have a poll function defined.`); throw new Error(
`The node type "${node.type}" of node "${node.name}" does not have a poll function defined.`,
);
} }
return nodeType.poll!.call(pollFunctions); return nodeType.poll.call(pollFunctions);
} }
/** /**
* Executes the webhook data to see what it should return and if the * Executes the webhook data to see what it should return and if the
* workflow should be started or not * workflow should be started or not
@ -882,7 +977,13 @@ export class Workflow {
* @returns {Promise<IWebhookResponseData>} * @returns {Promise<IWebhookResponseData>}
* @memberof Workflow * @memberof Workflow
*/ */
async runWebhook(webhookData: IWebhookData, node: INode, additionalData: IWorkflowExecuteAdditionalData, nodeExecuteFunctions: INodeExecuteFunctions, mode: WorkflowExecuteMode): Promise<IWebhookResponseData> { async runWebhook(
webhookData: IWebhookData,
node: INode,
additionalData: IWorkflowExecuteAdditionalData,
nodeExecuteFunctions: INodeExecuteFunctions,
mode: WorkflowExecuteMode,
): Promise<IWebhookResponseData> {
const nodeType = this.nodeTypes.getByName(node.type); const nodeType = this.nodeTypes.getByName(node.type);
if (nodeType === undefined) { if (nodeType === undefined) {
throw new Error(`The type of the webhook node "${node.name}" is not known.`); throw new Error(`The type of the webhook node "${node.name}" is not known.`);
@ -890,11 +991,16 @@ export class Workflow {
throw new Error(`The node "${node.name}" does not have any webhooks defined.`); throw new Error(`The node "${node.name}" does not have any webhooks defined.`);
} }
const thisArgs = nodeExecuteFunctions.getExecuteWebhookFunctions(this, node, additionalData, mode, webhookData); const thisArgs = nodeExecuteFunctions.getExecuteWebhookFunctions(
this,
node,
additionalData,
mode,
webhookData,
);
return nodeType.webhook.call(thisArgs); return nodeType.webhook.call(thisArgs);
} }
/** /**
* Executes the given node. * Executes the given node.
* *
@ -908,7 +1014,15 @@ export class Workflow {
* @returns {(Promise<INodeExecutionData[][] | null>)} * @returns {(Promise<INodeExecutionData[][] | null>)}
* @memberof Workflow * @memberof Workflow
*/ */
async runNode(node: INode, inputData: ITaskDataConnections, runExecutionData: IRunExecutionData, runIndex: number, additionalData: IWorkflowExecuteAdditionalData, nodeExecuteFunctions: INodeExecuteFunctions, mode: WorkflowExecuteMode): Promise<INodeExecutionData[][] | null | undefined> { async runNode(
node: INode,
inputData: ITaskDataConnections,
runExecutionData: IRunExecutionData,
runIndex: number,
additionalData: IWorkflowExecuteAdditionalData,
nodeExecuteFunctions: INodeExecuteFunctions,
mode: WorkflowExecuteMode,
): Promise<INodeExecutionData[][] | null | undefined> {
if (node.disabled === true) { if (node.disabled === true) {
// If node is disabled simply pass the data through // If node is disabled simply pass the data through
// return NodeRunHelpers. // return NodeRunHelpers.
@ -917,7 +1031,7 @@ export class Workflow {
if (inputData.main[0] === null) { if (inputData.main[0] === null) {
return undefined; return undefined;
} }
return [(inputData.main[0] as INodeExecutionData[])]; return [inputData.main[0]];
} }
return undefined; return undefined;
} }
@ -935,7 +1049,7 @@ export class Workflow {
if (inputData.hasOwnProperty('main') && inputData.main.length > 0) { if (inputData.hasOwnProperty('main') && inputData.main.length > 0) {
// We always use the data of main input and the first input for executeSingle // We always use the data of main input and the first input for executeSingle
connectionInputData = (inputData.main[0] as INodeExecutionData[]); connectionInputData = inputData.main[0] as INodeExecutionData[];
} }
if (connectionInputData.length === 0) { if (connectionInputData.length === 0) {
@ -944,7 +1058,10 @@ export class Workflow {
} }
} }
if (runExecutionData.resultData.lastNodeExecuted === node.name && runExecutionData.resultData.error !== undefined) { if (
runExecutionData.resultData.lastNodeExecuted === node.name &&
runExecutionData.resultData.error !== undefined
) {
// The node did already fail. So throw an error here that it displays and logs it correctly. // The node did already fail. So throw an error here that it displays and logs it correctly.
// Does get used by webhook and trigger nodes in case they throw an error that it is possible // Does get used by webhook and trigger nodes in case they throw an error that it is possible
// to log the error and display in Editor-UI. // to log the error and display in Editor-UI.
@ -959,7 +1076,8 @@ export class Workflow {
connectionInputData = connectionInputData.slice(0, 1); connectionInputData = connectionInputData.slice(0, 1);
const newInputData: ITaskDataConnections = {}; const newInputData: ITaskDataConnections = {};
for (const inputName of Object.keys(inputData)) { for (const inputName of Object.keys(inputData)) {
newInputData[inputName] = inputData[inputName].map(input => { newInputData[inputName] = inputData[inputName].map((input) => {
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
return input && input.slice(0, 1); return input && input.slice(0, 1);
}); });
} }
@ -970,9 +1088,19 @@ export class Workflow {
const returnPromises: Array<Promise<INodeExecutionData>> = []; const returnPromises: Array<Promise<INodeExecutionData>> = [];
for (let itemIndex = 0; itemIndex < connectionInputData.length; itemIndex++) { for (let itemIndex = 0; itemIndex < connectionInputData.length; itemIndex++) {
const thisArgs = nodeExecuteFunctions.getExecuteSingleFunctions(this, runExecutionData, runIndex, connectionInputData, inputData, node, itemIndex, additionalData, mode); const thisArgs = nodeExecuteFunctions.getExecuteSingleFunctions(
this,
runExecutionData,
runIndex,
connectionInputData,
inputData,
node,
itemIndex,
additionalData,
mode,
);
returnPromises.push(nodeType.executeSingle!.call(thisArgs)); returnPromises.push(nodeType.executeSingle.call(thisArgs));
} }
if (returnPromises.length === 0) { if (returnPromises.length === 0) {
@ -990,21 +1118,41 @@ export class Workflow {
return [promiseResults]; return [promiseResults];
} }
} else if (nodeType.execute) { } else if (nodeType.execute) {
const thisArgs = nodeExecuteFunctions.getExecuteFunctions(this, runExecutionData, runIndex, connectionInputData, inputData, node, additionalData, mode); const thisArgs = nodeExecuteFunctions.getExecuteFunctions(
this,
runExecutionData,
runIndex,
connectionInputData,
inputData,
node,
additionalData,
mode,
);
return nodeType.execute.call(thisArgs); return nodeType.execute.call(thisArgs);
} else if (nodeType.poll) { } else if (nodeType.poll) {
if (mode === 'manual') { if (mode === 'manual') {
// In manual mode run the poll function // In manual mode run the poll function
const thisArgs = nodeExecuteFunctions.getExecutePollFunctions(this, node, additionalData, mode, 'manual'); const thisArgs = nodeExecuteFunctions.getExecutePollFunctions(
this,
node,
additionalData,
mode,
'manual',
);
return nodeType.poll.call(thisArgs); return nodeType.poll.call(thisArgs);
} else { }
// In any other mode pass data through as it already contains the result of the poll // In any other mode pass data through as it already contains the result of the poll
return inputData.main as INodeExecutionData[][]; return inputData.main as INodeExecutionData[][];
}
} else if (nodeType.trigger) { } else if (nodeType.trigger) {
if (mode === 'manual') { if (mode === 'manual') {
// In manual mode start the trigger // In manual mode start the trigger
const triggerResponse = await this.runTrigger(node, nodeExecuteFunctions.getExecuteTriggerFunctions, additionalData, mode, 'manual'); const triggerResponse = await this.runTrigger(
node,
nodeExecuteFunctions.getExecuteTriggerFunctions,
additionalData,
mode,
'manual',
);
if (triggerResponse === undefined) { if (triggerResponse === undefined) {
return null; return null;
@ -1027,11 +1175,9 @@ export class Workflow {
} }
return response; return response;
} else { }
// For trigger nodes in any mode except "manual" do we simply pass the data through // For trigger nodes in any mode except "manual" do we simply pass the data through
return inputData.main as INodeExecutionData[][]; return inputData.main as INodeExecutionData[][];
}
} else if (nodeType.webhook) { } else if (nodeType.webhook) {
// For webhook nodes always simply pass the data through // For webhook nodes always simply pass the data through
return inputData.main as INodeExecutionData[][]; return inputData.main as INodeExecutionData[][];

Some files were not shown because too many files have changed in this diff Show more