1
1
mirror of https://github.com/n8n-io/n8n.git synced 2024-07-07 07:18:07 +03:00

🎨 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_size = 2
[*.ts]
quote_type = single
[*.yml]
indent_style = space
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 build --if-present
npm test
npm run tslint
npm run lint
env:
CI: true

4
.gitignore vendored
View File

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

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

View File

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

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

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

View File

@ -1,32 +1,16 @@
import {
Command,
flags,
} from '@oclif/command';
/* eslint-disable @typescript-eslint/restrict-plus-operands */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable no-console */
import { Command, flags } from '@oclif/command';
import {
Credentials,
UserSettings,
} from 'n8n-core';
import { Credentials, UserSettings } from 'n8n-core';
import {
IDataObject
} from 'n8n-workflow';
import {
Db,
ICredentialsDecryptedDb,
} from '../../src';
import {
getLogger,
} from '../../src/Logger';
import {
LoggerProxy,
} from 'n8n-workflow';
import { IDataObject, LoggerProxy } from 'n8n-workflow';
import * as fs from 'fs';
import * as path from 'path';
import { getLogger } from '../../src/Logger';
import { Db, ICredentialsDecryptedDb } from '../../src';
export class ExportCredentialsCommand extends Command {
static description = 'Export credentials';
@ -45,7 +29,8 @@ export class ExportCredentialsCommand extends Command {
description: 'Export all credentials',
}),
backup: flags.boolean({
description: 'Sets --all --pretty --separate for simple backups. Only --output has to be set additionally.',
description:
'Sets --all --pretty --separate for simple backups. Only --output has to be set additionally.',
}),
id: flags.string({
description: 'The ID of the credential to export',
@ -58,19 +43,23 @@ export class ExportCredentialsCommand extends Command {
description: 'Format the output in an easier to read fashion',
}),
separate: flags.boolean({
description: 'Exports one file per credential (useful for versioning). Must inform a directory via --output.',
description:
'Exports one file per credential (useful for versioning). Must inform a directory via --output.',
}),
decrypted: flags.boolean({
description: 'Exports data decrypted / in plain text. ALL SENSITIVE INFORMATION WILL BE VISIBLE IN THE FILES. Use to migrate from a installation to another that have a different secret key (in the config file).',
description:
'Exports data decrypted / in plain text. ALL SENSITIVE INFORMATION WILL BE VISIBLE IN THE FILES. Use to migrate from a installation to another that have a different secret key (in the config file).',
}),
};
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async run() {
const logger = getLogger();
LoggerProxy.init(logger);
// eslint-disable-next-line @typescript-eslint/no-shadow
const { flags } = this.parse(ExportCredentialsCommand);
if (flags.backup) {
flags.all = true;
flags.pretty = true;
@ -103,7 +92,9 @@ export class ExportCredentialsCommand extends Command {
fs.mkdirSync(flags.output, { recursive: true });
}
} catch (e) {
console.error('Aborting execution as a filesystem error has been encountered while creating the output directory. See log messages for details.');
console.error(
'Aborting execution as a filesystem error has been encountered while creating the output directory. See log messages for details.',
);
logger.error('\nFILESYSTEM ERROR');
logger.info('====================================');
logger.error(e.message);
@ -127,6 +118,7 @@ export class ExportCredentialsCommand extends Command {
findQuery.id = flags.id;
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const credentials = await Db.collections.Credentials!.find(findQuery);
if (flags.decrypted) {
@ -148,17 +140,22 @@ export class ExportCredentialsCommand extends Command {
}
if (flags.separate) {
let fileContents: string, i: number;
let fileContents: string;
let i: number;
for (i = 0; i < credentials.length; i++) {
fileContents = JSON.stringify(credentials[i], null, flags.pretty ? 2 : undefined);
const filename = (flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) + credentials[i].id + '.json';
const filename = `${
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
(flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) +
credentials[i].id
}.json`;
fs.writeFileSync(filename, fileContents);
}
console.info(`Successfully exported ${i} credentials.`);
} else {
const fileContents = JSON.stringify(credentials, null, flags.pretty ? 2 : undefined);
if (flags.output) {
fs.writeFileSync(flags.output!, fileContents);
fs.writeFileSync(flags.output, fileContents);
console.info(`Successfully exported ${credentials.length} credentials.`);
} else {
console.info(fileContents);

View File

@ -1,26 +1,13 @@
import {
Command,
flags,
} from '@oclif/command';
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable no-console */
import { Command, flags } from '@oclif/command';
import {
IDataObject
} from 'n8n-workflow';
import {
Db,
} from '../../src';
import {
getLogger,
} from '../../src/Logger';
import {
LoggerProxy,
} from 'n8n-workflow';
import { IDataObject, LoggerProxy } from 'n8n-workflow';
import * as fs from 'fs';
import * as path from 'path';
import { getLogger } from '../../src/Logger';
import { Db } from '../../src';
export class ExportWorkflowsCommand extends Command {
static description = 'Export workflows';
@ -38,7 +25,8 @@ export class ExportWorkflowsCommand extends Command {
description: 'Export all workflows',
}),
backup: flags.boolean({
description: 'Sets --all --pretty --separate for simple backups. Only --output has to be set additionally.',
description:
'Sets --all --pretty --separate for simple backups. Only --output has to be set additionally.',
}),
id: flags.string({
description: 'The ID of the workflow to export',
@ -51,14 +39,17 @@ export class ExportWorkflowsCommand extends Command {
description: 'Format the output in an easier to read fashion',
}),
separate: flags.boolean({
description: 'Exports one file per workflow (useful for versioning). Must inform a directory via --output.',
description:
'Exports one file per workflow (useful for versioning). Must inform a directory via --output.',
}),
};
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async run() {
const logger = getLogger();
LoggerProxy.init(logger);
// eslint-disable-next-line @typescript-eslint/no-shadow
const { flags } = this.parse(ExportWorkflowsCommand);
if (flags.backup) {
@ -93,7 +84,9 @@ export class ExportWorkflowsCommand extends Command {
fs.mkdirSync(flags.output, { recursive: true });
}
} catch (e) {
console.error('Aborting execution as a filesystem error has been encountered while creating the output directory. See log messages for details.');
console.error(
'Aborting execution as a filesystem error has been encountered while creating the output directory. See log messages for details.',
);
logger.error('\nFILESYSTEM ERROR');
logger.info('====================================');
logger.error(e.message);
@ -117,6 +110,7 @@ export class ExportWorkflowsCommand extends Command {
findQuery.id = flags.id;
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const workflows = await Db.collections.Workflow!.find(findQuery);
if (workflows.length === 0) {
@ -124,18 +118,27 @@ export class ExportWorkflowsCommand extends Command {
}
if (flags.separate) {
let fileContents: string, i: number;
let fileContents: string;
let i: number;
for (i = 0; i < workflows.length; i++) {
fileContents = JSON.stringify(workflows[i], null, flags.pretty ? 2 : undefined);
const filename = (flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) + workflows[i].id + '.json';
const filename = `${
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands, @typescript-eslint/no-non-null-assertion
(flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) +
workflows[i].id
}.json`;
fs.writeFileSync(filename, fileContents);
}
console.info(`Successfully exported ${i} workflows.`);
} else {
const fileContents = JSON.stringify(workflows, null, flags.pretty ? 2 : undefined);
if (flags.output) {
fs.writeFileSync(flags.output!, fileContents);
console.info(`Successfully exported ${workflows.length} ${workflows.length === 1 ? 'workflow.' : 'workflows.'}`);
fs.writeFileSync(flags.output, fileContents);
console.info(
`Successfully exported ${workflows.length} ${
workflows.length === 1 ? 'workflow.' : 'workflows.'
}`,
);
} else {
console.info(fileContents);
}

View File

@ -1,28 +1,16 @@
import {
Command,
flags,
} from '@oclif/command';
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable no-console */
import { Command, flags } from '@oclif/command';
import {
Credentials,
UserSettings,
} from 'n8n-core';
import { Credentials, UserSettings } from 'n8n-core';
import {
Db,
} from '../../src';
import {
getLogger,
} from '../../src/Logger';
import {
LoggerProxy,
} from 'n8n-workflow';
import { LoggerProxy } from 'n8n-workflow';
import * as fs from 'fs';
import * as glob from 'fast-glob';
import * as path from 'path';
import { getLogger } from '../../src/Logger';
import { Db } from '../../src';
export class ImportCredentialsCommand extends Command {
static description = 'Import credentials';
@ -43,10 +31,12 @@ export class ImportCredentialsCommand extends Command {
}),
};
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async run() {
const logger = getLogger();
LoggerProxy.init(logger);
// eslint-disable-next-line @typescript-eslint/no-shadow
const { flags } = this.parse(ImportCredentialsCommand);
if (!flags.input) {
@ -76,18 +66,25 @@ export class ImportCredentialsCommand extends Command {
}
if (flags.separate) {
const files = await glob((flags.input.endsWith(path.sep) ? flags.input : flags.input + path.sep) + '*.json');
const files = await glob(
`${flags.input.endsWith(path.sep) ? flags.input : flags.input + path.sep}*.json`,
);
for (i = 0; i < files.length; i++) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const credential = JSON.parse(fs.readFileSync(files[i], { encoding: 'utf8' }));
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (typeof credential.data === 'object') {
// plain data / decrypted input. Should be encrypted first.
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
Credentials.prototype.setData.call(credential, credential.data, encryptionKey);
}
// eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-non-null-assertion
await Db.collections.Credentials!.save(credential);
}
} else {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const fileContents = JSON.parse(fs.readFileSync(flags.input, { encoding: 'utf8' }));
if (!Array.isArray(fileContents)) {
@ -97,8 +94,13 @@ export class ImportCredentialsCommand extends Command {
for (i = 0; i < fileContents.length; i++) {
if (typeof fileContents[i].data === 'object') {
// plain data / decrypted input. Should be encrypted first.
Credentials.prototype.setData.call(fileContents[i], fileContents[i].data, encryptionKey);
Credentials.prototype.setData.call(
fileContents[i],
fileContents[i].data,
encryptionKey,
);
}
// eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-non-null-assertion
await Db.collections.Credentials!.save(fileContents[i]);
}
}

View File

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

View File

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

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 { join as pathJoin } from 'path';
import { readFile as fsReadFile } from 'fs/promises';
import { readFileSync as fsReadFileSync } from 'fs';
import { IDataObject } from 'n8n-workflow';
import * as config from '../config';
import { IPackageVersions } from './';
// eslint-disable-next-line import/no-cycle
import { IPackageVersions } from '.';
let versionCache: IPackageVersions | undefined;
@ -16,18 +22,17 @@ let versionCache: IPackageVersions | undefined;
* @returns {string}
*/
export function getBaseUrl(): string {
const protocol = config.get('protocol') as string;
const host = config.get('host') as string;
const port = config.get('port') as number;
const path = config.get('path') as string;
const protocol = config.get('protocol');
const host = config.get('host');
const port = config.get('port');
const path = config.get('path');
if (protocol === 'http' && port === 80 || protocol === 'https' && port === 443) {
if ((protocol === 'http' && port === 80) || (protocol === 'https' && port === 443)) {
return `${protocol}://${host}${path}`;
}
return `${protocol}://${host}:${port}${path}`;
}
/**
* Returns the session id if one is set
*
@ -39,7 +44,6 @@ export function getSessionId(req: express.Request): string | undefined {
return req.headers.sessionid as string | undefined;
}
/**
* Returns information which version of the packages are installed
*
@ -51,10 +55,12 @@ export async function getVersions(): Promise<IPackageVersions> {
return versionCache;
}
const packageFile = await fsReadFile(pathJoin(__dirname, '../../package.json'), 'utf8') as string;
const packageFile = await fsReadFile(pathJoin(__dirname, '../../package.json'), 'utf8');
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const packageData = JSON.parse(packageFile);
versionCache = {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
cli: packageData.version,
};
@ -71,9 +77,11 @@ export async function getVersions(): Promise<IPackageVersions> {
function extractSchemaForKey(configKey: string, configSchema: IDataObject): IDataObject {
const configKeyParts = configKey.split('.');
// eslint-disable-next-line no-restricted-syntax
for (const key of configKeyParts) {
if (configSchema[key] === undefined) {
throw new Error(`Key "${key}" of ConfigKey "${configKey}" does not exist`);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
} else if ((configSchema[key]! as IDataObject)._cvtProperties === undefined) {
configSchema = configSchema[key] as IDataObject;
} else {
@ -90,7 +98,9 @@ function extractSchemaForKey(configKey: string, configSchema: IDataObject): IDat
* @param {string} configKey The key of the config data to get
* @returns {(Promise<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
const configSchema = config.getSchema();
// @ts-ignore
@ -102,7 +112,7 @@ export async function getConfigValue(configKey: string): Promise<string | boolea
}
// Check if special file enviroment variable exists
const fileEnvironmentVariable = process.env[currentSchema.env + '_FILE'];
const fileEnvironmentVariable = process.env[`${currentSchema.env}_FILE`];
if (fileEnvironmentVariable === undefined) {
// Does not exist, so return value from config
return config.get(configKey);
@ -110,7 +120,7 @@ export async function getConfigValue(configKey: string): Promise<string | boolea
let data;
try {
data = await fsReadFile(fileEnvironmentVariable, 'utf8') as string;
data = await fsReadFile(fileEnvironmentVariable, 'utf8');
} catch (error) {
if (error.code === 'ENOENT') {
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
const fileEnvironmentVariable = process.env[currentSchema.env + '_FILE'];
const fileEnvironmentVariable = process.env[`${currentSchema.env}_FILE`];
if (fileEnvironmentVariable === undefined) {
// Does not exist, so return value from config
return config.get(configKey);
@ -149,7 +159,7 @@ export function getConfigValueSync(configKey: string): string | boolean | number
let data;
try {
data = fsReadFileSync(fileEnvironmentVariable, 'utf8') as string;
data = fsReadFileSync(fileEnvironmentVariable, 'utf8');
} catch (error) {
if (error.code === 'ENOENT') {
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 {
ExecutionError,
ICredentialDataDecryptedObject,
@ -10,15 +12,15 @@ import {
IRunExecutionData,
ITaskData,
IWorkflowBase as IWorkflowBaseWorkflow,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
IWorkflowCredentials,
Workflow,
WorkflowExecuteMode,
} from 'n8n-workflow';
import {
IDeferredPromise, WorkflowExecute,
} from 'n8n-core';
import { IDeferredPromise, WorkflowExecute } from 'n8n-core';
// eslint-disable-next-line import/no-extraneous-dependencies
import * as PCancelable from 'p-cancelable';
import { Repository } from 'typeorm';
@ -85,7 +87,7 @@ export interface ITagDb {
}
export type UsageCount = {
usageCount: number
usageCount: number;
};
export type ITagWithCountDb = ITagDb & UsageCount;
@ -214,7 +216,6 @@ export interface IExecutionsSummary {
workflowName?: string;
}
export interface IExecutionsCurrentSummary {
id: string;
retryOf?: string;
@ -223,7 +224,6 @@ export interface IExecutionsCurrentSummary {
workflowId: string;
}
export interface IExecutionDeleteFilter {
deleteBefore?: Date;
filters?: IDataObject;
@ -240,22 +240,33 @@ export interface IExecutingWorkflowData {
export interface IExternalHooks {
credentials?: {
create?: Array<{ (this: IExternalHooksFunctions, credentialsData: ICredentialsEncrypted): Promise<void>; }>
delete?: Array<{ (this: IExternalHooksFunctions, credentialId: string): Promise<void>; }>
update?: Array<{ (this: IExternalHooksFunctions, credentialsData: ICredentialsDb): Promise<void>; }>
create?: Array<{
(this: IExternalHooksFunctions, credentialsData: ICredentialsEncrypted): Promise<void>;
}>;
delete?: Array<{ (this: IExternalHooksFunctions, credentialId: string): Promise<void> }>;
update?: Array<{
(this: IExternalHooksFunctions, credentialsData: ICredentialsDb): Promise<void>;
}>;
};
workflow?: {
activate?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb): Promise<void>; }>
create?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowBase): Promise<void>; }>
delete?: Array<{ (this: IExternalHooksFunctions, workflowId: string): Promise<void>; }>
execute?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb, mode: WorkflowExecuteMode): Promise<void>; }>
update?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb): Promise<void>; }>
activate?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb): Promise<void> }>;
create?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowBase): Promise<void> }>;
delete?: Array<{ (this: IExternalHooksFunctions, workflowId: string): Promise<void> }>;
execute?: Array<{
(
this: IExternalHooksFunctions,
workflowData: IWorkflowDb,
mode: WorkflowExecuteMode,
): Promise<void>;
}>;
update?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb): Promise<void> }>;
};
}
export interface IExternalHooksFileData {
[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 {
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 {
@ -295,12 +307,14 @@ export interface IN8nConfigEndpoints {
webhookTest: string;
}
// eslint-disable-next-line import/export
export interface IN8nConfigExecutions {
saveDataOnError: SaveExecutionDataType;
saveDataOnSuccess: SaveExecutionDataType;
saveDataManualExecutions: boolean;
}
// eslint-disable-next-line import/export
export interface IN8nConfigExecutions {
saveDataOnError: SaveExecutionDataType;
saveDataOnSuccess: SaveExecutionDataType;
@ -409,13 +423,11 @@ export interface IPushDataNodeExecuteAfter {
nodeName: string;
}
export interface IPushDataNodeExecuteBefore {
executionId: string;
nodeName: string;
}
export interface IPushDataTestWebhook {
executionId: string;
workflowId: string;
@ -432,7 +444,6 @@ export interface IResponseCallbackData {
responseCode?: number;
}
export interface ITransferNodeTypes {
[key: string]: {
className: string;
@ -440,7 +451,6 @@ export interface ITransferNodeTypes {
};
}
export interface IWorkflowErrorData {
[key: string]: IDataObject | string | number | ExecutionError;
execution: {
@ -457,7 +467,8 @@ export interface IWorkflowErrorData {
export interface IProcessMessageDataHook {
hook: string;
parameters: any[]; // tslint:disable-line:no-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
parameters: any[];
}
export interface IWorkflowExecutionDataProcess {
@ -471,7 +482,6 @@ export interface IWorkflowExecutionDataProcess {
workflowData: IWorkflowBase;
}
export interface IWorkflowExecutionDataProcessWithExecution extends IWorkflowExecutionDataProcess {
credentialsOverwrite: ICredentialsOverwrite;
credentialsTypeData: ICredentialsTypeData;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +1,15 @@
import * as Bull from 'bull';
import * as config from '../config';
// eslint-disable-next-line import/no-cycle
import { IBullJobData } from './Interfaces';
export class Queue {
private jobQueue: Bull.Queue;
constructor() {
const prefix = config.get('queue.bull.prefix') as string;
const redisOptions = config.get('queue.bull.redis') as object;
// Disabling ready check is necessary as it allows worker to
// Disabling ready check is necessary as it allows worker to
// quickly reconnect to Redis if Redis crashes or is unreachable
// for some time. With it enabled, worker might take minutes to realize
// redis is back up and resume working.
@ -16,25 +17,25 @@ export class Queue {
// @ts-ignore
this.jobQueue = new Bull('jobs', { prefix, redis: redisOptions, enableReadyCheck: false });
}
async add(jobData: IBullJobData, jobOptions: object): Promise<Bull.Job> {
return await this.jobQueue.add(jobData,jobOptions);
return this.jobQueue.add(jobData, jobOptions);
}
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[]> {
return await this.jobQueue.getJobs(jobTypes);
return this.jobQueue.getJobs(jobTypes);
}
getBullObjectInstance(): Bull.Queue {
return this.jobQueue;
}
/**
*
*
* @param job A Bull.Job instance
* @returns boolean true if we were able to securely stop the job
*/
@ -43,15 +44,15 @@ export class Queue {
// Job is already running so tell it to stop
await job.progress(-1);
return true;
} else {
// Job did not get started yet so remove from queue
try {
await job.remove();
return true;
} catch (e) {
await job.progress(-1);
}
}
// Job did not get started yet so remove from queue
try {
await job.remove();
return true;
} catch (e) {
await job.progress(-1);
}
return false;
}
}
@ -62,6 +63,6 @@ export function getInstance(): Queue {
if (activeQueueInstance === undefined) {
activeQueueInstance = new Queue();
}
return activeQueueInstance;
}

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 { parse, stringify } from 'flatted';
// eslint-disable-next-line import/no-cycle
import {
IExecutionDb,
IExecutionFlatted,
IExecutionFlattedDb,
IExecutionResponse,
IWorkflowDb,
} from './';
} from '.';
/**
* Special Error which allows to return also an error code and http status code
@ -17,7 +23,6 @@ import {
* @extends {Error}
*/
export class ResponseError extends Error {
// The HTTP status code of response
httpStatusCode?: number;
@ -35,7 +40,7 @@ export class ResponseError extends Error {
* @param {string} [hint] The error hint to provide a context (webhook related)
* @memberof ResponseError
*/
constructor(message: string, errorCode?: number, httpStatusCode?: number, hint?:string) {
constructor(message: string, errorCode?: number, httpStatusCode?: number, hint?: string) {
super(message);
this.name = 'ResponseError';
@ -51,21 +56,23 @@ export class ResponseError extends Error {
}
}
export function basicAuthAuthorizationError(resp: Response, realm: string, message?: string) {
resp.statusCode = 401;
resp.setHeader('WWW-Authenticate', `Basic realm="${realm}"`);
resp.json({code: resp.statusCode, message});
resp.json({ code: resp.statusCode, message });
}
export function jwtAuthAuthorizationError(resp: Response, message?: string) {
resp.statusCode = 403;
resp.json({code: resp.statusCode, message});
resp.json({ code: resp.statusCode, message });
}
export function sendSuccessResponse(res: Response, data: any, raw?: boolean, responseCode?: number) { // tslint:disable-line:no-any
export function sendSuccessResponse(
res: Response,
data: any,
raw?: boolean,
responseCode?: number,
) {
if (responseCode !== undefined) {
res.status(responseCode);
}
@ -83,7 +90,6 @@ export function sendSuccessResponse(res: Response, data: any, raw?: boolean, res
}
}
export function sendErrorResponse(res: Response, error: ResponseError) {
let httpStatusCode = 500;
if (error.httpStatusCode) {
@ -122,7 +128,6 @@ export function sendErrorResponse(res: Response, error: ResponseError) {
res.status(httpStatusCode).json(response);
}
/**
* A helper function which does not just allow to return Promises it also makes sure that
* all the responses have the same format
@ -133,8 +138,7 @@ export function sendErrorResponse(res: Response, error: ResponseError) {
* @returns
*/
export function send(processFunction: (req: Request, res: Response) => Promise<any>) { // tslint:disable-line:no-any
export function send(processFunction: (req: Request, res: Response) => Promise<any>) {
return async (req: Request, res: Response) => {
try {
const data = await processFunction(req, res);
@ -148,7 +152,6 @@ export function send(processFunction: (req: Request, res: Response) => Promise<a
};
}
/**
* Flattens the Execution 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 {
// Flatten the data
const returnData: IExecutionFlatted = Object.assign({}, {
const returnData: IExecutionFlatted = {
data: stringify(fullExecutionData.data),
mode: fullExecutionData.mode,
// @ts-ignore
waitTill: fullExecutionData.waitTill,
startedAt: fullExecutionData.startedAt,
stoppedAt: fullExecutionData.stoppedAt,
finished: fullExecutionData.finished ? fullExecutionData.finished : false,
workflowId: fullExecutionData.workflowId,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
workflowData: fullExecutionData.workflowData!,
});
};
if (fullExecutionData.id !== undefined) {
returnData.id = fullExecutionData.id!.toString();
returnData.id = fullExecutionData.id.toString();
}
if (fullExecutionData.retryOf !== undefined) {
returnData.retryOf = fullExecutionData.retryOf!.toString();
returnData.retryOf = fullExecutionData.retryOf.toString();
}
if (fullExecutionData.retrySuccessId !== undefined) {
returnData.retrySuccessId = fullExecutionData.retrySuccessId!.toString();
returnData.retrySuccessId = fullExecutionData.retrySuccessId.toString();
}
return returnData;
}
/**
* Unflattens the Execution data.
*
@ -195,8 +199,7 @@ export function flattenExecutionData(fullExecutionData: IExecutionDb): IExecutio
* @returns {IExecutionResponse}
*/
export function unflattenExecutionData(fullExecutionData: IExecutionFlattedDb): IExecutionResponse {
const returnData: IExecutionResponse = Object.assign({}, {
const returnData: IExecutionResponse = {
id: fullExecutionData.id.toString(),
workflowData: fullExecutionData.workflowData as IWorkflowDb,
data: parse(fullExecutionData.data),
@ -206,7 +209,7 @@ export function unflattenExecutionData(fullExecutionData: IExecutionFlattedDb):
stoppedAt: fullExecutionData.stoppedAt,
finished: fullExecutionData.finished ? fullExecutionData.finished : false,
workflowId: fullExecutionData.workflowId,
});
};
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 {
ResponseHelper,
} from ".";
import { ResponseHelper } from '.';
import {
TagEntity,
} from "./databases/entities/TagEntity";
import {
ITagWithCountDb,
} from "./Interfaces";
import { TagEntity } from './databases/entities/TagEntity';
import { ITagWithCountDb } from './Interfaces';
// ----------------------------------
// utils
@ -29,7 +25,7 @@ export function sortByRequestOrder(tagsDb: TagEntity[], tagIds: string[]) {
return acc;
}, {} as { [key: string]: TagEntity });
return tagIds.map(tagId => tagMap[tagId]);
return tagIds.map((tagId) => tagMap[tagId]);
}
// ----------------------------------
@ -43,6 +39,7 @@ export async function validateTag(newTag: TagEntity) {
const errors = await validate(newTag);
if (errors.length) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const validationErrorMessage = Object.values(errors[0].constraints!)[0];
throw new ResponseHelper.ResponseError(validationErrorMessage, undefined, 400);
}
@ -64,23 +61,30 @@ export function throwDuplicateEntryError(error: Error) {
/**
* Retrieve all tags and the number of workflows each tag is related to.
*/
export function getTagsWithCountDb(tablePrefix: string): Promise<ITagWithCountDb[]> {
export async function getTagsWithCountDb(tablePrefix: string): Promise<ITagWithCountDb[]> {
return getConnection()
.createQueryBuilder()
.select(`${tablePrefix}tag_entity.id`, 'id')
.addSelect(`${tablePrefix}tag_entity.name`, 'name')
.addSelect(`COUNT(${tablePrefix}workflows_tags.workflowId)`, 'usageCount')
.from(`${tablePrefix}tag_entity`, 'tag_entity')
.leftJoin(`${tablePrefix}workflows_tags`, 'workflows_tags', `${tablePrefix}workflows_tags.tagId = tag_entity.id`)
.groupBy(`${tablePrefix}tag_entity.id`)
.getRawMany()
.then(tagsWithCount => {
tagsWithCount.forEach(tag => {
tag.id = tag.id.toString();
tag.usageCount = Number(tag.usageCount);
.createQueryBuilder()
.select(`${tablePrefix}tag_entity.id`, 'id')
.addSelect(`${tablePrefix}tag_entity.name`, 'name')
.addSelect(`COUNT(${tablePrefix}workflows_tags.workflowId)`, 'usageCount')
.from(`${tablePrefix}tag_entity`, 'tag_entity')
.leftJoin(
`${tablePrefix}workflows_tags`,
'workflows_tags',
`${tablePrefix}workflows_tags.tagId = tag_entity.id`,
)
.groupBy(`${tablePrefix}tag_entity.id`)
.getRawMany()
.then((tagsWithCount) => {
tagsWithCount.forEach((tag) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
tag.id = tag.id.toString();
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
tag.usageCount = Number(tag.usageCount);
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return tagsWithCount;
});
return tagsWithCount;
});
}
// ----------------------------------
@ -90,19 +94,19 @@ export function getTagsWithCountDb(tablePrefix: string): Promise<ITagWithCountDb
/**
* 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()
.createQueryBuilder()
.insert()
.into(`${tablePrefix}workflows_tags`)
.values(tagIds.map(tagId => ({ workflowId, tagId })))
.values(tagIds.map((tagId) => ({ workflowId, tagId })))
.execute();
}
/**
* Remove all tags for a workflow during a tag update operation.
*/
export function removeRelations(workflowId: string, tablePrefix: string) {
export async function removeRelations(workflowId: string, tablePrefix: string) {
return getConnection()
.createQueryBuilder()
.delete()

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

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 {
ActiveExecutions,
DatabaseType,
@ -7,38 +20,23 @@ import {
IExecutionsStopData,
IWorkflowExecutionDataProcess,
ResponseHelper,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
WorkflowCredentials,
WorkflowRunner,
} from '.';
import {
IRun,
LoggerProxy as Logger,
WorkflowOperationError,
} from 'n8n-workflow';
import {
FindManyOptions,
LessThanOrEqual,
ObjectLiteral,
} from 'typeorm';
import { DateUtils } from 'typeorm/util/DateUtils';
export class WaitTrackerClass {
activeExecutionsInstance: ActiveExecutions.ActiveExecutions;
private waitingExecutions: {
[key: string]: {
executionId: string,
timer: NodeJS.Timeout,
executionId: string;
timer: NodeJS.Timeout;
};
} = {};
mainTimer: NodeJS.Timeout;
constructor() {
this.activeExecutionsInstance = ActiveExecutions.getInstance();
@ -50,7 +48,7 @@ export class WaitTrackerClass {
this.getwaitingExecutions();
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async getwaitingExecutions() {
Logger.debug('Wait tracker querying database for waiting executions');
// Find all the executions which should be triggered in the next 70 seconds
@ -63,11 +61,13 @@ export class WaitTrackerClass {
waitTill: 'ASC',
},
};
const dbType = await GenericHelpers.getConfigValue('database.type') as DatabaseType;
const dbType = (await GenericHelpers.getConfigValue('database.type')) as DatabaseType;
if (dbType === 'sqlite') {
// This is needed because of issue in TypeORM <> SQLite:
// https://github.com/typeorm/typeorm/issues/2286
(findQuery.where! as ObjectLiteral).waitTill = LessThanOrEqual(DateUtils.mixedDateToUtcDatetimeString(new Date(Date.now() + 70000)));
(findQuery.where! as ObjectLiteral).waitTill = LessThanOrEqual(
DateUtils.mixedDateToUtcDatetimeString(new Date(Date.now() + 70000)),
);
}
const executions = await Db.collections.Execution!.find(findQuery);
@ -76,10 +76,13 @@ export class WaitTrackerClass {
return;
}
const executionIds = executions.map(execution => execution.id.toString()).join(', ');
Logger.debug(`Wait tracker found ${executions.length} executions. Setting timer for IDs: ${executionIds}`);
const executionIds = executions.map((execution) => execution.id.toString()).join(', ');
Logger.debug(
`Wait tracker found ${executions.length} executions. Setting timer for IDs: ${executionIds}`,
);
// Add timers for each waiting execution that they get started at the correct time
// eslint-disable-next-line no-restricted-syntax
for (const execution of executions) {
const executionId = execution.id.toString();
if (this.waitingExecutions[executionId] === undefined) {
@ -94,7 +97,6 @@ export class WaitTrackerClass {
}
}
async stopExecution(executionId: string): Promise<IExecutionsStopData> {
if (this.waitingExecutions[executionId] !== undefined) {
// The waiting execution was already sheduled to execute.
@ -124,7 +126,10 @@ export class WaitTrackerClass {
fullExecutionData.stoppedAt = new Date();
fullExecutionData.waitTill = undefined;
await Db.collections.Execution!.update(executionId, ResponseHelper.flattenExecutionData(fullExecutionData));
await Db.collections.Execution!.update(
executionId,
ResponseHelper.flattenExecutionData(fullExecutionData),
);
return {
mode: fullExecutionData.mode,
@ -134,9 +139,8 @@ export class WaitTrackerClass {
};
}
startExecution(executionId: string) {
Logger.debug(`Wait tracker resuming execution ${executionId}`, {executionId});
Logger.debug(`Wait tracker resuming execution ${executionId}`, { executionId });
delete this.waitingExecutions[executionId];
(async () => {
@ -149,7 +153,7 @@ export class WaitTrackerClass {
const fullExecutionData = ResponseHelper.unflattenExecutionData(fullExecutionDataFlatted);
if (fullExecutionData.finished === true) {
if (fullExecutionData.finished) {
throw new Error('The execution did succeed and can so not be started again.');
}
@ -163,13 +167,14 @@ export class WaitTrackerClass {
const workflowRunner = new WorkflowRunner();
await workflowRunner.run(data, false, false, executionId);
})().catch((error) => {
Logger.error(`There was a problem starting the waiting execution with id "${executionId}": "${error.message}"`, { executionId });
Logger.error(
`There was a problem starting the waiting execution with id "${executionId}": "${error.message}"`,
{ executionId },
);
});
}
}
let waitTrackerInstance: WaitTrackerClass | undefined;
export function WaitTracker(): WaitTrackerClass {

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 {
Db,
IExecutionResponse,
@ -6,26 +22,18 @@ import {
NodeTypes,
ResponseHelper,
WebhookHelpers,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
WorkflowCredentials,
WorkflowExecuteAdditionalData,
} from '.';
import {
INode,
IRunExecutionData,
NodeHelpers,
WebhookHttpMethod,
Workflow,
} from 'n8n-workflow';
import * as express from 'express';
import {
LoggerProxy as Logger,
} from 'n8n-workflow';
export class WaitingWebhooks {
async executeWebhook(httpMethod: WebhookHttpMethod, fullPath: string, req: express.Request, res: express.Response): Promise<IResponseCallbackData> {
async executeWebhook(
httpMethod: WebhookHttpMethod,
fullPath: string,
req: express.Request,
res: express.Response,
): Promise<IResponseCallbackData> {
Logger.debug(`Received waiting-webhoook "${httpMethod}" for path "${fullPath}"`);
// Reset request parameters
@ -44,47 +52,77 @@ export class WaitingWebhooks {
const execution = await Db.collections.Execution?.findOne(executionId);
if (execution === undefined) {
throw new ResponseHelper.ResponseError(`The execution "${executionId} does not exist.`, 404, 404);
throw new ResponseHelper.ResponseError(
`The execution "${executionId} does not exist.`,
404,
404,
);
}
const fullExecutionData = ResponseHelper.unflattenExecutionData(execution);
if (fullExecutionData.finished === true || fullExecutionData.data.resultData.error) {
throw new ResponseHelper.ResponseError(`The execution "${executionId} has finished already.`, 409, 409);
if (fullExecutionData.finished || fullExecutionData.data.resultData.error) {
throw new ResponseHelper.ResponseError(
`The execution "${executionId} has finished already.`,
409,
409,
);
}
return this.startExecution(httpMethod, path, fullExecutionData, req, res);
}
async startExecution(httpMethod: WebhookHttpMethod, path: string, fullExecutionData: IExecutionResponse, req: express.Request, res: express.Response): Promise<IResponseCallbackData> {
async startExecution(
httpMethod: WebhookHttpMethod,
path: string,
fullExecutionData: IExecutionResponse,
req: express.Request,
res: express.Response,
): Promise<IResponseCallbackData> {
const executionId = fullExecutionData.id;
if (fullExecutionData.finished === true) {
if (fullExecutionData.finished) {
throw new Error('The execution did succeed and can so not be started again.');
}
const lastNodeExecuted = fullExecutionData!.data.resultData.lastNodeExecuted as string;
const lastNodeExecuted = fullExecutionData.data.resultData.lastNodeExecuted as string;
// Set the node as disabled so that the data does not get executed again as it would result
// in starting the wait all over again
fullExecutionData!.data.executionData!.nodeExecutionStack[0].node.disabled = true;
fullExecutionData.data.executionData!.nodeExecutionStack[0].node.disabled = true;
// Remove waitTill information else the execution would stop
fullExecutionData!.data.waitTill = undefined;
fullExecutionData.data.waitTill = undefined;
// Remove the data of the node execution again else it will display the node as executed twice
fullExecutionData!.data.resultData.runData[lastNodeExecuted].pop();
fullExecutionData.data.resultData.runData[lastNodeExecuted].pop();
const workflowData = fullExecutionData.workflowData;
const { workflowData } = fullExecutionData;
const nodeTypes = NodeTypes();
const workflow = new Workflow({ id: workflowData.id!.toString(), name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, nodeTypes, staticData: workflowData.staticData, settings: workflowData.settings });
const workflow = new Workflow({
id: workflowData.id!.toString(),
name: workflowData.name,
nodes: workflowData.nodes,
connections: workflowData.connections,
active: workflowData.active,
nodeTypes,
staticData: workflowData.staticData,
settings: workflowData.settings,
});
const additionalData = await WorkflowExecuteAdditionalData.getBase();
const webhookData = NodeHelpers.getNodeWebhooks(workflow, workflow.getNode(lastNodeExecuted) as INode, additionalData).filter((webhook) => {
return (webhook.httpMethod === httpMethod && webhook.path === path && webhook.webhookDescription.restartWebhook === true);
const webhookData = NodeHelpers.getNodeWebhooks(
workflow,
workflow.getNode(lastNodeExecuted) as INode,
additionalData,
).filter((webhook) => {
return (
webhook.httpMethod === httpMethod &&
webhook.path === path &&
webhook.webhookDescription.restartWebhook === true
);
})[0];
if (webhookData === undefined) {
@ -100,18 +138,30 @@ export class WaitingWebhooks {
throw new ResponseHelper.ResponseError('Could not find node to process webhook.', 404, 404);
}
const runExecutionData = fullExecutionData.data as IRunExecutionData;
const runExecutionData = fullExecutionData.data;
return new Promise((resolve, reject) => {
const executionMode = 'webhook';
WebhookHelpers.executeWebhook(workflow, webhookData, workflowData as IWorkflowDb, workflowStartNode, executionMode, undefined, runExecutionData, fullExecutionData.id, req, res, (error: Error | null, data: object) => {
if (error !== null) {
return reject(error);
}
resolve(data);
});
// eslint-disable-next-line @typescript-eslint/no-floating-promises
WebhookHelpers.executeWebhook(
workflow,
webhookData,
workflowData as IWorkflowDb,
workflowStartNode,
executionMode,
undefined,
runExecutionData,
fullExecutionData.id,
req,
res,
// eslint-disable-next-line consistent-return
(error: Error | null, data: object) => {
if (error !== null) {
return reject(error);
}
resolve(data);
},
);
});
}
}

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

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 {
readFileSync,
} from 'fs';
import {
getConnectionManager,
} from 'typeorm';
import { readFileSync } from 'fs';
import { getConnectionManager } from 'typeorm';
import * as bodyParser from 'body-parser';
require('body-parser-xml')(bodyParser);
// eslint-disable-next-line import/no-extraneous-dependencies, @typescript-eslint/no-unused-vars
import * as _ from 'lodash';
import * as compression from 'compression';
// eslint-disable-next-line import/no-extraneous-dependencies
import * as parseUrl from 'parseurl';
// eslint-disable-next-line import/no-cycle
import {
ActiveExecutions,
ActiveWorkflowRunner,
@ -19,120 +27,157 @@ import {
IExternalHooksClass,
IPackageVersions,
ResponseHelper,
} from './';
} from '.';
import * as compression from 'compression';
import * as config from '../config';
import * as parseUrl from 'parseurl';
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-call
require('body-parser-xml')(bodyParser);
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function registerProductionWebhooks() {
// ----------------------------------------
// Regular Webhooks
// ----------------------------------------
// HEAD webhook requests
this.app.head(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhook.length + 2);
this.app.head(
`/${this.endpointWebhook}/*`,
async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
this.endpointWebhook.length + 2,
);
let response;
try {
response = await this.activeWorkflowRunner.executeWebhook('HEAD', requestUrl, req, res);
} catch (error) {
ResponseHelper.sendErrorResponse(res, error);
return;
}
let response;
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
response = await this.activeWorkflowRunner.executeWebhook('HEAD', requestUrl, req, res);
} catch (error) {
ResponseHelper.sendErrorResponse(res, error);
return;
}
if (response.noWebhookResponse === true) {
// Nothing else to do as the response got already sent
return;
}
if (response.noWebhookResponse === true) {
// Nothing else to do as the response got already sent
return;
}
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
});
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
},
);
// OPTIONS webhook requests
this.app.options(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhook.length + 2);
this.app.options(
`/${this.endpointWebhook}/*`,
async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
this.endpointWebhook.length + 2,
);
let allowedMethods: string[];
try {
allowedMethods = await this.activeWorkflowRunner.getWebhookMethods(requestUrl);
allowedMethods.push('OPTIONS');
let allowedMethods: string[];
try {
allowedMethods = await this.activeWorkflowRunner.getWebhookMethods(requestUrl);
allowedMethods.push('OPTIONS');
// Add custom "Allow" header to satisfy OPTIONS response.
res.append('Allow', allowedMethods);
} catch (error) {
ResponseHelper.sendErrorResponse(res, error);
return;
}
// Add custom "Allow" header to satisfy OPTIONS response.
res.append('Allow', allowedMethods);
} catch (error) {
ResponseHelper.sendErrorResponse(res, error);
return;
}
ResponseHelper.sendSuccessResponse(res, {}, true, 204);
});
ResponseHelper.sendSuccessResponse(res, {}, true, 204);
},
);
// GET webhook requests
this.app.get(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhook.length + 2);
this.app.get(
`/${this.endpointWebhook}/*`,
async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
this.endpointWebhook.length + 2,
);
let response;
try {
response = await this.activeWorkflowRunner.executeWebhook('GET', requestUrl, req, res);
} catch (error) {
ResponseHelper.sendErrorResponse(res, error);
return;
}
let response;
try {
response = await this.activeWorkflowRunner.executeWebhook('GET', requestUrl, req, res);
} catch (error) {
ResponseHelper.sendErrorResponse(res, error);
return;
}
if (response.noWebhookResponse === true) {
// Nothing else to do as the response got already sent
return;
}
if (response.noWebhookResponse === true) {
// Nothing else to do as the response got already sent
return;
}
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
});
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
},
);
// POST webhook requests
this.app.post(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhook.length + 2);
this.app.post(
`/${this.endpointWebhook}/*`,
async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
this.endpointWebhook.length + 2,
);
let response;
try {
response = await this.activeWorkflowRunner.executeWebhook('POST', requestUrl, req, res);
} catch (error) {
ResponseHelper.sendErrorResponse(res, error);
return;
}
let response;
try {
response = await this.activeWorkflowRunner.executeWebhook('POST', requestUrl, req, res);
} catch (error) {
ResponseHelper.sendErrorResponse(res, error);
return;
}
if (response.noWebhookResponse === true) {
// Nothing else to do as the response got already sent
return;
}
if (response.noWebhookResponse === true) {
// Nothing else to do as the response got already sent
return;
}
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
});
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
},
);
}
class App {
app: express.Application;
activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner;
endpointWebhook: string;
endpointPresetCredentials: string;
externalHooks: IExternalHooksClass;
saveDataErrorExecution: string;
saveDataSuccessExecution: string;
saveManualExecutions: boolean;
executionTimeout: number;
maxExecutionTimeout: number;
timezone: string;
activeExecutionsInstance: ActiveExecutions.ActiveExecutions;
versions: IPackageVersions | undefined;
restEndpoint: string;
protocol: string;
sslKey: string;
sslCert: string;
presetCredentialsLoaded: boolean;
@ -163,7 +208,6 @@ class App {
this.endpointPresetCredentials = config.get('credentials.overwrite.endpoint') as string;
}
/**
* Returns the current epoch time
*
@ -174,9 +218,7 @@ class App {
return new Date();
}
async config(): Promise<void> {
this.versions = await GenericHelpers.getVersions();
// Compress the response data
@ -191,49 +233,63 @@ class App {
});
// Support application/json type post data
this.app.use(bodyParser.json({
limit: '16mb', verify: (req, res, buf) => {
// @ts-ignore
req.rawBody = buf;
},
}));
this.app.use(
bodyParser.json({
limit: '16mb',
verify: (req, res, buf) => {
// @ts-ignore
req.rawBody = buf;
},
}),
);
// Support application/xml type post data
// @ts-ignore
this.app.use(bodyParser.xml({
limit: '16mb', xmlParseOptions: {
normalize: true, // Trim whitespace inside text nodes
normalizeTags: true, // Transform tags to lowercase
explicitArray: false, // Only put properties in array if length > 1
},
}));
this.app.use(
// @ts-ignore
bodyParser.xml({
limit: '16mb',
xmlParseOptions: {
normalize: true, // Trim whitespace inside text nodes
normalizeTags: true, // Transform tags to lowercase
explicitArray: false, // Only put properties in array if length > 1
},
}),
);
this.app.use(bodyParser.text({
limit: '16mb', verify: (req, res, buf) => {
// @ts-ignore
req.rawBody = buf;
},
}));
this.app.use(
bodyParser.text({
limit: '16mb',
verify: (req, res, buf) => {
// @ts-ignore
req.rawBody = buf;
},
}),
);
//support application/x-www-form-urlencoded post data
this.app.use(bodyParser.urlencoded({ extended: false,
verify: (req, res, buf) => {
// @ts-ignore
req.rawBody = buf;
},
}));
// support application/x-www-form-urlencoded post data
this.app.use(
bodyParser.urlencoded({
extended: false,
verify: (req, res, buf) => {
// @ts-ignore
req.rawBody = buf;
},
}),
);
if (process.env['NODE_ENV'] !== 'production') {
if (process.env.NODE_ENV !== 'production') {
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
// Allow access also from frontend when developing
res.header('Access-Control-Allow-Origin', 'http://localhost:8080');
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, sessionid');
res.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept, sessionid',
);
next();
});
}
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
if (Db.collections.Workflow === null) {
const error = new ResponseHelper.ResponseError('Database is not ready!', undefined, 503);
@ -243,25 +299,22 @@ class App {
next();
});
// ----------------------------------------
// Healthcheck
// ----------------------------------------
// Does very basic health check
this.app.get('/healthz', async (req: express.Request, res: express.Response) => {
const connection = getConnectionManager().get();
try {
if (connection.isConnected === false) {
if (!connection.isConnected) {
// Connection is not active
throw new Error('No active database connection!');
}
// DB ping
await connection.query('SELECT 1');
// eslint-disable-next-line id-denylist
} catch (err) {
const error = new ResponseHelper.ResponseError('No Database connection!', undefined, 503);
return ResponseHelper.sendErrorResponse(res, error);
@ -276,9 +329,7 @@ class App {
});
registerProductionWebhooks.apply(this);
}
}
export async function start(): Promise<void> {
@ -292,12 +343,14 @@ export async function start(): Promise<void> {
let server;
if (app.protocol === 'https' && app.sslKey && app.sslCert) {
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
const https = require('https');
const privateKey = readFileSync(app.sslKey, 'utf8');
const cert = readFileSync(app.sslCert, 'utf8');
const credentials = { key: privateKey, cert };
server = https.createServer(credentials, app.app);
} else {
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
const http = require('http');
server = http.createServer(app.app);
}

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,10 +6,8 @@ import {
WorkflowExecuteMode,
} from 'n8n-workflow';
import {
NodeExecuteFunctions,
} from './';
// eslint-disable-next-line import/no-cycle
import { NodeExecuteFunctions } from '.';
export class ActiveWebhooks {
private workflowWebhooks: {
@ -22,7 +20,6 @@ export class ActiveWebhooks {
testWebhooks = false;
/**
* Adds a new webhook
*
@ -31,19 +28,31 @@ export class ActiveWebhooks {
* @returns {Promise<void>}
* @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) {
throw new Error('Webhooks can only be added for saved workflows as an id is needed!');
}
if (webhookData.path.endsWith('/')) {
// eslint-disable-next-line no-param-reassign
webhookData.path = webhookData.path.slice(0, -1);
}
const webhookKey = this.getWebhookKey(webhookData.httpMethod, webhookData.path, webhookData.webhookId);
const webhookKey = this.getWebhookKey(
webhookData.httpMethod,
webhookData.path,
webhookData.webhookId,
);
//check that there is not a webhook already registed with that path/method
// check that there is not a webhook already registed with that path/method
if (this.webhookUrls[webhookKey] && !webhookData.webhookId) {
throw new Error(`Test-Webhook can not be activated because another one with the same method "${webhookData.httpMethod}" and path "${webhookData.path}" is already active!`);
throw new Error(
`Test-Webhook can not be activated because another one with the same method "${webhookData.httpMethod}" and path "${webhookData.path}" is already active!`,
);
}
if (this.workflowWebhooks[webhookData.workflowId] === undefined) {
@ -58,18 +67,33 @@ export class ActiveWebhooks {
this.webhookUrls[webhookKey].push(webhookData);
try {
const webhookExists = await workflow.runWebhookMethod('checkExists', webhookData, NodeExecuteFunctions, mode, activation, this.testWebhooks);
const webhookExists = await workflow.runWebhookMethod(
'checkExists',
webhookData,
NodeExecuteFunctions,
mode,
activation,
this.testWebhooks,
);
if (webhookExists !== true) {
// If webhook does not exist yet create it
await workflow.runWebhookMethod('create', webhookData, NodeExecuteFunctions, mode, activation, this.testWebhooks);
await workflow.runWebhookMethod(
'create',
webhookData,
NodeExecuteFunctions,
mode,
activation,
this.testWebhooks,
);
}
} catch (error) {
// If there was a problem unregister the webhook again
if (this.webhookUrls[webhookKey].length <= 1) {
delete this.webhookUrls[webhookKey];
} else {
this.webhookUrls[webhookKey] = this.webhookUrls[webhookKey].filter(webhook => webhook.path !== webhookData.path);
this.webhookUrls[webhookKey] = this.webhookUrls[webhookKey].filter(
(webhook) => webhook.path !== webhookData.path,
);
}
throw error;
@ -77,7 +101,6 @@ export class ActiveWebhooks {
this.workflowWebhooks[webhookData.workflowId].push(webhookData);
}
/**
* Returns webhookData if a webhook with matches is currently registered
*
@ -98,9 +121,9 @@ export class ActiveWebhooks {
const pathElementsSet = new Set(path.split('/'));
// check if static elements match in path
// if more results have been returned choose the one with the most static-route matches
this.webhookUrls[webhookKey].forEach(dynamicWebhook => {
const staticElements = dynamicWebhook.path.split('/').filter(ele => !ele.startsWith(':'));
const allStaticExist = staticElements.every(staticEle => pathElementsSet.has(staticEle));
this.webhookUrls[webhookKey].forEach((dynamicWebhook) => {
const staticElements = dynamicWebhook.path.split('/').filter((ele) => !ele.startsWith(':'));
const allStaticExist = staticElements.every((staticEle) => pathElementsSet.has(staticEle));
if (allStaticExist && staticElements.length > maxMatches) {
maxMatches = staticElements.length;
@ -120,13 +143,14 @@ export class ActiveWebhooks {
* @param path
*/
getWebhookMethods(path: string): string[] {
const methods : string[] = [];
const methods: string[] = [];
Object.keys(this.webhookUrls)
.filter(key => key.includes(path))
.map(key => {
methods.push(key.split('|')[0]);
});
.filter((key) => key.includes(path))
// eslint-disable-next-line array-callback-return
.map((key) => {
methods.push(key.split('|')[0]);
});
return methods;
}
@ -141,7 +165,6 @@ export class ActiveWebhooks {
return Object.keys(this.workflowWebhooks);
}
/**
* Returns key to uniquely identify a webhook
*
@ -155,6 +178,7 @@ export class ActiveWebhooks {
if (webhookId) {
if (path.startsWith(webhookId)) {
const cutFromIndex = path.indexOf('/') + 1;
// eslint-disable-next-line no-param-reassign
path = path.slice(cutFromIndex);
}
return `${httpMethod}|${webhookId}|${path.split('/').length}`;
@ -162,7 +186,6 @@ export class ActiveWebhooks {
return `${httpMethod}|${path}`;
}
/**
* Removes all webhooks of a workflow
*
@ -171,6 +194,7 @@ export class ActiveWebhooks {
* @memberof ActiveWebhooks
*/
async removeWorkflow(workflow: Workflow): Promise<boolean> {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const workflowId = workflow.id!.toString();
if (this.workflowWebhooks[workflowId] === undefined) {
@ -183,10 +207,21 @@ export class ActiveWebhooks {
const mode = 'internal';
// Go through all the registered webhooks of the workflow and remove them
// eslint-disable-next-line no-restricted-syntax
for (const webhookData of webhooks) {
await workflow.runWebhookMethod('delete', webhookData, NodeExecuteFunctions, mode, 'update', this.testWebhooks);
// eslint-disable-next-line no-await-in-loop
await workflow.runWebhookMethod(
'delete',
webhookData,
NodeExecuteFunctions,
mode,
'update',
this.testWebhooks,
);
delete this.webhookUrls[this.getWebhookKey(webhookData.httpMethod, webhookData.path, webhookData.webhookId)];
delete this.webhookUrls[
this.getWebhookKey(webhookData.httpMethod, webhookData.path, webhookData.webhookId)
];
}
// Remove also the workflow-webhook entry
@ -195,18 +230,16 @@ export class ActiveWebhooks {
return true;
}
/**
* Removes all the webhooks of the given workflows
*/
async removeAll(workflows: Workflow[]): Promise<void> {
const removePromises = [];
// eslint-disable-next-line no-restricted-syntax
for (const workflow of workflows) {
removePromises.push(this.removeWorkflow(workflow));
}
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 {
@ -13,18 +16,14 @@ import {
WorkflowExecuteMode,
} from 'n8n-workflow';
import {
ITriggerTime,
IWorkflowData,
} from './';
// eslint-disable-next-line import/no-cycle
import { ITriggerTime, IWorkflowData } from '.';
export class ActiveWorkflows {
private workflowData: {
[key: string]: IWorkflowData;
} = {};
/**
* Returns if the workflow is active
*
@ -33,10 +32,10 @@ export class ActiveWorkflows {
* @memberof ActiveWorkflows
*/
isActive(id: string): boolean {
// eslint-disable-next-line no-prototype-builtins
return this.workflowData.hasOwnProperty(id);
}
/**
* Returns the ids of the currently active workflows
*
@ -47,7 +46,6 @@ export class ActiveWorkflows {
return Object.keys(this.workflowData);
}
/**
* Returns the Workflow data for the workflow with
* the given id if it is currently active
@ -60,7 +58,6 @@ export class ActiveWorkflows {
return this.workflowData[id];
}
/**
* Makes a workflow active
*
@ -70,16 +67,31 @@ export class ActiveWorkflows {
* @returns {Promise<void>}
* @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] = {};
const triggerNodes = workflow.getTriggerNodes();
let triggerResponse: ITriggerResponse | undefined;
this.workflowData[id].triggerResponses = [];
for (const triggerNode of triggerNodes) {
triggerResponse = await workflow.runTrigger(triggerNode, getTriggerFunctions, additionalData, mode, activation);
triggerResponse = await workflow.runTrigger(
triggerNode,
getTriggerFunctions,
additionalData,
mode,
activation,
);
if (triggerResponse !== undefined) {
// If a response was given save it
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.workflowData[id].triggerResponses!.push(triggerResponse);
}
}
@ -88,12 +100,21 @@ export class ActiveWorkflows {
if (pollNodes.length) {
this.workflowData[id].pollResponses = [];
for (const pollNode of pollNodes) {
this.workflowData[id].pollResponses!.push(await this.activatePolling(pollNode, workflow, additionalData, getPollFunctions, mode, activation));
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.workflowData[id].pollResponses!.push(
await this.activatePolling(
pollNode,
workflow,
additionalData,
getPollFunctions,
mode,
activation,
),
);
}
}
}
/**
* Activates polling for the given node
*
@ -104,7 +125,14 @@ export class ActiveWorkflows {
* @returns {Promise<IPollResponse>}
* @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 pollTimes = pollFunctions.getNodeParameter('pollTimes') as unknown as {
@ -113,12 +141,12 @@ export class ActiveWorkflows {
// Define the order the cron-time-parameter appear
const parameterOrder = [
'second', // 0 - 59
'minute', // 0 - 59
'hour', // 0 - 23
'second', // 0 - 59
'minute', // 0 - 59
'hour', // 0 - 23
'dayOfMonth', // 1 - 31
'month', // 0 - 11(Jan - Dec)
'weekday', // 0 - 6(Sun - Sat)
'month', // 0 - 11(Jan - Dec)
'weekday', // 0 - 6(Sun - Sat)
];
// Get all the trigger times
@ -165,10 +193,15 @@ export class ActiveWorkflows {
// The trigger function to execute when the cron-time got reached
const executeTrigger = async () => {
Logger.info(`Polling trigger initiated for workflow "${workflow.name}"`, {workflowName: workflow.name, workflowId: workflow.id});
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
Logger.info(`Polling trigger initiated for workflow "${workflow.name}"`, {
workflowName: workflow.name,
workflowId: workflow.id,
});
const pollResponse = await workflow.runPoll(node, pollFunctions);
if (pollResponse !== null) {
// eslint-disable-next-line no-underscore-dangle
pollFunctions.__emit(pollResponse);
}
};
@ -180,6 +213,7 @@ export class ActiveWorkflows {
// Start the cron-jobs
const cronJobs: CronJob[] = [];
// eslint-disable-next-line @typescript-eslint/no-shadow
for (const cronTime of cronTimes) {
const cronTimeParts = cronTime.split(' ');
if (cronTimeParts.length > 0 && cronTimeParts[0].includes('*')) {
@ -201,7 +235,6 @@ export class ActiveWorkflows {
};
}
/**
* Makes a workflow inactive
*
@ -212,7 +245,9 @@ export class ActiveWorkflows {
async remove(id: string): Promise<void> {
if (!this.isActive(id)) {
// Workflow is currently not registered
throw new Error(`The workflow with the id "${id}" is currently not active and can so not be removed`);
throw new Error(
`The workflow with the id "${id}" is currently not active and can so not be removed`,
);
}
const workflowData = this.workflowData[id];
@ -235,5 +270,4 @@ export class ActiveWorkflows {
delete this.workflowData[id];
}
}

View File

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

View File

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

View File

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

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

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

View File

@ -1,8 +1,12 @@
try {
require('source-map-support').install();
} catch (error) {
/* eslint-disable import/no-cycle */
import * as NodeExecuteFunctions from './NodeExecuteFunctions';
import * as UserSettings from './UserSettings';
}
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, import/no-extraneous-dependencies, global-require, @typescript-eslint/no-var-requires
require('source-map-support').install();
// eslint-disable-next-line no-empty
} catch (error) {}
export * from './ActiveWorkflows';
export * from './ActiveWebhooks';
@ -13,10 +17,4 @@ export * from './Interfaces';
export * from './LoadNodeParameterOptions';
export * from './NodeExecuteFunctions';
export * from './WorkflowExecute';
import * as NodeExecuteFunctions from './NodeExecuteFunctions';
import * as UserSettings from './UserSettings';
export {
NodeExecuteFunctions,
UserSettings,
};
export { NodeExecuteFunctions, UserSettings };

View File

@ -1,88 +1,83 @@
import { Credentials } from '../src';
describe('Credentials', () => {
describe('without nodeType set', () => {
test('should be able to set and read key data without initial data set', () => {
const credentials = new Credentials('testName', 'testType', []);
describe('without nodeType set', () => {
const key = 'key1';
const password = 'password';
// const nodeType = 'base.noOp';
const newData = 1234;
test('should be able to set and read key data without initial data set', () => {
const credentials = new Credentials('testName', 'testType', []);
const key = 'key1';
const password = 'password';
// const nodeType = 'base.noOp';
const newData = 1234;
credentials.setDataKey(key, newData, password);
expect(credentials.getDataKey(key, password)).toEqual(newData);
});
test('should be able to set and read key data with initial data set', () => {
const key = 'key2';
const password = 'password';
// Saved under "key1"
const initialData = 4321;
const initialDataEncoded = 'U2FsdGVkX1+0baznXt+Ag/ub8A2kHLyoLxn/rR9h4XQ=';
const credentials = new Credentials('testName', 'testType', [], initialDataEncoded);
const newData = 1234;
// Set and read new data
credentials.setDataKey(key, newData, password);
expect(credentials.getDataKey(key, password)).toEqual(newData);
// Read the data which got provided encrypted on init
expect(credentials.getDataKey('key1', password)).toEqual(initialData);
});
credentials.setDataKey(key, newData, password);
expect(credentials.getDataKey(key, password)).toEqual(newData);
});
describe('with nodeType set', () => {
test('should be able to set and read key data with initial data set', () => {
const key = 'key2';
const password = 'password';
test('should be able to set and read key data without initial data set', () => {
// Saved under "key1"
const initialData = 4321;
const initialDataEncoded = 'U2FsdGVkX1+0baznXt+Ag/ub8A2kHLyoLxn/rR9h4XQ=';
const nodeAccess = [
{
nodeType: 'base.noOp',
user: 'userName',
date: new Date(),
},
];
const credentials = new Credentials('testName', 'testType', [], initialDataEncoded);
const credentials = new Credentials('testName', 'testType', nodeAccess);
const newData = 1234;
const key = 'key1';
const password = 'password';
const nodeType = 'base.noOp';
const newData = 1234;
// Set and read new data
credentials.setDataKey(key, newData, password);
expect(credentials.getDataKey(key, password)).toEqual(newData);
credentials.setDataKey(key, newData, password);
// Should be able to read with nodeType which has access
expect(credentials.getDataKey(key, password, nodeType)).toEqual(newData);
// Should not be able to read with nodeType which does NOT have access
// expect(credentials.getDataKey(key, password, 'base.otherNode')).toThrowError(Error);
try {
credentials.getDataKey(key, password, 'base.otherNode');
expect(true).toBe(false);
} catch (e) {
expect(e.message).toBe('The node of type "base.otherNode" does not have access to credentials "testName" of type "testType".');
}
// Get the data which will be saved in database
const dbData = credentials.getDataToSave();
expect(dbData.name).toEqual('testName');
expect(dbData.type).toEqual('testType');
expect(dbData.nodesAccess).toEqual(nodeAccess);
// Compare only the first 6 characters as the rest seems to change with each execution
expect(dbData.data!.slice(0, 6)).toEqual('U2FsdGVkX1+wpQWkj+YTzaPSNTFATjnlmFKIsUTZdhk='.slice(0, 6));
});
// Read the data which got provided encrypted on init
expect(credentials.getDataKey('key1', password)).toEqual(initialData);
});
});
describe('with nodeType set', () => {
test('should be able to set and read key data without initial data set', () => {
const nodeAccess = [
{
nodeType: 'base.noOp',
user: 'userName',
date: new Date(),
},
];
const credentials = new Credentials('testName', 'testType', nodeAccess);
const key = 'key1';
const password = 'password';
const nodeType = 'base.noOp';
const newData = 1234;
credentials.setDataKey(key, newData, password);
// Should be able to read with nodeType which has access
expect(credentials.getDataKey(key, password, nodeType)).toEqual(newData);
// Should not be able to read with nodeType which does NOT have access
// expect(credentials.getDataKey(key, password, 'base.otherNode')).toThrowError(Error);
try {
credentials.getDataKey(key, password, 'base.otherNode');
expect(true).toBe(false);
} catch (e) {
expect(e.message).toBe(
'The node of type "base.otherNode" does not have access to credentials "testName" of type "testType".',
);
}
// Get the data which will be saved in database
const dbData = credentials.getDataToSave();
expect(dbData.name).toEqual('testName');
expect(dbData.type).toEqual('testType');
expect(dbData.nodesAccess).toEqual(nodeAccess);
// Compare only the first 6 characters as the rest seems to change with each execution
expect(dbData.data!.slice(0, 6)).toEqual(
'U2FsdGVkX1+wpQWkj+YTzaPSNTFATjnlmFKIsUTZdhk='.slice(0, 6),
);
});
});
});

View File

@ -18,30 +18,27 @@ import {
WorkflowHooks,
} from 'n8n-workflow';
import {
Credentials,
IDeferredPromise,
IExecuteFunctions,
} from '../src';
import { Credentials, IDeferredPromise, IExecuteFunctions } from '../src';
export class CredentialsHelper extends ICredentialsHelper {
getDecrypted(name: string, type: string): Promise<ICredentialDataDecryptedObject> {
return new Promise(res => res({}));
return new Promise((res) => res({}));
}
getCredentials(name: string, type: string): Promise<Credentials> {
return new Promise(res => {
return new Promise((res) => {
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 {
nodeTypes: INodeTypeData = {
'n8n-nodes-base.if': {
sourcePath: '',
@ -161,9 +158,7 @@ class NodeTypesClass implements INodeTypes {
type: 'number',
displayOptions: {
hide: {
operation: [
'isEmpty',
],
operation: ['isEmpty'],
},
},
default: 0,
@ -229,10 +224,7 @@ class NodeTypesClass implements INodeTypes {
type: 'string',
displayOptions: {
hide: {
operation: [
'isEmpty',
'regex',
],
operation: ['isEmpty', 'regex'],
},
},
default: '',
@ -244,9 +236,7 @@ class NodeTypesClass implements INodeTypes {
type: 'string',
displayOptions: {
show: {
operation: [
'regex',
],
operation: ['regex'],
},
},
default: '',
@ -274,7 +264,8 @@ class NodeTypesClass implements INodeTypes {
},
],
default: 'all',
description: 'If multiple rules got set this settings decides if it is true as soon as ANY condition matches or only if ALL get meet.',
description:
'If multiple rules got set this settings decides if it is true as soon as ANY condition matches or only if ALL get meet.',
},
],
},
@ -291,19 +282,30 @@ class NodeTypesClass implements INodeTypes {
const compareOperationFunctions: {
[key: string]: (value1: NodeParameterValue, value2: NodeParameterValue) => boolean;
} = {
contains: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || '').toString().includes((value2 || '').toString()),
notContains: (value1: NodeParameterValue, value2: NodeParameterValue) => !(value1 || '').toString().includes((value2 || '').toString()),
endsWith: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 as string).endsWith(value2 as string),
contains: (value1: NodeParameterValue, value2: NodeParameterValue) =>
(value1 || '').toString().includes((value2 || '').toString()),
notContains: (value1: NodeParameterValue, value2: NodeParameterValue) =>
!(value1 || '').toString().includes((value2 || '').toString()),
endsWith: (value1: NodeParameterValue, value2: NodeParameterValue) =>
(value1 as string).endsWith(value2 as string),
equal: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 === value2,
notEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 !== value2,
larger: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) > (value2 || 0),
largerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) >= (value2 || 0),
smaller: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) < (value2 || 0),
smallerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) <= (value2 || 0),
startsWith: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 as string).startsWith(value2 as string),
isEmpty: (value1: NodeParameterValue) => [undefined, null, ''].includes(value1 as string),
larger: (value1: NodeParameterValue, value2: NodeParameterValue) =>
(value1 || 0) > (value2 || 0),
largerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) =>
(value1 || 0) >= (value2 || 0),
smaller: (value1: NodeParameterValue, value2: NodeParameterValue) =>
(value1 || 0) < (value2 || 0),
smallerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) =>
(value1 || 0) <= (value2 || 0),
startsWith: (value1: NodeParameterValue, value2: NodeParameterValue) =>
(value1 as string).startsWith(value2 as string),
isEmpty: (value1: NodeParameterValue) =>
[undefined, null, ''].includes(value1 as string),
regex: (value1: NodeParameterValue, value2: NodeParameterValue) => {
const regexMatch = (value2 || '').toString().match(new RegExp('^/(.*?)/([gimusy]*)$'));
const regexMatch = (value2 || '')
.toString()
.match(new RegExp('^/(.*?)/([gimusy]*)$'));
let regex: RegExp;
if (!regexMatch) {
@ -319,18 +321,13 @@ class NodeTypesClass implements INodeTypes {
};
// The different dataTypes to check the values in
const dataTypes = [
'boolean',
'number',
'string',
];
const dataTypes = ['boolean', 'number', 'string'];
// Itterate over all items to check which ones should be output as via output "true" and
// which ones via output "false"
let dataType: string;
let compareOperationResult: boolean;
itemLoop:
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
itemLoop: for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
item = items[itemIndex];
let compareData: INodeParameters;
@ -340,9 +337,16 @@ class NodeTypesClass implements INodeTypes {
// Check all the values of the different dataTypes
for (dataType of dataTypes) {
// Check all the values of the current dataType
for (compareData of this.getNodeParameter(`conditions.${dataType}`, itemIndex, []) as INodeParameters[]) {
for (compareData of this.getNodeParameter(
`conditions.${dataType}`,
itemIndex,
[],
) as INodeParameters[]) {
// Check if the values passes
compareOperationResult = compareOperationFunctions[compareData.operation as string](compareData.value1 as NodeParameterValue, compareData.value2 as NodeParameterValue);
compareOperationResult = compareOperationFunctions[compareData.operation as string](
compareData.value1 as NodeParameterValue,
compareData.value2 as NodeParameterValue,
);
if (compareOperationResult === true && combineOperation === 'any') {
// If it passes and the operation is "any" we do not have to check any
@ -397,21 +401,25 @@ class NodeTypesClass implements INodeTypes {
{
name: 'Append',
value: 'append',
description: 'Combines data of both inputs. The output will contain items of input 1 and input 2.',
description:
'Combines data of both inputs. The output will contain items of input 1 and input 2.',
},
{
name: 'Pass-through',
value: 'passThrough',
description: 'Passes through data of one input. The output will conain only items of the defined input.',
description:
'Passes through data of one input. The output will conain only items of the defined input.',
},
{
name: 'Wait',
value: 'wait',
description: 'Waits till data of both inputs is available and will then output a single empty item.',
description:
'Waits till data of both inputs is available and will then output a single empty item.',
},
],
default: 'append',
description: 'How data should be merged. If it should simply<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',
@ -419,9 +427,7 @@ class NodeTypesClass implements INodeTypes {
type: 'options',
displayOptions: {
show: {
mode: [
'passThrough',
],
mode: ['passThrough'],
},
},
options: [
@ -512,7 +518,8 @@ class NodeTypesClass implements INodeTypes {
name: 'keepOnlySet',
type: 'boolean',
default: false,
description: 'If only the values set on this node should be<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',
@ -534,7 +541,8 @@ class NodeTypesClass implements INodeTypes {
name: 'name',
type: 'string',
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',
@ -554,7 +562,8 @@ class NodeTypesClass implements INodeTypes {
name: 'name',
type: 'string',
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',
@ -574,7 +583,8 @@ class NodeTypesClass implements INodeTypes {
name: 'name',
type: 'string',
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',
@ -610,7 +620,6 @@ class NodeTypesClass implements INodeTypes {
],
},
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
if (items.length === 0) {
@ -643,31 +652,37 @@ class NodeTypesClass implements INodeTypes {
}
// Add boolean values
(this.getNodeParameter('values.boolean', itemIndex, []) as INodeParameters[]).forEach((setItem) => {
if (options.dotNotation === false) {
newItem.json[setItem.name as string] = !!setItem.value;
} else {
set(newItem.json, setItem.name as string, !!setItem.value);
}
});
(this.getNodeParameter('values.boolean', itemIndex, []) as INodeParameters[]).forEach(
(setItem) => {
if (options.dotNotation === false) {
newItem.json[setItem.name as string] = !!setItem.value;
} else {
set(newItem.json, setItem.name as string, !!setItem.value);
}
},
);
// Add number values
(this.getNodeParameter('values.number', itemIndex, []) as INodeParameters[]).forEach((setItem) => {
if (options.dotNotation === false) {
newItem.json[setItem.name as string] = setItem.value;
} else {
set(newItem.json, setItem.name as string, setItem.value);
}
});
(this.getNodeParameter('values.number', itemIndex, []) as INodeParameters[]).forEach(
(setItem) => {
if (options.dotNotation === false) {
newItem.json[setItem.name as string] = setItem.value;
} else {
set(newItem.json, setItem.name as string, setItem.value);
}
},
);
// Add string values
(this.getNodeParameter('values.string', itemIndex, []) as INodeParameters[]).forEach((setItem) => {
if (options.dotNotation === false) {
newItem.json[setItem.name as string] = setItem.value;
} else {
set(newItem.json, setItem.name as string, setItem.value);
}
});
(this.getNodeParameter('values.string', itemIndex, []) as INodeParameters[]).forEach(
(setItem) => {
if (options.dotNotation === false) {
newItem.json[setItem.name as string] = setItem.value;
} else {
set(newItem.json, setItem.name as string, setItem.value);
}
},
);
returnData.push(newItem);
}
@ -702,7 +717,7 @@ class NodeTypesClass implements INodeTypes {
},
};
async init(nodeTypes: INodeTypeData): Promise<void> { }
async init(nodeTypes: INodeTypeData): Promise<void> {}
getAll(): INodeType[] {
return Object.values(this.nodeTypes).map((data) => data.type);
@ -715,7 +730,6 @@ class NodeTypesClass implements INodeTypes {
let nodeTypesInstance: NodeTypesClass | undefined;
export function NodeTypes(): NodeTypesClass {
if (nodeTypesInstance === undefined) {
nodeTypesInstance = new NodeTypesClass();
@ -725,8 +739,10 @@ export function NodeTypes(): NodeTypesClass {
return nodeTypesInstance;
}
export function WorkflowExecuteAdditionalData(waitPromise: IDeferredPromise<IRun>, nodeExecutionOrder: string[]): IWorkflowExecuteAdditionalData {
export function WorkflowExecuteAdditionalData(
waitPromise: IDeferredPromise<IRun>,
nodeExecutionOrder: string[],
): IWorkflowExecuteAdditionalData {
const hookFunctions = {
nodeExecuteAfter: [
async (nodeName: string, data: ITaskData): Promise<void> => {
@ -752,7 +768,7 @@ export function WorkflowExecuteAdditionalData(waitPromise: IDeferredPromise<IRun
return {
credentialsHelper: new CredentialsHelper(''),
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) => {},
restApiUrl: '',
encryptionKey: 'test',

File diff suppressed because it is too large Load Diff

View File

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

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: [
// /\/node_modules\/quill/
// ]
plugins: [
"@babel/plugin-proposal-class-properties",
],
};
// // https://stackoverflow.com/questions/44625868/es6-babel-class-constructor-cannot-be-invoked-without-new

View File

@ -16,11 +16,11 @@
"scripts": {
"build": "cross-env VUE_APP_PUBLIC_PATH=\"/%BASE_PATH%/\" vue-cli-service build",
"dev": "npm run serve",
"lint": "vue-cli-service lint",
"format": "cd ../.. && node_modules/prettier/bin-prettier.js packages/editor-ui/**/**.ts --write",
"lint": "tslint -p tsconfig.json -c tslint.json",
"lintfix": "tslint --fix -p tsconfig.json -c tslint.json",
"serve": "cross-env VUE_APP_URL_BASE_API=http://localhost:5678/ vue-cli-service serve",
"test": "npm run test:unit",
"tslint": "tslint -p tsconfig.json -c tslint.json",
"tslintfix": "tslint --fix -p tsconfig.json -c tslint.json",
"test:e2e": "vue-cli-service test:e2e",
"test:unit": "vue-cli-service test:unit"
},
@ -44,11 +44,11 @@
"@types/node": "^14.14.40",
"@types/quill": "^2.0.1",
"@types/uuid": "^8.3.0",
"@typescript-eslint/eslint-plugin": "^4.18.0",
"@typescript-eslint/parser": "^4.18.0",
"@typescript-eslint/eslint-plugin": "^4.29.0",
"@typescript-eslint/parser": "^4.29.0",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-typescript": "~4.5.0",
"@vue/cli-plugin-typescript": "~4.5.6",
"@vue/cli-plugin-unit-jest": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/eslint-config-standard": "^5.0.1",
@ -60,9 +60,9 @@
"cross-env": "^7.0.2",
"dateformat": "^3.0.3",
"element-ui": "~2.13.0",
"eslint": "^6.8.0",
"eslint-plugin-import": "^2.19.1",
"eslint-plugin-vue": "^6.2.2",
"eslint": "^7.32.0",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-vue": "^7.16.0",
"file-saver": "^2.0.2",
"flatted": "^2.0.0",
"jquery": "^3.4.1",
@ -81,7 +81,7 @@
"string-template-parser": "^1.2.6",
"ts-jest": "^26.3.0",
"tslint": "^6.1.2",
"typescript": "~3.9.7",
"typescript": "~4.3.5",
"uuid": "^8.3.0",
"vue": "^2.6.11",
"vue-cli-plugin-webpack-bundle-analyzer": "^2.0.0",

View File

@ -1,4 +1,5 @@
<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">
<div :class="$style.header">
<div>

View File

@ -1,12 +1,7 @@
import {
UserSettings,
} from "n8n-core";
import { UserSettings } from 'n8n-core';
import { Command, flags } from '@oclif/command';
import {
buildFiles,
IBuildOptions,
} from '../src';
import { buildFiles, IBuildOptions } from '../src';
export class Build extends Command {
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()}]`,
}),
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() {
// eslint-disable-next-line @typescript-eslint/no-shadow
const { flags } = this.parse(Build);
this.log('\nBuild credentials and nodes');
@ -47,13 +45,12 @@ export class Build extends Command {
const outputDirectory = await buildFiles(options);
this.log(`The nodes got build and saved into the following folder:\n${outputDirectory}`);
} 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('====================================');
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
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 fs from 'fs';
import * as inquirer from 'inquirer';
import { Command } from '@oclif/command';
import { join } from 'path';
const { promisify } = require('util');
const fsAccess = promisify(fs.access);
import { createTemplate } from '../src';
import {
createTemplate
} from '../src';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { promisify } = require('util');
const fsAccess = promisify(fs.access);
export class New extends Command {
static description = 'Create new credentials/node';
static examples = [
`$ n8n-node-dev new`,
];
static examples = [`$ n8n-node-dev new`];
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async run() {
try {
this.log('\nCreate new credentials/node');
this.log('=========================');
@ -30,10 +31,7 @@ export class New extends Command {
type: 'list',
default: 'Node',
message: 'What do you want to create?',
choices: [
'Credentials',
'Node',
],
choices: ['Credentials', 'Node'],
};
const typeAnswers = await inquirer.prompt(typeQuestion);
@ -43,7 +41,6 @@ export class New extends Command {
let defaultName = '';
let getDescription = false;
if (typeAnswers.type === 'Node') {
// Create new node
@ -54,11 +51,7 @@ export class New extends Command {
type: 'list',
default: 'Execute',
message: 'What kind of node do you want to create?',
choices: [
'Execute',
'Trigger',
'Webhook',
],
choices: ['Execute', 'Trigger', 'Webhook'],
};
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
additionalQuestions.push({
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;
// Define the source file to be used and the location and name of the new
// 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);
@ -150,12 +149,13 @@ export class New extends Command {
this.log('\nExecution was successfull:');
this.log('====================================');
this.log('Node got created: ' + destinationFilePath);
this.log(`Node got created: ${destinationFilePath}`);
} 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('====================================');
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
this.log(error.stack);
return;
}
}
}

View File

@ -21,10 +21,11 @@
"scripts": {
"dev": "npm run watch",
"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",
"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"
},
"bin": {
@ -64,7 +65,7 @@
"oauth-1.0a": "^2.2.6",
"replace-in-file": "^6.0.0",
"request": "^2.88.2",
"tmp-promise": "^2.0.2",
"typescript": "~3.9.7"
"tmp-promise": "^3.0.2",
"typescript": "~4.3.5"
}
}

View File

@ -1,26 +1,23 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { ChildProcess, spawn } from 'child_process';
const copyfiles = require('copyfiles');
import {
readFile as fsReadFile,
} from 'fs/promises';
import {
write as fsWrite,
} from 'fs';
import { readFile as fsReadFile } from 'fs/promises';
import { write as fsWrite } from 'fs';
import { join } from 'path';
import { file } from 'tmp-promise';
import { promisify } from 'util';
const fsReadFileAsync = promisify(fsReadFile);
const fsWriteAsync = promisify(fsWrite);
import { UserSettings } from 'n8n-core';
// eslint-disable-next-line import/no-cycle
import { IBuildOptions } from '.';
import {
UserSettings,
} from 'n8n-core';
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires
const copyfiles = require('copyfiles');
// 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
@ -30,23 +27,26 @@ import {
* @export
* @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
const tsconfigPath = join(__dirname, '../../src/tsconfig-build.json');
// 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);
// Set absolute include paths
const newIncludeFiles = [];
// eslint-disable-next-line no-restricted-syntax
for (const includeFile of tsConfig.include) {
newIncludeFiles.push(join(process.cwd(), includeFile));
}
tsConfig.include = newIncludeFiles;
// Write new custom tsconfig file
// eslint-disable-next-line @typescript-eslint/unbound-method
const { fd, path, cleanup } = await file({ dir: process.cwd() });
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
*
@ -64,7 +63,8 @@ export async function createCustomTsconfig () {
* @param {IBuildOptions} [options] Options to overwrite default behaviour
* @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 || {};
let typescriptPath;
@ -79,24 +79,31 @@ export async function buildFiles (options?: IBuildOptions): Promise<string> {
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
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) {
buildCommand += ' --watch';
}
let buildProcess: ChildProcess;
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
// that the user can see what is happening
//@ts-ignore
// @ts-ignore
buildProcess.stdout.pipe(process.stdout);
//@ts-ignore
// @ts-ignore
buildProcess.stderr.pipe(process.stderr);
// Make sure that the child process gets also always terminated
@ -105,27 +112,33 @@ export async function buildFiles (options?: IBuildOptions): Promise<string> {
buildProcess.kill();
});
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
let errorMessage = error.message;
if (error.stdout !== undefined) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
errorMessage = `${errorMessage}\nGot following output:\n${error.stdout}`;
}
// Remove the tmp tsconfig file
// eslint-disable-next-line @typescript-eslint/no-floating-promises
tsconfigData.cleanup();
throw new Error(errorMessage);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
return new Promise((resolve, reject) => {
['*.png', '*.node.json'].forEach(filenamePattern => {
copyfiles(
[join(process.cwd(), `./${filenamePattern}`), outputDirectory],
{ up: true },
() => resolve(outputDirectory));
['*.png', '*.node.json'].forEach((filenamePattern) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
copyfiles([join(process.cwd(), `./${filenamePattern}`), outputDirectory], { up: true }, () =>
resolve(outputDirectory),
);
});
buildProcess.on('exit', code => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
buildProcess.on('exit', (code) => {
// Remove the tmp tsconfig file
// eslint-disable-next-line @typescript-eslint/no-floating-promises
tsconfigData.cleanup();
});
});

View File

@ -1,10 +1,11 @@
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 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
@ -15,16 +16,18 @@ const fsCopyFile = promisify(fs.copyFile);
* @param {object} replaceValues The values to replace in the template file
* @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
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
await fsCopyFile(sourceFilePath, destinationFilePath);
// Replace the variables in the template file
const options: ReplaceInFileConfig = {
files: [
destinationFilePath,
],
files: [destinationFilePath],
from: [],
to: [],
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -552,6 +552,7 @@ export class Slack implements INodeType {
attachment.fields = attachment.fields.item;
} else {
// If it does not have any items set remove it
// @ts-ignore
delete attachment.fields;
}
}
@ -786,6 +787,7 @@ export class Slack implements INodeType {
attachment.fields = attachment.fields.item;
} else {
// If it does not have any items set remove it
// @ts-ignore
delete attachment.fields;
}
}
@ -1037,11 +1039,11 @@ export class Slack implements INodeType {
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const qs: IDataObject = {};
Object.assign(qs, additionalFields);
responseData = await slackApiRequest.call(this, 'POST', '/users.profile.get', undefined, qs);
responseData = responseData.profile;
}
}

View File

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

View File

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

View File

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

View File

@ -17,8 +17,9 @@
"scripts": {
"dev": "npm run watch",
"build": "tsc",
"tslint": "tslint -p tsconfig.json -c tslint.json",
"tslintfix": "tslint --fix -p tsconfig.json -c tslint.json",
"format": "cd ../.. && node_modules/prettier/bin-prettier.js packages/workflow/**/**.ts --write",
"lint": "cd ../.. && node_modules/eslint/bin/eslint.js packages/workflow",
"lintfix": "cd ../.. && node_modules/eslint/bin/eslint.js packages/workflow --fix",
"watch": "tsc --watch",
"test": "jest"
},
@ -31,10 +32,18 @@
"@types/lodash.get": "^4.4.6",
"@types/node": "^14.14.40",
"@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",
"prettier": "^2.3.2",
"ts-jest": "^26.3.0",
"tslint": "^6.1.2",
"typescript": "~3.9.7"
"typescript": "~4.3.5"
},
"dependencies": {
"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 {
INode,
INodeExecutionData,
@ -9,28 +11,26 @@ import {
Workflow,
WorkflowDataProxy,
WorkflowExecuteMode,
} from './';
} from '.';
// @ts-ignore
import * as tmpl from 'riot-tmpl';
// 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('{{ }}');
// Make sure that it does not always print an error when it could not resolve
// a variable
tmpl.tmpl.errorHandler = () => { };
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
tmpl.tmpl.errorHandler = () => {};
export class Expression {
workflow: Workflow;
constructor(workflow: Workflow) {
this.workflow = workflow;
}
/**
* Converts an object to a string in a way to make it clear that
* the value comes from an object
@ -44,8 +44,6 @@ export class Expression {
return `[${typeName}: ${JSON.stringify(value)}]`;
}
/**
* 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.
@ -60,7 +58,19 @@ export class Expression {
* @returns {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[])}
* @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
if (typeof parameterValue !== 'string' || parameterValue.charAt(0) !== '=') {
// Is no expression so return value
@ -70,30 +80,44 @@ export class Expression {
// Is an expression
// Remove the equal sign
// eslint-disable-next-line no-param-reassign
parameterValue = parameterValue.substr(1);
// 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();
// Execute the expression
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);
if (typeof returnValue === 'function') {
throw new Error('Expression resolved to a function. Please add "()"');
} else if (returnValue !== null && typeof returnValue === 'object') {
if (returnObjectAsString === true) {
if (returnObjectAsString) {
return this.convertObjectValueToString(returnValue);
}
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return returnValue;
} 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}`);
}
}
/**
* Resolves value of parameter. But does not work for workflow-data.
*
@ -103,7 +127,13 @@ export class Expression {
* @returns {(string | undefined)}
* @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) {
// Value is not set so return the default
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.
*
@ -133,7 +170,19 @@ export class Expression {
* @returns {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | undefined)}
* @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) {
// Value is not set so return the default
return defaultValue;
@ -150,14 +199,34 @@ export class Expression {
};
// 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
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
* 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[])}
* @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
const isComplexParameter = (value: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[]) => {
const isComplexParameter = (
value: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[],
) => {
return typeof value === 'object';
};
// 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)) {
return this.getParameterValue(value, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, mode, additionalKeys, returnObjectAsString, selfData);
} else {
return this.resolveSimpleParameterValue(value as NodeParameterValue, siblingParameters, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, mode, additionalKeys, returnObjectAsString, selfData);
return this.getParameterValue(
value,
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
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
@ -198,28 +317,33 @@ export class Expression {
if (Array.isArray(parameterValue)) {
// Data is an array
const returnData = [];
// eslint-disable-next-line no-restricted-syntax
for (const item of parameterValue) {
returnData.push(resolveParameterValue(item, {}));
}
if (returnObjectAsString === true && typeof returnData === 'object') {
if (returnObjectAsString && typeof returnData === 'object') {
return this.convertObjectValueToString(returnData);
}
return returnData as NodeParameterValue[] | INodeParameters[];
} else if (parameterValue === null || parameterValue === undefined) {
return parameterValue;
} else {
// Data is an object
const returnData: INodeParameters = {};
for (const key of Object.keys(parameterValue)) {
returnData[key] = resolveParameterValue((parameterValue as INodeParameters)[key], parameterValue as INodeParameters);
}
if (returnObjectAsString === true && typeof returnData === 'object') {
return this.convertObjectValueToString(returnData);
}
return returnData;
}
if (parameterValue === null || parameterValue === undefined) {
return parameterValue;
}
// Data is an object
const returnData: INodeParameters = {};
// eslint-disable-next-line no-restricted-syntax
for (const key of Object.keys(parameterValue)) {
returnData[key] = resolveParameterValue(
(parameterValue as INodeParameters)[key],
parameterValue as INodeParameters,
);
}
if (returnObjectAsString && typeof returnData === 'object') {
return this.convertObjectValueToString(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 { WorkflowHooks } from './WorkflowHooks';
import { WorkflowOperationError } from './WorkflowErrors';
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 {
[key: string]: string | undefined;
@ -43,8 +55,11 @@ export interface IGetCredentials {
export abstract class ICredentials {
name: string;
type: string;
data: string | undefined;
nodesAccess: ICredentialNodeAccess[];
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 getDataKey(key: string, encryptionKey: string, nodeType?: string): CredentialInformation;
abstract getDataToSave(): ICredentialsEncrypted;
abstract hasNodeAccess(nodeType: string): boolean;
abstract setData(data: ICredentialDataDecryptedObject, 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 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 {
@ -116,7 +148,7 @@ export interface ICredentialType {
export interface ICredentialTypes {
credentialTypes?: {
[key: string]: ICredentialType
[key: string]: ICredentialType;
};
init(credentialTypes?: { [key: string]: ICredentialType }): Promise<void>;
getAll(): ICredentialType[];
@ -133,7 +165,6 @@ export interface ICredentialData {
// The encrypted credentials which the nodes can access
export type CredentialInformation = string | number | boolean | IDataObject;
// The encrypted credentials which the nodes can access
export interface ICredentialDataDecryptedObject {
[key: string]: CredentialInformation;
@ -159,92 +190,150 @@ export interface IDataObject {
[key: string]: GenericValue | IDataObject | GenericValue[] | IDataObject[];
}
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 {
(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 {
(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 {
(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 {
(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 {
(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 {
data: ITaskDataConnections;
node: INode;
}
export type IContextObject = {
[key: string]: any; // tslint:disable-line:no-any
[key: string]: any;
};
export interface IExecuteContextData {
// Keys are: "flow" | "node:<NODE_NAME>"
[key: string]: IContextObject;
}
export interface IExecuteFunctions {
continueOnFail(): boolean;
evaluateExpression(expression: string, itemIndex: number): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[];
executeWorkflow(workflowInfo: IExecuteWorkflowInfo, inputData?: INodeExecutionData[]): Promise<any>; // tslint:disable-line:no-any
evaluateExpression(
expression: string,
itemIndex: number,
): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[];
executeWorkflow(
workflowInfo: IExecuteWorkflowInfo,
inputData?: INodeExecutionData[],
): Promise<any>;
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[];
getMode(): WorkflowExecuteMode;
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;
getWorkflowStaticData(type: string): IDataObject;
getRestApiUrl(): string;
getTimezone(): string;
getWorkflow(): IWorkflowMetadata;
prepareOutputData(outputData: INodeExecutionData[], outputIndex?: number): Promise<INodeExecutionData[][]>;
prepareOutputData(
outputData: INodeExecutionData[],
outputIndex?: number,
): Promise<INodeExecutionData[][]>;
putExecutionToWait(waitTill: Date): Promise<void>;
sendMessageToUI(message: any): void; // tslint:disable-line:no-any
helpers: {
[key: string]: (...args: any[]) => any //tslint:disable-line:no-any
[key: string]: (...args: any[]) => any;
};
}
export interface IExecuteSingleFunctions {
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;
getCredentials(type: string): Promise<ICredentialDataDecryptedObject | undefined>;
getInputData(inputIndex?: number, inputName?: string): INodeExecutionData;
getMode(): WorkflowExecuteMode;
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;
getTimezone(): string;
getWorkflow(): IWorkflowMetadata;
getWorkflowDataProxy(): IWorkflowDataProxyData;
getWorkflowStaticData(type: string): IDataObject;
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 {
getCredentials(type: string): Promise<ICredentialDataDecryptedObject | undefined>;
getNode(): INode;
getNodeParameter(parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object; //tslint:disable-line:no-any
getCurrentNodeParameter(parameterName: string): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object | undefined;
getNodeParameter(
parameterName: string,
fallbackValue?: any,
): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object;
getCurrentNodeParameter(
parameterName: string,
):
| NodeParameterValue
| INodeParameters
| NodeParameterValue[]
| INodeParameters[]
| object
| undefined;
getCurrentNodeParameters(): INodeParameters | undefined;
getTimezone(): string;
getRestApiUrl(): string;
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;
getNode(): INode;
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;
getWebhookDescription(name: string): IWebhookDescription | undefined;
getWebhookName(): string;
getWorkflow(): IWorkflowMetadata;
getWorkflowStaticData(type: string): IDataObject;
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;
getActivationMode(): WorkflowActivateMode;
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;
getTimezone(): string;
getWorkflow(): IWorkflowMetadata;
getWorkflowStaticData(type: string): IDataObject;
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;
getActivationMode(): WorkflowActivateMode;
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;
getTimezone(): string;
getWorkflow(): IWorkflowMetadata;
getWorkflowStaticData(type: string): IDataObject;
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;
getMode(): WorkflowExecuteMode;
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;
getParamsData(): object;
getQueryData(): object;
@ -331,9 +443,12 @@ export interface IWebhookFunctions {
getWebhookName(): string;
getWorkflowStaticData(type: string): IDataObject;
getWorkflow(): IWorkflowMetadata;
prepareOutputData(outputData: INodeExecutionData[], outputIndex?: number): Promise<INodeExecutionData[][]>;
prepareOutputData(
outputData: INodeExecutionData[],
outputIndex?: number,
): Promise<INodeExecutionData[][]>;
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;
}
export interface INodes {
[key: string]: INode;
}
export interface IObservableObject {
[key: string]: any; // tslint:disable-line:no-any
[key: string]: any;
__dataChanged: boolean;
}
export interface IBinaryKeyData {
[key: string]: IBinaryData;
}
@ -385,7 +497,6 @@ export interface INodeExecutionData {
binary?: IBinaryKeyData;
}
export interface INodeExecuteFunctions {
getExecutePollFunctions: IGetExecutePollFunctions;
getExecuteTriggerFunctions: IGetExecuteTriggerFunctions;
@ -395,7 +506,6 @@ export interface INodeExecuteFunctions {
getExecuteWebhookFunctions: IGetExecuteWebhookFunctions;
}
// The values a node property can have
export type NodeParameterValue = string | number | boolean | undefined | null;
@ -404,25 +514,37 @@ export interface 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 interface INodePropertyTypeOptions {
alwaysOpenEditWindow?: boolean; // Supported by: string
editor?: EditorTypes; // Supported by: string
loadOptionsDependsOn?: string[]; // Supported by: options
loadOptionsMethod?: string; // Supported by: options
maxValue?: number; // Supported by: number
minValue?: number; // Supported by: number
multipleValues?: boolean; // Supported by: <All>
multipleValueButtonText?: string; // Supported when "multipleValues" set to true
numberPrecision?: number; // Supported by: number
numberStepSize?: number; // Supported by: number
password?: boolean; // Supported by: string
rows?: number; // Supported by: string
showAlpha?: boolean; // Supported by: color
sortable?: boolean; // Supported when "multipleValues" set to true
editor?: EditorTypes; // Supported by: string
loadOptionsDependsOn?: string[]; // Supported by: options
loadOptionsMethod?: string; // Supported by: options
maxValue?: number; // Supported by: number
minValue?: number; // Supported by: number
multipleValues?: boolean; // Supported by: <All>
multipleValueButtonText?: string; // Supported when "multipleValues" set to true
numberPrecision?: number; // Supported by: number
numberStepSize?: number; // Supported by: number
password?: boolean; // Supported by: string
rows?: number; // Supported by: string
showAlpha?: boolean; // Supported by: color
sortable?: boolean; // Supported when "multipleValues" set to true
[key: string]: boolean | number | string | EditorTypes | undefined | string[];
}
@ -435,7 +557,6 @@ export interface IDisplayOptions {
};
}
export interface INodeProperties {
displayName: string;
name: string;
@ -492,7 +613,7 @@ export interface INodeType {
methods?: {
loadOptions?: {
[key: string]: (this: ILoadOptionsFunctions) => Promise<INodePropertyOptions[]>;
}
};
};
webhookMethods?: {
[key: string]: IWebhookSetupMethods;
@ -501,7 +622,6 @@ export interface INodeType {
export type WebhookSetupMethodNames = 'checkExists' | 'create' | 'delete';
export interface IWebhookSetupMethods {
[key: string]: ((this: IHookFunctions) => Promise<boolean>) | undefined;
checkExists?: (this: IHookFunctions) => Promise<boolean>;
@ -509,7 +629,6 @@ export interface IWebhookSetupMethods {
delete?: (this: IHookFunctions) => Promise<boolean>;
}
export interface INodeCredentialDescription {
name: string;
required?: boolean;
@ -596,17 +715,17 @@ export interface IWebhookDescription {
}
export interface IWorkflowDataProxyData {
$binary: any; // tslint:disable-line:no-any
$data: any; // tslint:disable-line:no-any
$env: any; // tslint:disable-line:no-any
$evaluateExpression: any; // tslint:disable-line:no-any
$item: any; // tslint:disable-line:no-any
$items: any; // tslint:disable-line:no-any
$json: any; // tslint:disable-line:no-any
$node: any; // tslint:disable-line:no-any
$parameter: any; // tslint:disable-line:no-any
$position: any; // tslint:disable-line:no-any
$workflow: any; // tslint:disable-line:no-any
$binary: any;
$data: any;
$env: any;
$evaluateExpression: any;
$item: any;
$items: any;
$json: any;
$node: any;
$parameter: any;
$position: any;
$workflow: any;
}
export interface IWorkflowDataProxyAdditionalKeys {
@ -623,7 +742,7 @@ export type WebhookHttpMethod = 'GET' | 'POST' | 'HEAD' | 'OPTIONS';
export interface IWebhookResponseData {
workflowData?: INodeExecutionData[][];
webhookResponse?: any; // tslint:disable-line:no-any
webhookResponse?: any;
noWebhookResponse?: boolean;
}
@ -637,7 +756,6 @@ export interface INodeTypes {
getByName(nodeType: string): INodeType | undefined;
}
export interface INodeTypeData {
[key: string]: {
type: INodeType;
@ -654,7 +772,6 @@ export interface IRun {
stoppedAt?: Date;
}
// Contains all the data which is needed to execute a workflow and so also to
// start restart it again after it did fail.
// The RunData, ExecuteData and WaitForExecution contain often the same data.
@ -676,13 +793,11 @@ export interface IRunExecutionData {
waitTill?: Date;
}
export interface IRunData {
// node-name: result-data
[key: string]: ITaskData[];
}
// The data that gets returned when a node runs
export interface ITaskData {
startTime: number;
@ -691,7 +806,6 @@ export interface ITaskData {
error?: ExecutionError;
}
// The data for al the different kind of connectons (like main) and all the indexes
export interface ITaskDataConnections {
// 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>;
}
// Keeps data while workflow gets executed and allows when provided to restart execution
export interface IWaitingForExecution {
// Node name
[key: string]: {
// Run index
[key: number]: ITaskDataConnections
[key: number]: ITaskDataConnections;
};
}
export interface IWorkflowBase {
id?: number | string | any; // tslint:disable-line:no-any
id?: number | string | any;
name: string;
active: boolean;
createdAt: Date;
@ -732,26 +843,34 @@ export interface IWorkflowCredentials {
};
}
export interface IWorkflowExecuteHooks {
[key: string]: Array<((...args: any[]) => Promise<void>)> | undefined; // tslint:disable-line:no-any
nodeExecuteAfter?: Array<((nodeName: string, data: ITaskData, executionData: 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>)>;
[key: string]: Array<(...args: any[]) => Promise<void>> | undefined;
nodeExecuteAfter?: Array<
(nodeName: string, data: ITaskData, executionData: 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 {
credentialsHelper: ICredentialsHelper;
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;
executionId?: string;
hooks?: WorkflowHooks;
httpResponse?: express.Response;
httpRequest?: express.Request;
restApiUrl: string;
sendMessageToUI?: (source: string, message: any) => void; // tslint:disable-line:no-any
sendMessageToUI?: (source: string, message: any) => void;
timezone: string;
webhookBaseUrl: string;
webhookWaitingBaseUrl: string;
@ -760,7 +879,15 @@ export interface IWorkflowExecuteAdditionalData {
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 interface IWorkflowHooksOptionalParameters {
@ -790,7 +917,7 @@ export interface IStatusCodeMessages {
export type CodexData = {
categories?: string[];
subcategories?: {[category: string]: string[]};
subcategories?: { [category: string]: string[] };
alias?: string[];
};

View File

@ -1,8 +1,6 @@
import {
ILogger,
LogTypes,
} from './Interfaces';
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
// eslint-disable-next-line import/no-cycle
import { ILogger, LogTypes } from './Interfaces';
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';
// 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.
@ -33,7 +41,14 @@ const ERROR_MESSAGE_PROPERTIES = [
/**
* 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.
@ -46,8 +61,11 @@ const ERROR_NESTING_PROPERTIES = ['error', 'err', 'response', 'body', 'data'];
*/
abstract class NodeError extends Error {
description: string | null | undefined;
cause: Error | JsonObject;
node: INode;
timestamp: number;
constructor(node: INode, error: Error | JsonObject) {
@ -95,13 +113,17 @@ abstract class NodeError extends Error {
potentialKeys: string[],
traversalKeys: string[] = [],
): string | null {
for(const key of potentialKeys) {
// eslint-disable-next-line no-restricted-syntax
for (const key of potentialKeys) {
if (error[key]) {
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 (Array.isArray(error[key])) {
// @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 === 'number') return error.toString();
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) {
if (this.isTraversableObject(error[key])) {
const property = this.findProperty(error[key] as JsonObject, potentialKeys, traversalKeys);
@ -140,18 +163,24 @@ abstract class NodeError extends Error {
/**
* 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
return value && typeof value === 'object' && !Array.isArray(value) && !!Object.keys(value).length;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected isTraversableObject(value: any): value is JsonObject {
return (
value && typeof value === 'object' && !Array.isArray(value) && !!Object.keys(value).length
);
}
/**
* Remove circular references from objects.
*/
protected removeCircularRefs(obj: JsonObject, seen = new Set()) {
protected removeCircularRefs(obj: JsonObject, seen = new Set()) {
seen.add(obj);
Object.entries(obj).forEach(([key, 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;
}
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.
*/
export class NodeOperationError extends NodeError {
constructor(node: INode, error: Error | string) {
if (typeof error === 'string') {
error = new Error(error);
@ -211,10 +239,16 @@ export class NodeApiError extends NodeError {
constructor(
node: INode,
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);
if (error.error) { // only for request library error
if (error.error) {
// only for request library error
this.removeCircularRefs(error.error as JsonObject);
}
if (message) {
@ -236,11 +270,17 @@ export class NodeApiError extends NodeError {
}
private setDescriptionFromXml(xml: string) {
// eslint-disable-next-line @typescript-eslint/naming-convention
parseString(xml, { explicitArray: false }, (_, result) => {
if (!result) return;
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 {
IContextObject,
INode,
@ -17,13 +31,7 @@ import {
WebhookHttpMethod,
} from './Interfaces';
import {
Workflow
} from './Workflow';
import { get, isEqual } from 'lodash';
import { Workflow } from './Workflow';
/**
* Gets special parameters which should be added to nodeTypes depending
@ -99,12 +107,7 @@ export function getSpecialNodeParameters(nodeType: INodeType) {
},
displayOptions: {
hide: {
mode: [
'custom',
'everyHour',
'everyMinute',
'everyX',
],
mode: ['custom', 'everyHour', 'everyMinute', 'everyX'],
},
},
default: 14,
@ -120,11 +123,7 @@ export function getSpecialNodeParameters(nodeType: INodeType) {
},
displayOptions: {
hide: {
mode: [
'custom',
'everyMinute',
'everyX',
],
mode: ['custom', 'everyMinute', 'everyX'],
},
},
default: 0,
@ -136,9 +135,7 @@ export function getSpecialNodeParameters(nodeType: INodeType) {
type: 'number',
displayOptions: {
show: {
mode: [
'everyMonth',
],
mode: ['everyMonth'],
},
},
typeOptions: {
@ -154,9 +151,7 @@ export function getSpecialNodeParameters(nodeType: INodeType) {
type: 'options',
displayOptions: {
show: {
mode: [
'everyWeek',
],
mode: ['everyWeek'],
},
},
options: [
@ -198,13 +193,12 @@ export function getSpecialNodeParameters(nodeType: INodeType) {
type: 'string',
displayOptions: {
show: {
mode: [
'custom',
],
mode: ['custom'],
},
},
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',
@ -216,9 +210,7 @@ export function getSpecialNodeParameters(nodeType: INodeType) {
},
displayOptions: {
show: {
mode: [
'everyX',
],
mode: ['everyX'],
},
},
default: 2,
@ -230,9 +222,7 @@ export function getSpecialNodeParameters(nodeType: INodeType) {
type: 'options',
displayOptions: {
show: {
mode: [
'everyX',
],
mode: ['everyX'],
},
},
options: [
@ -258,7 +248,6 @@ export function getSpecialNodeParameters(nodeType: INodeType) {
return [];
}
/**
* 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
* @returns
*/
export function displayParameter(nodeValues: INodeParameters, parameter: INodeProperties | INodeCredentialDescription, nodeValuesRoot?: INodeParameters) {
export function displayParameter(
nodeValues: INodeParameters,
parameter: INodeProperties | INodeCredentialDescription,
nodeValuesRoot?: INodeParameters,
) {
if (!parameter.displayOptions) {
return true;
}
@ -277,7 +270,8 @@ export function displayParameter(nodeValues: INodeParameters, parameter: INodePr
nodeValuesRoot = nodeValuesRoot || nodeValues;
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) {
// All the defined rules have to match to display parameter
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);
}
if (values.some(v => (typeof v) === 'string' && (v as string).charAt(0) === '=')) {
if (values.some((v) => typeof v === 'string' && v.charAt(0) === '=')) {
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;
}
}
@ -324,7 +321,10 @@ export function displayParameter(nodeValues: INodeParameters, parameter: INodePr
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;
}
}
@ -333,7 +333,6 @@ export function displayParameter(nodeValues: INodeParameters, parameter: INodePr
return true;
}
/**
* Returns if the given parameter should be displayed or not considering the path
* to the properties
@ -345,28 +344,25 @@ export function displayParameter(nodeValues: INodeParameters, parameter: INodePr
* @param {string} path The path to the property
* @returns
*/
export function displayParameterPath(nodeValues: INodeParameters, parameter: INodeProperties | INodeCredentialDescription, path: string) {
export function displayParameterPath(
nodeValues: INodeParameters,
parameter: INodeProperties | INodeCredentialDescription,
path: string,
) {
let resolvedNodeValues = nodeValues;
if (path !== '') {
resolvedNodeValues = get(
nodeValues,
path,
) as INodeParameters;
resolvedNodeValues = get(nodeValues, path) as INodeParameters;
}
// Get the root parameter data
let nodeValuesRoot = nodeValues;
if (path && path.split('.').indexOf('parameters') === 0) {
nodeValuesRoot = get(
nodeValues,
'parameters',
) as INodeParameters;
nodeValuesRoot = get(nodeValues, 'parameters') as INodeParameters;
}
return displayParameter(resolvedNodeValues, parameter, nodeValuesRoot);
}
/**
* 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
* @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) {
// TODO: Should not happen leave it for test now
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) {
// eslint-disable-next-line no-param-reassign
runExecutionData.executionData.contextData[key] = {};
}
return runExecutionData.executionData.contextData[key];
}
/**
* Returns which parameters are dependent on which
*
@ -409,7 +409,9 @@ export function getContext(runExecutionData: IRunExecutionData, type: string, no
* @param {INodeProperties[]} nodePropertiesArray
* @returns {IParameterDependencies}
*/
export function getParamterDependencies(nodePropertiesArray: INodeProperties[]): IParameterDependencies {
export function getParamterDependencies(
nodePropertiesArray: INodeProperties[],
): IParameterDependencies {
const dependencies: IParameterDependencies = {};
let displayRule: string;
@ -436,7 +438,6 @@ export function getParamterDependencies(nodePropertiesArray: INodeProperties[]):
return dependencies;
}
/**
* Returns in which order the parameters should be resolved
* to have the parameters available they depend on
@ -446,7 +447,10 @@ export function getParamterDependencies(nodePropertiesArray: INodeProperties[]):
* @param {IParameterDependencies} parameterDependencies
* @returns {number[]}
*/
export function getParameterResolveOrder(nodePropertiesArray: INodeProperties[], parameterDependencies: IParameterDependencies): number[] {
export function getParamterResolveOrder(
nodePropertiesArray: INodeProperties[],
parameterDependencies: IParameterDependencies,
): number[] {
const executionOrder: number[] = [];
const indexToResolve = Array.from({ length: nodePropertiesArray.length }, (v, k) => k);
const resolvedParamters: string[] = [];
@ -457,7 +461,7 @@ export function getParameterResolveOrder(nodePropertiesArray: INodeProperties[],
let lastIndexLength = indexToResolve.length;
let lastIndexReduction = -1;
let iterations = 0 ;
let iterations = 0;
while (indexToResolve.length !== 0) {
iterations += 1;
@ -495,7 +499,9 @@ export function getParameterResolveOrder(nodePropertiesArray: INodeProperties[],
}
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;
}
@ -503,7 +509,6 @@ export function getParameterResolveOrder(nodePropertiesArray: INodeProperties[],
return executionOrder;
}
/**
* Returns the node parameter values. Depending on the settings it either just returns the none
* 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
* @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) {
parameterDependencies = getParamterDependencies(nodePropertiesArray);
}
@ -541,27 +556,43 @@ export function getNodeParameters(nodePropertiesArray: INodeProperties[], nodeVa
const nodeParametersFull: INodeParameters = {};
let nodeValuesDisplayCheck = nodeParametersFull;
if (dataIsResolved !== true && returnNoneDisplayed === false) {
nodeValuesDisplayCheck = getNodeParameters(nodePropertiesArray, nodeValues, true, true, true, true, nodeValuesRoot, parentType, parameterDependencies) as INodeParameters;
if (!dataIsResolved && !returnNoneDisplayed) {
nodeValuesDisplayCheck = getNodeParameters(
nodePropertiesArray,
nodeValues,
true,
true,
true,
true,
nodeValuesRoot,
parentType,
parameterDependencies,
) as INodeParameters;
}
nodeValuesRoot = nodeValuesRoot || nodeValuesDisplayCheck;
// Go through the parameters in order of their dependencies
const parameterItterationOrderIndex = getParameterResolveOrder(nodePropertiesArray, parameterDependencies);
const parameterItterationOrderIndex = getParamterResolveOrder(
nodePropertiesArray,
parameterDependencies,
);
for (const parameterIndex of parameterItterationOrderIndex) {
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
continue;
}
if (returnNoneDisplayed === false && !displayParameter(nodeValuesDisplayCheck, nodeProperties, nodeValuesRoot)) {
if (returnNoneDisplayed === false) {
continue;
}
if (returnDefaults === false) {
if (
!returnNoneDisplayed &&
!displayParameter(nodeValuesDisplayCheck, nodeProperties, nodeValuesRoot)
) {
if (!returnNoneDisplayed || !returnDefaults) {
continue;
}
}
@ -575,19 +606,27 @@ export function getNodeParameters(nodePropertiesArray: INodeProperties[], nodeVa
}
}
if (returnDefaults === true) {
if (returnDefaults) {
// Set also when it has the default value
if (['boolean', 'number', 'options'].includes(nodeProperties.type)) {
// Boolean, numbers and options are special as false and 0 are valid values
// 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 {
nodeParameters[nodeProperties.name] = nodeValues[nodeProperties.name] || nodeProperties.default;
nodeParameters[nodeProperties.name] =
nodeValues[nodeProperties.name] || nodeProperties.default;
}
nodeParametersFull[nodeProperties.name] = nodeParameters[nodeProperties.name];
} else if ((nodeValues[nodeProperties.name] !== nodeProperties.default && typeof nodeValues[nodeProperties.name] !== 'object') ||
(typeof nodeValues[nodeProperties.name] === 'object' && !isEqual(nodeValues[nodeProperties.name], nodeProperties.default)) ||
(nodeValues[nodeProperties.name] !== undefined && parentType === 'collection')) {
} else if (
(nodeValues[nodeProperties.name] !== nodeProperties.default &&
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
nodeParameters[nodeProperties.name] = nodeValues[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.
continue;
}
@ -605,16 +644,21 @@ export function getNodeParameters(nodePropertiesArray: INodeProperties[], nodeVa
if (nodeProperties.type === '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
// Return directly the values like they are
if (nodeValues[nodeProperties.name] !== undefined) {
nodeParameters[nodeProperties.name] = nodeValues[nodeProperties.name];
} else if (returnDefaults === true) {
} else if (returnDefaults) {
// Does not have values defined but defaults should be returned
if (Array.isArray(nodeProperties.default)) {
nodeParameters[nodeProperties.name] = JSON.parse(JSON.stringify(nodeProperties.default));
nodeParameters[nodeProperties.name] = JSON.parse(
JSON.stringify(nodeProperties.default),
);
} else {
// 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
@ -622,20 +666,27 @@ export function getNodeParameters(nodePropertiesArray: INodeProperties[], nodeVa
}
}
nodeParametersFull[nodeProperties.name] = nodeParameters[nodeProperties.name];
} else {
if (nodeValues[nodeProperties.name] !== undefined) {
// 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);
} else if (nodeValues[nodeProperties.name] !== undefined) {
// 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,
);
if (tempNodeParameters !== null) {
nodeParameters[nodeProperties.name] = tempNodeParameters;
nodeParametersFull[nodeProperties.name] = nodeParameters[nodeProperties.name];
}
} else if (returnDefaults === true) {
// Does not have values defined but defaults should be returned
nodeParameters[nodeProperties.name] = JSON.parse(JSON.stringify(nodeProperties.default));
if (tempNodeParameters !== null) {
nodeParameters[nodeProperties.name] = tempNodeParameters;
nodeParametersFull[nodeProperties.name] = nodeParameters[nodeProperties.name];
}
} else if (returnDefaults) {
// Does not have values defined but defaults should be returned
nodeParameters[nodeProperties.name] = JSON.parse(JSON.stringify(nodeProperties.default));
nodeParametersFull[nodeProperties.name] = nodeParameters[nodeProperties.name];
}
} else if (nodeProperties.type === 'fixedCollection') {
// Is fixedCollection
@ -646,7 +697,7 @@ export function getNodeParameters(nodePropertiesArray: INodeProperties[], nodeVa
let nodePropertyOptions: INodePropertyCollection | undefined;
let propertyValues = nodeValues[nodeProperties.name];
if (returnDefaults === true) {
if (returnDefaults) {
if (propertyValues === undefined) {
propertyValues = JSON.parse(JSON.stringify(nodeProperties.default));
}
@ -654,20 +705,39 @@ export function getNodeParameters(nodePropertiesArray: INodeProperties[], nodeVa
// Iterate over all collections
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
const tempArrayValue: INodeParameters[] = [];
// Iterate over all items as it contains multiple ones
for (const nodeValue of (propertyValues as INodeParameters)[itemName] as INodeParameters[]) {
nodePropertyOptions = nodeProperties!.options!.find((nodePropertyOptions) => nodePropertyOptions.name === itemName) as INodePropertyCollection;
for (const nodeValue of (propertyValues as INodeParameters)[
itemName
] as INodeParameters[]) {
nodePropertyOptions = nodeProperties.options!.find(
// eslint-disable-next-line @typescript-eslint/no-shadow
(nodePropertyOptions) => nodePropertyOptions.name === itemName,
) as INodePropertyCollection;
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!;
tempValue = getNodeParameters(tempNodePropertiesArray, nodeValue as INodeParameters, returnDefaults, returnNoneDisplayed, false, false, nodeValuesRoot, nodeProperties.type);
tempNodePropertiesArray = nodePropertyOptions.values!;
tempValue = getNodeParameters(
tempNodePropertiesArray,
nodeValue,
returnDefaults,
returnNoneDisplayed,
false,
false,
nodeValuesRoot,
nodeProperties.type,
);
if (tempValue !== null) {
tempArrayValue.push(tempValue);
}
@ -678,11 +748,23 @@ export function getNodeParameters(nodePropertiesArray: INodeProperties[], nodeVa
tempNodeParameters = {};
// 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) {
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) {
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
if (returnDefaults === true) {
if (returnDefaults) {
// Set also when it has the default value
if (collectionValues === undefined) {
nodeParameters[nodeProperties.name] = JSON.parse(JSON.stringify(nodeProperties.default));
nodeParameters[nodeProperties.name] = JSON.parse(
JSON.stringify(nodeProperties.default),
);
} else {
nodeParameters[nodeProperties.name] = collectionValues;
}
@ -717,7 +801,6 @@ export function getNodeParameters(nodePropertiesArray: INodeProperties[], nodeVa
return nodeParameters;
}
/**
* 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]
* @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
const returnData = [];
@ -739,8 +825,6 @@ export async function prepareOutputData(outputData: INodeExecutionData[], output
return returnData;
}
/**
* 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
* @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) {
// Node is disabled so webhooks will also not be enabled
return [];
@ -767,15 +856,21 @@ export function getNodeWebhooks(workflow: Workflow, node: INode, additionalData:
const returnData: IWebhookData[] = [];
for (const webhookDescription of nodeType.description.webhooks) {
if (ignoreRestartWehbooks === true && webhookDescription.restartWebhook === true) {
if (ignoreRestartWehbooks && webhookDescription.restartWebhook === true) {
continue;
}
let nodeWebhookPath = workflow.expression.getSimpleParameterValue(node, webhookDescription['path'], mode, {});
let nodeWebhookPath = workflow.expression.getSimpleParameterValue(
node,
webhookDescription.path,
mode,
{},
);
if (nodeWebhookPath === undefined) {
// 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;
}
@ -788,15 +883,35 @@ export function getNodeWebhooks(workflow: Workflow, node: INode, additionalData:
nodeWebhookPath = nodeWebhookPath.slice(0, -1);
}
const isFullPath: boolean = workflow.expression.getSimpleParameterValue(node, webhookDescription['isFullPath'], 'internal', {}, false) as boolean;
const restartWebhook: boolean = workflow.expression.getSimpleParameterValue(node, webhookDescription['restartWebhook'], 'internal', {}, false) as boolean;
const isFullPath: boolean = workflow.expression.getSimpleParameterValue(
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 httpMethod = workflow.expression.getSimpleParameterValue(node, webhookDescription['httpMethod'], mode, {}, 'GET');
const httpMethod = workflow.expression.getSimpleParameterValue(
node,
webhookDescription.httpMethod,
mode,
{},
'GET',
);
if (httpMethod === undefined) {
// 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;
}
@ -838,10 +953,17 @@ export function getNodeWebhooksBasic(workflow: Workflow, node: INode): IWebhookD
const returnData: IWebhookData[] = [];
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) {
// 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;
}
@ -854,19 +976,32 @@ export function getNodeWebhooksBasic(workflow: Workflow, node: INode): IWebhookD
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 httpMethod = workflow.expression.getSimpleParameterValue(node, webhookDescription['httpMethod'], mode, {});
const httpMethod = workflow.expression.getSimpleParameterValue(
node,
webhookDescription.httpMethod,
mode,
{},
);
if (httpMethod === undefined) {
// 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;
}
//@ts-ignore
// @ts-ignore
returnData.push({
httpMethod: httpMethod.toString() as WebhookHttpMethod,
node: node.name,
@ -879,7 +1014,6 @@ export function getNodeWebhooksBasic(workflow: Workflow, node: INode): IWebhookD
return returnData;
}
/**
* Returns the webhook path
*
@ -889,11 +1023,18 @@ export function getNodeWebhooksBasic(workflow: Workflow, node: INode): IWebhookD
* @param {string} path
* @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 = '';
if (restartWebhook === true) {
return path;
} else if (node.webhookId === undefined) {
}
if (node.webhookId === undefined) {
webhookPath = `${workflowId}/${encodeURIComponent(node.name.toLowerCase())}/${path}`;
} else {
if (isFullPath === true) {
@ -904,7 +1045,6 @@ export function getNodeWebhookPath(workflowId: string, node: INode, path: string
return webhookPath;
}
/**
* Returns the webhook URL
*
@ -916,7 +1056,13 @@ export function getNodeWebhookPath(workflowId: string, node: INode, path: string
* @param {boolean} isFullPath
* @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) {
// setting this to false to prefix the webhookId
isFullPath = false;
@ -927,7 +1073,6 @@ export function getNodeWebhookUrl(baseUrl: string, workflowId: string, node: INo
return `${baseUrl}/${getNodeWebhookPath(workflowId, node, path, isFullPath)}`;
}
/**
* 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
* @returns {(INodeIssues | null)}
*/
export function getNodeParametersIssues(nodePropertiesArray: INodeProperties[], node: INode): INodeIssues | null {
export function getNodeParametersIssues(
nodePropertiesArray: INodeProperties[],
node: INode,
): INodeIssues | null {
const foundIssues: INodeIssues = {};
let propertyIssues: INodeIssues;
@ -957,7 +1105,6 @@ export function getNodeParametersIssues(nodePropertiesArray: INodeProperties[],
return foundIssues;
}
/**
* Returns the issues of the node as string
*
@ -973,12 +1120,10 @@ export function nodeIssuesToString(issues: INodeIssues, node?: INode): string[]
nodeIssues.push(`Execution Error.`);
}
const objectProperties = [
'parameters',
'credentials',
];
const objectProperties = ['parameters', 'credentials'];
let issueText: string, parameterName: string;
let issueText: string;
let parameterName: string;
for (const propertyName of objectProperties) {
if (issues[propertyName] !== undefined) {
for (parameterName of Object.keys(issues[propertyName] as object)) {
@ -1000,7 +1145,6 @@ export function nodeIssuesToString(issues: INodeIssues, node?: INode): string[]
return nodeIssues;
}
/**
* 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 {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
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 === 'dateTime' && value === undefined)) {
(nodeProperties.type === 'dateTime' && value === undefined)
) {
// Parameter is requried but empty
if (foundIssues.parameters === undefined) {
foundIssues.parameters = {};
@ -1022,11 +1172,12 @@ export function addToIssuesIfMissing(foundIssues: INodeIssues, nodeProperties: I
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
*
@ -1036,15 +1187,14 @@ export function addToIssuesIfMissing(foundIssues: INodeIssues, nodeProperties: I
* @param {string} path The path to the properties
* @returns
*/
export function getParameterValueByPath(nodeValues: INodeParameters, parameterName: string, path: string) {
return get(
nodeValues,
path ? path + '.' + parameterName : parameterName,
);
export function getParameterValueByPath(
nodeValues: INodeParameters,
parameterName: string,
path: string,
) {
return get(nodeValues, path ? `${path}.${parameterName}` : parameterName);
}
/**
* 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
* @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 = {};
let value;
@ -1062,11 +1216,15 @@ export function getParameterIssues(nodeProperties: INodeProperties, nodeValues:
if (displayParameterPath(nodeValues, nodeProperties, 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
if (Array.isArray(value)) {
for (const singleValue of value as NodeParameterValue[]) {
addToIssuesIfMissing(foundIssues, nodeProperties, singleValue as NodeParameterValue);
addToIssuesIfMissing(foundIssues, nodeProperties, singleValue);
}
}
} else {
@ -1106,7 +1264,7 @@ export function getParameterIssues(nodeProperties: INodeProperties, nodeValues:
});
}
} else if (nodeProperties.type === 'fixedCollection') {
basePath = basePath ? `${basePath}.` : '' + nodeProperties.name + '.';
basePath = basePath ? `${basePath}.` : `${nodeProperties.name}.`;
let propertyOptions: INodePropertyCollection;
for (propertyOptions of nodeProperties.options as INodePropertyCollection[]) {
@ -1116,14 +1274,18 @@ export function getParameterIssues(nodeProperties: INodeProperties, nodeValues:
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
if (Array.isArray(value)) {
for (let i = 0; i < (value as INodeParameters[]).length; i++) {
for (const option of propertyOptions.values) {
checkChildNodeProperties.push({
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) {
checkChildNodeProperties.push({
basePath: basePath + propertyOptions.name,
data: option as INodeProperties,
data: option,
});
}
}
@ -1146,14 +1308,13 @@ export function getParameterIssues(nodeProperties: INodeProperties, nodeValues:
let propertyIssues;
for (const optionData of checkChildNodeProperties) {
propertyIssues = getParameterIssues(optionData.data as INodeProperties, nodeValues, optionData.basePath);
propertyIssues = getParameterIssues(optionData.data, nodeValues, optionData.basePath);
mergeIssues(foundIssues, propertyIssues);
}
return foundIssues;
}
/**
* Merges multiple NodeIssues together
*
@ -1172,10 +1333,7 @@ export function mergeIssues(destination: INodeIssues, source: INodeIssues | null
destination.execution = true;
}
const objectProperties = [
'parameters',
'credentials',
];
const objectProperties = ['parameters', 'credentials'];
let destinationProperty: INodeIssueObjectProperty;
for (const propertyName of objectProperties) {
@ -1190,7 +1348,10 @@ export function mergeIssues(destination: INodeIssues, source: INodeIssues | null
if (destinationProperty[parameterName] === undefined) {
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
*
@ -1209,10 +1368,13 @@ export function mergeIssues(destination: INodeIssues, source: INodeIssues | null
* @param {INodeProperties[]} mainProperties
* @param {INodeProperties[]} addProperties
*/
export function mergeNodeProperties(mainProperties: INodeProperties[], addProperties: INodeProperties[]): void {
export function mergeNodeProperties(
mainProperties: INodeProperties[],
addProperties: INodeProperties[],
): void {
let existingIndex: number;
for (const property of addProperties) {
existingIndex = mainProperties.findIndex(element => element.name === property.name);
existingIndex = mainProperties.findIndex((element) => element.name === property.name);
if (existingIndex === -1) {
// Property does not exist yet, so add

View File

@ -1,20 +1,35 @@
import {
IDataObject,
IObservableObject,
} from './';
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-shadow */
/* eslint-disable no-param-reassign */
/* eslint-disable no-underscore-dangle */
// eslint-disable-next-line import/no-cycle
import { IDataObject, IObservableObject } from '.';
export interface IObservableOptions {
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;
// Make all the children of target also observeable
// eslint-disable-next-line no-restricted-syntax
for (const key in target) {
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,
});
return new Proxy(target, {
// eslint-disable-next-line @typescript-eslint/no-shadow
deleteProperty(target, name) {
if (parent === undefined) {
// 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) {
if (parent === undefined) {
// If no parent is given mark current data as changed
if (option !== undefined && option.ignoreEmptyOnFirstChild === true && depth === 0
&& target[name.toString()] === undefined && typeof value === 'object' && Object.keys(value).length === 0) {
if (
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 {
(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 {
Expression,
IConnections,
@ -26,20 +37,27 @@ import {
WebhookSetupMethodNames,
WorkflowActivateMode,
WorkflowExecuteMode,
} from './';
} from '.';
import { IConnection, IDataObject, IObservableObject } from './Interfaces';
export class Workflow {
id: string | undefined;
name: string | undefined;
nodes: INodes = {};
connectionsBySourceNode: IConnections;
connectionsByDestinationNode: IConnections;
nodeTypes: INodeTypes;
expression: Expression;
active: boolean;
settings: IWorkflowSettings;
// To save workflow specific static data like for example
@ -47,7 +65,16 @@ export class Workflow {
staticData: IDataObject;
// 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.name = parameters.name;
this.nodeTypes = parameters.nodeTypes;
@ -70,7 +97,12 @@ export class Workflow {
}
// 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 : {};
}
this.connectionsBySourceNode = parameters.connections;
@ -80,15 +112,15 @@ export class Workflow {
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.expression = new Expression(this);
}
/**
* The default connections are by source node. This function rewrites them by destination nodes
* to easily find parent nodes.
@ -140,8 +172,6 @@ export class Workflow {
return returnConnection;
}
/**
* A workflow can only be activated if it has a node which has either triggers
* or webhooks defined.
@ -162,6 +192,7 @@ export class Workflow {
continue;
}
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
if (ignoreNodeTypes !== undefined && ignoreNodeTypes.includes(node.type)) {
continue;
}
@ -173,7 +204,11 @@ export class Workflow {
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.
return true;
}
@ -182,8 +217,6 @@ export class Workflow {
return false;
}
/**
* Checks if everything in the workflow is complete
* and ready to be executed. If it returns null everything
@ -216,7 +249,7 @@ export class Workflow {
typeUnknown: true,
};
} else {
nodeIssues = NodeHelpers.getNodeParametersIssues(nodeType.description.properties!, node);
nodeIssues = NodeHelpers.getNodeParametersIssues(nodeType.description.properties, node);
}
if (nodeIssues !== null) {
@ -231,8 +264,6 @@ export class Workflow {
return workflowIssues;
}
/**
* Returns the static data of the workflow.
* It gets saved with the workflow and will be the same for
@ -249,11 +280,15 @@ export class Workflow {
key = 'global';
} else if (type === 'node') {
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}`;
} 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) {
@ -265,8 +300,6 @@ export class Workflow {
return this.staticData[key] as IDataObject;
}
/**
* Returns all the trigger nodes in the workflow.
*
@ -274,10 +307,9 @@ export class Workflow {
* @memberof Workflow
*/
getTriggerNodes(): INode[] {
return this.queryNodes((nodeType: INodeType) => !!nodeType.trigger );
return this.queryNodes((nodeType: INodeType) => !!nodeType.trigger);
}
/**
* Returns all the poll nodes in the workflow
*
@ -285,10 +317,9 @@ export class Workflow {
* @memberof Workflow
*/
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
* checkFunction return true
@ -321,8 +352,6 @@ export class Workflow {
return returnNodes;
}
/**
* Returns the node with the given name if it exists else null
*
@ -338,7 +367,6 @@ export class Workflow {
return null;
}
/**
* Renames nodes in expressions
*
@ -348,7 +376,11 @@ export class Workflow {
* @returns {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[])}
* @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') {
// Reached the actual value
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
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)) {
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) {
returnArray.push(this.renameNodeInExpressions(currentValue, currentName, newName));
@ -379,17 +416,21 @@ export class Workflow {
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 || {})) {
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;
}
/**
* Rename a node in the workflow
*
@ -398,7 +439,6 @@ export class Workflow {
* @memberof Workflow
*/
renameNode(currentName: string, newName: string) {
// Rename the node itself
if (this.nodes[currentName] !== undefined) {
this.nodes[newName] = this.nodes[currentName];
@ -409,7 +449,11 @@ export class Workflow {
// Update the expressions which reference the node
// with its old name
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
@ -419,12 +463,21 @@ export class Workflow {
}
// 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 (type of Object.keys(this.connectionsBySourceNode[sourceNode])) {
for (sourceIndex of Object.keys(this.connectionsBySourceNode[sourceNode][type])) {
for (connectionIndex of Object.keys(this.connectionsBySourceNode[sourceNode][type][parseInt(sourceIndex, 10)])) {
connectionData = this.connectionsBySourceNode[sourceNode][type][parseInt(sourceIndex, 10)][parseInt(connectionIndex, 10)];
for (connectionIndex of Object.keys(
this.connectionsBySourceNode[sourceNode][type][parseInt(sourceIndex, 10)],
)) {
connectionData =
this.connectionsBySourceNode[sourceNode][type][parseInt(sourceIndex, 10)][
parseInt(connectionIndex, 10)
];
if (connectionData.node === currentName) {
connectionData.node = newName;
}
@ -434,11 +487,11 @@ export class Workflow {
}
// 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
*
@ -448,7 +501,12 @@ export class Workflow {
* @returns {string[]}
* @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[] = [];
if (this.nodes[nodeName].disabled === false) {
// If the current node is not disabled itself is the highest
@ -467,23 +525,28 @@ export class Workflow {
checkedNodes = checkedNodes || [];
if (checkedNodes!.includes(nodeName)) {
if (checkedNodes.includes(nodeName)) {
// Node got checked already before
return currentHighest;
}
checkedNodes!.push(nodeName);
checkedNodes.push(nodeName);
const returnNodes: string[] = [];
let addNodes: string[];
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 a connection-index is given ignore all other ones
continue;
}
connectionsByIndex = this.connectionsByDestinationNode[nodeName][type][connectionIndex];
// eslint-disable-next-line @typescript-eslint/no-loop-func
connectionsByIndex.forEach((connection) => {
if (checkedNodes!.includes(connection.node)) {
// Node got checked already before
@ -512,8 +575,6 @@ export class Workflow {
return returnNodes;
}
/**
* Returns all the after the given one
*
@ -527,8 +588,6 @@ export class Workflow {
return this.getConnectedNodes(this.connectionsBySourceNode, nodeName, type, depth);
}
/**
* Returns all the nodes before the given one
*
@ -542,8 +601,6 @@ export class Workflow {
return this.getConnectedNodes(this.connectionsByDestinationNode, nodeName, type, depth);
}
/**
* Gets all the nodes which are connected nodes starting from
* the given one
@ -556,7 +613,13 @@ export class Workflow {
* @returns {string[]}
* @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;
const newDepth = depth === -1 ? depth : depth - 1;
if (depth === 0) {
@ -576,12 +639,12 @@ export class Workflow {
checkedNodes = checkedNodes || [];
if (checkedNodes!.includes(nodeName)) {
if (checkedNodes.includes(nodeName)) {
// Node got checked already before
return [];
}
checkedNodes!.push(nodeName);
checkedNodes.push(nodeName);
const returnNodes: string[] = [];
let addNodes: string[];
@ -597,7 +660,13 @@ export class Workflow {
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) {
// Because nodes can have multiple parents it is possible that
@ -620,8 +689,6 @@ export class Workflow {
return returnNodes;
}
/**
* Returns via which output of the parent-node the node
* is connected to.
@ -634,7 +701,13 @@ export class Workflow {
* @returns {(number | undefined)}
* @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);
if (node === null) {
return undefined;
@ -665,12 +738,12 @@ export class Workflow {
checkedNodes = checkedNodes || [];
if (checkedNodes!.includes(nodeName)) {
if (checkedNodes.includes(nodeName)) {
// Node got checked already before
return undefined;
}
checkedNodes!.push(nodeName);
checkedNodes.push(nodeName);
let outputIndex: number | undefined;
for (const connectionsByIndex of this.connectionsByDestinationNode[nodeName][type]) {
@ -679,12 +752,18 @@ export class Workflow {
return connection.index;
}
if (checkedNodes!.includes(connection.node)) {
if (checkedNodes.includes(connection.node)) {
// Node got checked already before so continue with the next one
continue;
}
outputIndex = this.getNodeConnectionOutputIndex(connection.node, parentNodeName, type, newDepth, checkedNodes);
outputIndex = this.getNodeConnectionOutputIndex(
connection.node,
parentNodeName,
type,
newDepth,
checkedNodes,
);
if (outputIndex !== undefined) {
return outputIndex;
@ -695,9 +774,6 @@ export class Workflow {
return undefined;
}
/**
* Returns from which of the given nodes the workflow should get started from
*
@ -713,7 +789,6 @@ export class Workflow {
node = this.nodes[nodeName];
nodeType = this.nodeTypes.getByName(node.type) as INodeType;
if (nodeType.trigger !== undefined || nodeType.poll !== undefined) {
if (node.disabled === true) {
continue;
@ -734,8 +809,6 @@ export class Workflow {
return undefined;
}
/**
* Returns the start node to start the worfklow from
*
@ -744,7 +817,6 @@ export class Workflow {
* @memberof Workflow
*/
getStartNode(destinationNode?: string): INode | undefined {
if (destinationNode) {
// Find the highest parent nodes of the given one
const nodeNames = this.getHighestNode(destinationNode);
@ -769,8 +841,6 @@ export class Workflow {
return this.__getStartNode(Object.keys(this.nodes));
}
/**
* Executes the Webhooks method of the node
*
@ -781,11 +851,17 @@ export class Workflow {
* @returns {(Promise<boolean | undefined>)}
* @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 nodeType = this.nodeTypes.getByName(node.type) as INodeType;
if (nodeType.webhookMethods === undefined) {
return;
}
@ -798,11 +874,19 @@ export class Workflow {
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);
}
/**
* Runs the given trigger node so that it can trigger the workflow
* when the node has data.
@ -814,7 +898,13 @@ export class Workflow {
* @returns {(Promise<ITriggerResponse | undefined>)}
* @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 nodeType = this.nodeTypes.getByName(node.type);
@ -824,29 +914,30 @@ export class Workflow {
}
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') {
// 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
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
triggerResponse!.manualTriggerResponse = new Promise((resolve) => {
// eslint-disable-next-line @typescript-eslint/no-shadow
triggerFunctions.emit = ((resolve) => (data: INodeExecutionData[][]) => {
resolve(data);
})(resolve);
});
return triggerResponse;
} else {
// In all other modes simply start the trigger
return nodeType.trigger!.call(triggerFunctions);
}
// In all other modes simply start the trigger
return nodeType.trigger.call(triggerFunctions);
}
/**
* Runs the given trigger node so that it can trigger the workflow
* when the node has data.
@ -856,7 +947,10 @@ export class Workflow {
* @returns
* @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);
if (nodeType === undefined) {
@ -864,13 +958,14 @@ export class Workflow {
}
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
* workflow should be started or not
@ -882,7 +977,13 @@ export class Workflow {
* @returns {Promise<IWebhookResponseData>}
* @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);
if (nodeType === undefined) {
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.`);
}
const thisArgs = nodeExecuteFunctions.getExecuteWebhookFunctions(this, node, additionalData, mode, webhookData);
const thisArgs = nodeExecuteFunctions.getExecuteWebhookFunctions(
this,
node,
additionalData,
mode,
webhookData,
);
return nodeType.webhook.call(thisArgs);
}
/**
* Executes the given node.
*
@ -908,7 +1014,15 @@ export class Workflow {
* @returns {(Promise<INodeExecutionData[][] | null>)}
* @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 is disabled simply pass the data through
// return NodeRunHelpers.
@ -917,7 +1031,7 @@ export class Workflow {
if (inputData.main[0] === null) {
return undefined;
}
return [(inputData.main[0] as INodeExecutionData[])];
return [inputData.main[0]];
}
return undefined;
}
@ -935,7 +1049,7 @@ export class Workflow {
if (inputData.hasOwnProperty('main') && inputData.main.length > 0) {
// 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) {
@ -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.
// 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.
@ -959,7 +1076,8 @@ export class Workflow {
connectionInputData = connectionInputData.slice(0, 1);
const newInputData: ITaskDataConnections = {};
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);
});
}
@ -970,9 +1088,19 @@ export class Workflow {
const returnPromises: Array<Promise<INodeExecutionData>> = [];
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) {
@ -990,21 +1118,41 @@ export class Workflow {
return [promiseResults];
}
} 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);
} else if (nodeType.poll) {
if (mode === 'manual') {
// 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);
} else {
// In any other mode pass data through as it already contains the result of the poll
return inputData.main as INodeExecutionData[][];
}
// In any other mode pass data through as it already contains the result of the poll
return inputData.main as INodeExecutionData[][];
} else if (nodeType.trigger) {
if (mode === 'manual') {
// 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) {
return null;
@ -1027,11 +1175,9 @@ export class Workflow {
}
return response;
} else {
// For trigger nodes in any mode except "manual" do we simply pass the data through
return inputData.main as INodeExecutionData[][];
}
// For trigger nodes in any mode except "manual" do we simply pass the data through
return inputData.main as INodeExecutionData[][];
} else if (nodeType.webhook) {
// For webhook nodes always simply pass the data through
return inputData.main as INodeExecutionData[][];

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