Merge branch 'n8n-io:master' into Add-schema-registry-into-kafka

This commit is contained in:
Ricardo Georgel 2021-08-31 12:04:43 -03:00 committed by GitHub
commit fa7ab27c94
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
727 changed files with 44318 additions and 10298 deletions

View file

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

3
.eslintignore Normal file
View file

@ -0,0 +1,3 @@
packages/nodes-base
packages/editor-ui
packages/design-system

354
.eslintrc.js Normal file
View file

@ -0,0 +1,354 @@
module.exports = {
env: {
browser: true,
es6: true,
node: true,
},
parser: '@typescript-eslint/parser',
parserOptions: {
project: ['./packages/*/tsconfig.json'],
sourceType: 'module',
},
ignorePatterns: [
'.eslintrc.js',
'**/*.js',
'**/node_modules/**',
'**/dist/**',
'**/test/**',
'**/templates/**',
'**/ormconfig.ts',
'**/migrations/**',
],
extends: [
/**
* Config for typescript-eslint recommended ruleset (without type checking)
*
* https://github.com/typescript-eslint/typescript-eslint/blob/1c1b572c3000d72cfe665b7afbada0ec415e7855/packages/eslint-plugin/src/configs/recommended.ts
*/
'plugin:@typescript-eslint/recommended',
/**
* Config for typescript-eslint recommended ruleset (with type checking)
*
* https://github.com/typescript-eslint/typescript-eslint/blob/1c1b572c3000d72cfe665b7afbada0ec415e7855/packages/eslint-plugin/src/configs/recommended-requiring-type-checking.ts
*/
'plugin:@typescript-eslint/recommended-requiring-type-checking',
/**
* Config for Airbnb style guide for TS, /base to remove React rules
*
* https://github.com/iamturns/eslint-config-airbnb-typescript
* https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb-base/rules
*/
'eslint-config-airbnb-typescript/base',
/**
* Config to disable ESLint rules covered by Prettier
*
* https://github.com/prettier/eslint-config-prettier
*/
'eslint-config-prettier',
],
plugins: [
/**
* Plugin with lint rules for import/export syntax
* https://github.com/import-js/eslint-plugin-import
*/
'eslint-plugin-import',
/**
* @typescript-eslint/eslint-plugin is required by eslint-config-airbnb-typescript
* See step 2: https://github.com/iamturns/eslint-config-airbnb-typescript#2-install-eslint-plugins
*/
'@typescript-eslint',
/**
* Plugin to report formatting violations as lint violations
* https://github.com/prettier/eslint-plugin-prettier
*/
'eslint-plugin-prettier',
],
rules: {
// ******************************************************************
// required by prettier plugin
// ******************************************************************
// The following rule enables eslint-plugin-prettier
// See: https://github.com/prettier/eslint-plugin-prettier#recommended-configuration
'prettier/prettier': 'error',
// The following two rules must be disabled when using eslint-plugin-prettier:
// See: https://github.com/prettier/eslint-plugin-prettier#arrow-body-style-and-prefer-arrow-callback-issue
/**
* https://eslint.org/docs/rules/arrow-body-style
*/
'arrow-body-style': 'off',
/**
* https://eslint.org/docs/rules/prefer-arrow-callback
*/
'prefer-arrow-callback': 'off',
// ******************************************************************
// additions to base ruleset
// ******************************************************************
// ----------------------------------
// ESLint
// ----------------------------------
/**
* https://eslint.org/docs/rules/id-denylist
*/
'id-denylist': [
'error',
'err',
'cb',
'callback',
'any',
'Number',
'number',
'String',
'string',
'Boolean',
'boolean',
'Undefined',
'undefined',
],
// ----------------------------------
// @typescript-eslint
// ----------------------------------
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/array-type.md
*/
'@typescript-eslint/array-type': ['error', { default: 'array-simple' }],
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/ban-ts-comment.md
*/
'@typescript-eslint/ban-ts-comment': 'off',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/ban-types.md
*/
'@typescript-eslint/ban-types': [
'error',
{
types: {
Object: {
message: 'Use object instead',
fixWith: 'object',
},
String: {
message: 'Use string instead',
fixWith: 'string',
},
Boolean: {
message: 'Use boolean instead',
fixWith: 'boolean',
},
Number: {
message: 'Use number instead',
fixWith: 'number',
},
Symbol: {
message: 'Use symbol instead',
fixWith: 'symbol',
},
Function: {
message: [
'The `Function` type accepts any function-like value.',
'It provides no type safety when calling the function, which can be a common source of bugs.',
'It also accepts things like class declarations, which will throw at runtime as they will not be called with `new`.',
'If you are expecting the function to accept certain arguments, you should explicitly define the function shape.',
].join('\n'),
},
},
extendDefaults: false,
},
],
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/consistent-type-assertions.md
*/
'@typescript-eslint/consistent-type-assertions': 'error',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/explicit-member-accessibility.md
*/
'@typescript-eslint/explicit-member-accessibility': ['error', { accessibility: 'no-public' }],
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/member-delimiter-style.md
*/
'@typescript-eslint/member-delimiter-style': [
'error',
{
multiline: {
delimiter: 'semi',
requireLast: true,
},
singleline: {
delimiter: 'semi',
requireLast: false,
},
},
],
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/naming-convention.md
*/
'@typescript-eslint/naming-convention': [
'error',
{
selector: 'default',
format: ['camelCase'],
},
{
selector: 'variable',
format: ['camelCase', 'snake_case', 'UPPER_CASE'],
leadingUnderscore: 'allowSingleOrDouble',
trailingUnderscore: 'allowSingleOrDouble',
},
{
selector: 'property',
format: ['camelCase', 'snake_case'],
leadingUnderscore: 'allowSingleOrDouble',
trailingUnderscore: 'allowSingleOrDouble',
},
{
selector: 'typeLike',
format: ['PascalCase'],
},
{
selector: ['method', 'function'],
format: ['camelCase'],
leadingUnderscore: 'allowSingleOrDouble',
},
],
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-duplicate-imports.md
*/
'@typescript-eslint/no-duplicate-imports': 'error',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-invalid-void-type.md
*/
'@typescript-eslint/no-invalid-void-type': 'error',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-misused-promises.md
*/
'@typescript-eslint/no-misused-promises': ['error', { checksVoidReturn: false }],
/**
* https://eslint.org/docs/1.0.0/rules/no-throw-literal
*/
'@typescript-eslint/no-throw-literal': 'error',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unnecessary-boolean-literal-compare.md
*/
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'error',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unnecessary-qualifier.md
*/
'@typescript-eslint/no-unnecessary-qualifier': 'error',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unused-expressions.md
*/
'@typescript-eslint/no-unused-expressions': 'error',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unused-vars.md
*/
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '_' }],
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md
*/
'@typescript-eslint/prefer-nullish-coalescing': 'error',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-optional-chain.md
*/
'@typescript-eslint/prefer-optional-chain': 'error',
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/promise-function-async.md
*/
'@typescript-eslint/promise-function-async': 'error',
// ----------------------------------
// eslint-plugin-import
// ----------------------------------
/**
* https://github.com/import-js/eslint-plugin-import/blob/master/docs/rules/no-default-export.md
*/
'import/no-default-export': 'error',
/**
* https://github.com/import-js/eslint-plugin-import/blob/master/docs/rules/order.md
*/
'import/order': 'error',
// ******************************************************************
// overrides to base ruleset
// ******************************************************************
// ----------------------------------
// ESLint
// ----------------------------------
/**
* https://eslint.org/docs/rules/class-methods-use-this
*/
'class-methods-use-this': 'off',
/**
* https://eslint.org/docs/rules/eqeqeq
*/
eqeqeq: 'error',
/**
* https://eslint.org/docs/rules/no-plusplus
*/
'no-plusplus': 'off',
/**
* https://eslint.org/docs/rules/object-shorthand
*/
'object-shorthand': 'error',
/**
* https://eslint.org/docs/rules/prefer-const
*/
'prefer-const': 'error',
/**
* https://eslint.org/docs/rules/prefer-spread
*/
'prefer-spread': 'error',
// ----------------------------------
// import
// ----------------------------------
/**
* https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/prefer-default-export.md
*/
'import/prefer-default-export': 'off',
},
};

View file

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

5
.gitignore vendored
View file

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

3
.prettierignore Normal file
View file

@ -0,0 +1,3 @@
packages/nodes-base
packages/editor-ui
packages/design-system

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

@ -37,6 +37,7 @@ The most important directories:
- [/packages/core](/packages/core) - Core code which handles workflow - [/packages/core](/packages/core) - Core code which handles workflow
execution, active webhooks and execution, active webhooks and
workflows workflows
- [/packages/design-system](/packages/design-system) - Vue frontend components
- [/packages/editor-ui](/packages/editor-ui) - Vue frontend workflow editor - [/packages/editor-ui](/packages/editor-ui) - Vue frontend workflow editor
- [/packages/node-dev](/packages/node-dev) - CLI to create new n8n-nodes - [/packages/node-dev](/packages/node-dev) - CLI to create new n8n-nodes
- [/packages/nodes-base](/packages/nodes-base) - Base n8n nodes - [/packages/nodes-base](/packages/nodes-base) - Base n8n nodes

View file

@ -14,6 +14,7 @@ COPY lerna.json .
COPY package.json . COPY package.json .
COPY packages/cli/ ./packages/cli/ COPY packages/cli/ ./packages/cli/
COPY packages/core/ ./packages/core/ COPY packages/core/ ./packages/core/
COPY packages/design-system/ ./packages/design-system/
COPY packages/editor-ui/ ./packages/editor-ui/ COPY packages/editor-ui/ ./packages/editor-ui/
COPY packages/nodes-base/ ./packages/nodes-base/ COPY packages/nodes-base/ ./packages/nodes-base/
COPY packages/workflow/ ./packages/workflow/ COPY packages/workflow/ ./packages/workflow/

View file

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

View file

@ -2,6 +2,49 @@
This list shows all the versions which include breaking changes and how to upgrade. This list shows all the versions which include breaking changes and how to upgrade.
## 0.135.0
### What changed?
The in-node core methods for credentials and binary data have changed.
### When is action necessary?
If you are using custom n8n nodes.
### How to upgrade:
1. The method `this.getCredentials(myNodeCredentials)` is now async. So `await` has to be added in front of it.
Example:
```typescript
// Before 0.135.0:
const credentials = this.getCredentials(credentialTypeName);
// From 0.135.0:
const credentials = await this.getCredentials(myNodeCredentials);
```
2. Binary data should not get accessed directly anymore, instead the method `await this.helpers.getBinaryDataBuffer(itemIndex, binaryPropertyName)` has to be used.
Example:
```typescript
const items = this.getInputData();
for (const i = 0; i < items.length; i++) {
const item = items[i].binary as IBinaryKeyData;
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string;
const binaryData = item[binaryPropertyName] as IBinaryData;
// Before 0.135.0:
const binaryDataBuffer = Buffer.from(binaryData.data, BINARY_ENCODING);
// From 0.135.0:
const binaryDataBuffer = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName);
}
```
## 0.131.0 ## 0.131.0
### What changed? ### What changed?

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,6 @@
/* eslint-disable no-console */
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import * as convict from 'convict'; import * as convict from 'convict';
import * as dotenv from 'dotenv'; import * as dotenv from 'dotenv';
import * as path from 'path'; import * as path from 'path';
@ -6,7 +9,6 @@ import * as core from 'n8n-core';
dotenv.config(); dotenv.config();
const config = convict({ const config = convict({
database: { database: {
type: { type: {
doc: 'Type of database to use', doc: 'Type of database to use',
@ -84,7 +86,6 @@ const config = convict({
env: 'DB_POSTGRESDB_SSL_REJECT_UNAUTHORIZED', env: 'DB_POSTGRESDB_SSL_REJECT_UNAUTHORIZED',
}, },
}, },
}, },
mysqldb: { mysqldb: {
database: { database: {
@ -159,7 +160,6 @@ const config = convict({
}, },
executions: { executions: {
// By default workflows get always executed in their own process. // By default workflows get always executed in their own process.
// If this option gets set to "main" it will run them in the // If this option gets set to "main" it will run them in the
// main-process instead. // main-process instead.
@ -489,6 +489,12 @@ const config = convict({
env: 'N8N_ENDPOINT_WEBHOOK', env: 'N8N_ENDPOINT_WEBHOOK',
doc: 'Path for webhook endpoint', doc: 'Path for webhook endpoint',
}, },
webhookWaiting: {
format: String,
default: 'webhook-waiting',
env: 'N8N_ENDPOINT_WEBHOOK_WAIT',
doc: 'Path for waiting-webhook endpoint',
},
webhookTest: { webhookTest: {
format: String, format: String,
default: 'webhook-test', default: 'webhook-test',
@ -567,7 +573,6 @@ const config = convict({
throw new Error(); throw new Error();
} }
} }
} catch (error) { } catch (error) {
throw new TypeError(`The Nodes to exclude is not a valid Array of strings.`); throw new TypeError(`The Nodes to exclude is not a valid Array of strings.`);
} }
@ -638,7 +643,6 @@ const config = convict({
env: 'N8N_VERSION_NOTIFICATIONS_INFO_URL', env: 'N8N_VERSION_NOTIFICATIONS_INFO_URL',
}, },
}, },
}); });
// Overwrite default configuration with settings which got defined in // Overwrite default configuration with settings which got defined in

View file

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

@ -1,6 +1,6 @@
{ {
"name": "n8n", "name": "n8n",
"version": "0.132.2", "version": "0.136.0",
"description": "n8n Workflow Automation Tool", "description": "n8n Workflow Automation Tool",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
@ -21,14 +21,15 @@
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"dev": "concurrently -k -n \"TypeScript,Node\" -c \"yellow.bold,cyan.bold\" \"npm run watch\" \"nodemon\"", "dev": "concurrently -k -n \"TypeScript,Node\" -c \"yellow.bold,cyan.bold\" \"npm run watch\" \"nodemon\"",
"format": "cd ../.. && node_modules/prettier/bin-prettier.js packages/cli/**/**.ts --write",
"lint": "cd ../.. && node_modules/eslint/bin/eslint.js packages/cli",
"lintfix": "cd ../.. && node_modules/eslint/bin/eslint.js packages/cli --fix",
"postpack": "rm -f oclif.manifest.json", "postpack": "rm -f oclif.manifest.json",
"prepack": "echo \"Building project...\" && rm -rf dist && tsc -b && oclif-dev manifest", "prepack": "echo \"Building project...\" && rm -rf dist && tsc -b && oclif-dev manifest",
"start": "run-script-os", "start": "run-script-os",
"start:default": "cd bin && ./n8n", "start:default": "cd bin && ./n8n",
"start:windows": "cd bin && n8n", "start:windows": "cd bin && n8n",
"test": "jest", "test": "jest",
"tslint": "tslint -p tsconfig.json -c tslint.json",
"tslintfix": "tslint --fix -p tsconfig.json -c tslint.json",
"watch": "tsc --watch", "watch": "tsc --watch",
"typeorm": "ts-node ./node_modules/typeorm/cli.js" "typeorm": "ts-node ./node_modules/typeorm/cli.js"
}, },
@ -77,7 +78,7 @@
"ts-jest": "^26.3.0", "ts-jest": "^26.3.0",
"ts-node": "^8.9.1", "ts-node": "^8.9.1",
"tslint": "^6.1.2", "tslint": "^6.1.2",
"typescript": "~3.9.7" "typescript": "~4.3.5"
}, },
"dependencies": { "dependencies": {
"@oclif/command": "^1.5.18", "@oclif/command": "^1.5.18",
@ -98,8 +99,8 @@
"csrf": "^3.1.0", "csrf": "^3.1.0",
"dotenv": "^8.0.0", "dotenv": "^8.0.0",
"express": "^4.16.4", "express": "^4.16.4",
"fast-glob": "^3.2.5",
"flatted": "^2.0.0", "flatted": "^2.0.0",
"glob-promise": "^3.4.0",
"google-timezones-json": "^1.0.2", "google-timezones-json": "^1.0.2",
"inquirer": "^7.0.1", "inquirer": "^7.0.1",
"json-diff": "^0.5.4", "json-diff": "^0.5.4",
@ -107,11 +108,11 @@
"jwks-rsa": "~1.12.1", "jwks-rsa": "~1.12.1",
"localtunnel": "^2.0.0", "localtunnel": "^2.0.0",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"mysql2": "~2.2.0", "mysql2": "~2.3.0",
"n8n-core": "~0.78.0", "n8n-core": "~0.81.0",
"n8n-editor-ui": "~0.100.0", "n8n-editor-ui": "~0.104.0",
"n8n-nodes-base": "~0.129.1", "n8n-nodes-base": "~0.133.0",
"n8n-workflow": "~0.64.0", "n8n-workflow": "~0.66.0",
"oauth-1.0a": "^2.2.6", "oauth-1.0a": "^2.2.6",
"open": "^7.0.0", "open": "^7.0.0",
"pg": "^8.3.0", "pg": "^8.3.0",
@ -120,7 +121,7 @@
"sqlite3": "^5.0.1", "sqlite3": "^5.0.1",
"sse-channel": "^3.1.1", "sse-channel": "^3.1.1",
"tslib": "1.14.1", "tslib": "1.14.1",
"typeorm": "0.2.34", "typeorm": "^0.2.30",
"winston": "^3.3.3" "winston": "^3.3.3"
}, },
"jest": { "jest": {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable import/no-cycle */
import { import {
ExecutionError, ExecutionError,
ICredentialDataDecryptedObject, ICredentialDataDecryptedObject,
@ -10,15 +12,15 @@ import {
IRunExecutionData, IRunExecutionData,
ITaskData, ITaskData,
IWorkflowBase as IWorkflowBaseWorkflow, IWorkflowBase as IWorkflowBaseWorkflow,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
IWorkflowCredentials, IWorkflowCredentials,
Workflow, Workflow,
WorkflowExecuteMode, WorkflowExecuteMode,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import { IDeferredPromise, WorkflowExecute } from 'n8n-core';
IDeferredPromise, WorkflowExecute,
} from 'n8n-core';
// eslint-disable-next-line import/no-extraneous-dependencies
import * as PCancelable from 'p-cancelable'; import * as PCancelable from 'p-cancelable';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
@ -85,7 +87,7 @@ export interface ITagDb {
} }
export type UsageCount = { export type UsageCount = {
usageCount: number usageCount: number;
}; };
export type ITagWithCountDb = ITagDb & UsageCount; export type ITagWithCountDb = ITagDb & UsageCount;
@ -150,6 +152,7 @@ export interface IExecutionBase {
// Data in regular format with references // Data in regular format with references
export interface IExecutionDb extends IExecutionBase { export interface IExecutionDb extends IExecutionBase {
data: IRunExecutionData; data: IRunExecutionData;
waitTill?: Date;
workflowData?: IWorkflowBase; workflowData?: IWorkflowBase;
} }
@ -163,6 +166,7 @@ export interface IExecutionResponse extends IExecutionBase {
data: IRunExecutionData; data: IRunExecutionData;
retryOf?: string; retryOf?: string;
retrySuccessId?: string; retrySuccessId?: string;
waitTill?: Date;
workflowData: IWorkflowBase; workflowData: IWorkflowBase;
} }
@ -176,6 +180,7 @@ export interface IExecutionFlatted extends IExecutionBase {
export interface IExecutionFlattedDb extends IExecutionBase { export interface IExecutionFlattedDb extends IExecutionBase {
id: number | string; id: number | string;
data: string; data: string;
waitTill?: Date | null;
workflowData: IWorkflowBase; workflowData: IWorkflowBase;
} }
@ -204,13 +209,13 @@ export interface IExecutionsSummary {
mode: WorkflowExecuteMode; mode: WorkflowExecuteMode;
retryOf?: string; retryOf?: string;
retrySuccessId?: string; retrySuccessId?: string;
waitTill?: Date;
startedAt: Date; startedAt: Date;
stoppedAt?: Date; stoppedAt?: Date;
workflowId: string; workflowId: string;
workflowName?: string; workflowName?: string;
} }
export interface IExecutionsCurrentSummary { export interface IExecutionsCurrentSummary {
id: string; id: string;
retryOf?: string; retryOf?: string;
@ -219,7 +224,6 @@ export interface IExecutionsCurrentSummary {
workflowId: string; workflowId: string;
} }
export interface IExecutionDeleteFilter { export interface IExecutionDeleteFilter {
deleteBefore?: Date; deleteBefore?: Date;
filters?: IDataObject; filters?: IDataObject;
@ -236,22 +240,33 @@ export interface IExecutingWorkflowData {
export interface IExternalHooks { export interface IExternalHooks {
credentials?: { credentials?: {
create?: Array<{ (this: IExternalHooksFunctions, credentialsData: ICredentialsEncrypted): Promise<void>; }> create?: Array<{
delete?: Array<{ (this: IExternalHooksFunctions, credentialId: string): Promise<void>; }> (this: IExternalHooksFunctions, credentialsData: ICredentialsEncrypted): Promise<void>;
update?: Array<{ (this: IExternalHooksFunctions, credentialsData: ICredentialsDb): Promise<void>; }> }>;
delete?: Array<{ (this: IExternalHooksFunctions, credentialId: string): Promise<void> }>;
update?: Array<{
(this: IExternalHooksFunctions, credentialsData: ICredentialsDb): Promise<void>;
}>;
}; };
workflow?: { workflow?: {
activate?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb): Promise<void>; }> activate?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb): Promise<void> }>;
create?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowBase): Promise<void>; }> create?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowBase): Promise<void> }>;
delete?: Array<{ (this: IExternalHooksFunctions, workflowId: string): Promise<void>; }> delete?: Array<{ (this: IExternalHooksFunctions, workflowId: string): Promise<void> }>;
execute?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb, mode: WorkflowExecuteMode): Promise<void>; }> execute?: Array<{
update?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb): Promise<void>; }> (
this: IExternalHooksFunctions,
workflowData: IWorkflowDb,
mode: WorkflowExecuteMode,
): Promise<void>;
}>;
update?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb): Promise<void> }>;
}; };
} }
export interface IExternalHooksFileData { export interface IExternalHooksFileData {
[key: string]: { [key: string]: {
[key: string]: Array<(...args: any[]) => Promise<void>>; //tslint:disable-line:no-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: Array<(...args: any[]) => Promise<void>>;
}; };
} }
@ -261,7 +276,8 @@ export interface IExternalHooksFunctions {
export interface IExternalHooksClass { export interface IExternalHooksClass {
init(): Promise<void>; init(): Promise<void>;
run(hookName: string, hookParameters?: any[]): Promise<void>; // tslint:disable-line:no-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
run(hookName: string, hookParameters?: any[]): Promise<void>;
} }
export interface IN8nConfig { export interface IN8nConfig {
@ -291,12 +307,14 @@ export interface IN8nConfigEndpoints {
webhookTest: string; webhookTest: string;
} }
// eslint-disable-next-line import/export
export interface IN8nConfigExecutions { export interface IN8nConfigExecutions {
saveDataOnError: SaveExecutionDataType; saveDataOnError: SaveExecutionDataType;
saveDataOnSuccess: SaveExecutionDataType; saveDataOnSuccess: SaveExecutionDataType;
saveDataManualExecutions: boolean; saveDataManualExecutions: boolean;
} }
// eslint-disable-next-line import/export
export interface IN8nConfigExecutions { export interface IN8nConfigExecutions {
saveDataOnError: SaveExecutionDataType; saveDataOnError: SaveExecutionDataType;
saveDataOnSuccess: SaveExecutionDataType; saveDataOnSuccess: SaveExecutionDataType;
@ -405,13 +423,11 @@ export interface IPushDataNodeExecuteAfter {
nodeName: string; nodeName: string;
} }
export interface IPushDataNodeExecuteBefore { export interface IPushDataNodeExecuteBefore {
executionId: string; executionId: string;
nodeName: string; nodeName: string;
} }
export interface IPushDataTestWebhook { export interface IPushDataTestWebhook {
executionId: string; executionId: string;
workflowId: string; workflowId: string;
@ -428,7 +444,6 @@ export interface IResponseCallbackData {
responseCode?: number; responseCode?: number;
} }
export interface ITransferNodeTypes { export interface ITransferNodeTypes {
[key: string]: { [key: string]: {
className: string; className: string;
@ -436,7 +451,6 @@ export interface ITransferNodeTypes {
}; };
} }
export interface IWorkflowErrorData { export interface IWorkflowErrorData {
[key: string]: IDataObject | string | number | ExecutionError; [key: string]: IDataObject | string | number | ExecutionError;
execution: { execution: {
@ -453,11 +467,11 @@ export interface IWorkflowErrorData {
export interface IProcessMessageDataHook { export interface IProcessMessageDataHook {
hook: string; hook: string;
parameters: any[]; // tslint:disable-line:no-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
parameters: any[];
} }
export interface IWorkflowExecutionDataProcess { export interface IWorkflowExecutionDataProcess {
credentials: IWorkflowCredentials;
destinationNode?: string; destinationNode?: string;
executionMode: WorkflowExecuteMode; executionMode: WorkflowExecuteMode;
executionData?: IRunExecutionData; executionData?: IRunExecutionData;
@ -468,7 +482,6 @@ export interface IWorkflowExecutionDataProcess {
workflowData: IWorkflowBase; workflowData: IWorkflowBase;
} }
export interface IWorkflowExecutionDataProcessWithExecution extends IWorkflowExecutionDataProcess { export interface IWorkflowExecutionDataProcessWithExecution extends IWorkflowExecutionDataProcess {
credentialsOverwrite: ICredentialsOverwrite; credentialsOverwrite: ICredentialsOverwrite;
credentialsTypeData: ICredentialsTypeData; credentialsTypeData: ICredentialsTypeData;

View file

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

View file

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

View file

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

View file

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

View file

@ -1,14 +1,15 @@
import * as Bull from 'bull'; import * as Bull from 'bull';
import * as config from '../config'; import * as config from '../config';
// eslint-disable-next-line import/no-cycle
import { IBullJobData } from './Interfaces'; import { IBullJobData } from './Interfaces';
export class Queue { export class Queue {
private jobQueue: Bull.Queue; private jobQueue: Bull.Queue;
constructor() { constructor() {
const prefix = config.get('queue.bull.prefix') as string; const prefix = config.get('queue.bull.prefix') as string;
const redisOptions = config.get('queue.bull.redis') as object; 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 // quickly reconnect to Redis if Redis crashes or is unreachable
// for some time. With it enabled, worker might take minutes to realize // for some time. With it enabled, worker might take minutes to realize
// redis is back up and resume working. // redis is back up and resume working.
@ -16,25 +17,25 @@ export class Queue {
// @ts-ignore // @ts-ignore
this.jobQueue = new Bull('jobs', { prefix, redis: redisOptions, enableReadyCheck: false }); this.jobQueue = new Bull('jobs', { prefix, redis: redisOptions, enableReadyCheck: false });
} }
async add(jobData: IBullJobData, jobOptions: object): Promise<Bull.Job> { async add(jobData: IBullJobData, jobOptions: object): Promise<Bull.Job> {
return await this.jobQueue.add(jobData,jobOptions); return this.jobQueue.add(jobData, jobOptions);
} }
async getJob(jobId: Bull.JobId): Promise<Bull.Job | null> { async getJob(jobId: Bull.JobId): Promise<Bull.Job | null> {
return await this.jobQueue.getJob(jobId); return this.jobQueue.getJob(jobId);
} }
async getJobs(jobTypes: Bull.JobStatus[]): Promise<Bull.Job[]> { async getJobs(jobTypes: Bull.JobStatus[]): Promise<Bull.Job[]> {
return await this.jobQueue.getJobs(jobTypes); return this.jobQueue.getJobs(jobTypes);
} }
getBullObjectInstance(): Bull.Queue { getBullObjectInstance(): Bull.Queue {
return this.jobQueue; return this.jobQueue;
} }
/** /**
* *
* @param job A Bull.Job instance * @param job A Bull.Job instance
* @returns boolean true if we were able to securely stop the job * @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 // Job is already running so tell it to stop
await job.progress(-1); await job.progress(-1);
return true; return true;
} else {
// Job did not get started yet so remove from queue
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; return false;
} }
} }
@ -62,6 +63,6 @@ export function getInstance(): Queue {
if (activeQueueInstance === undefined) { if (activeQueueInstance === undefined) {
activeQueueInstance = new Queue(); activeQueueInstance = new Queue();
} }
return activeQueueInstance; 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 { Request, Response } from 'express';
import { parse, stringify } from 'flatted'; import { parse, stringify } from 'flatted';
// eslint-disable-next-line import/no-cycle
import { import {
IExecutionDb, IExecutionDb,
IExecutionFlatted, IExecutionFlatted,
IExecutionFlattedDb, IExecutionFlattedDb,
IExecutionResponse, IExecutionResponse,
IWorkflowDb, IWorkflowDb,
} from './'; } from '.';
/** /**
* Special Error which allows to return also an error code and http status code * Special Error which allows to return also an error code and http status code
@ -17,7 +23,6 @@ import {
* @extends {Error} * @extends {Error}
*/ */
export class ResponseError extends Error { export class ResponseError extends Error {
// The HTTP status code of response // The HTTP status code of response
httpStatusCode?: number; httpStatusCode?: number;
@ -35,7 +40,7 @@ export class ResponseError extends Error {
* @param {string} [hint] The error hint to provide a context (webhook related) * @param {string} [hint] The error hint to provide a context (webhook related)
* @memberof ResponseError * @memberof ResponseError
*/ */
constructor(message: string, errorCode?: number, httpStatusCode?: number, hint?:string) { constructor(message: string, errorCode?: number, httpStatusCode?: number, hint?: string) {
super(message); super(message);
this.name = 'ResponseError'; this.name = 'ResponseError';
@ -51,21 +56,23 @@ export class ResponseError extends Error {
} }
} }
export function basicAuthAuthorizationError(resp: Response, realm: string, message?: string) { export function basicAuthAuthorizationError(resp: Response, realm: string, message?: string) {
resp.statusCode = 401; resp.statusCode = 401;
resp.setHeader('WWW-Authenticate', `Basic realm="${realm}"`); resp.setHeader('WWW-Authenticate', `Basic realm="${realm}"`);
resp.json({code: resp.statusCode, message}); resp.json({ code: resp.statusCode, message });
} }
export function jwtAuthAuthorizationError(resp: Response, message?: string) { export function jwtAuthAuthorizationError(resp: Response, message?: string) {
resp.statusCode = 403; resp.statusCode = 403;
resp.json({code: resp.statusCode, message}); resp.json({ code: resp.statusCode, message });
} }
export function sendSuccessResponse(
export function sendSuccessResponse(res: Response, data: any, raw?: boolean, responseCode?: number) { // tslint:disable-line:no-any res: Response,
data: any,
raw?: boolean,
responseCode?: number,
) {
if (responseCode !== undefined) { if (responseCode !== undefined) {
res.status(responseCode); res.status(responseCode);
} }
@ -83,7 +90,6 @@ export function sendSuccessResponse(res: Response, data: any, raw?: boolean, res
} }
} }
export function sendErrorResponse(res: Response, error: ResponseError) { export function sendErrorResponse(res: Response, error: ResponseError) {
let httpStatusCode = 500; let httpStatusCode = 500;
if (error.httpStatusCode) { if (error.httpStatusCode) {
@ -122,7 +128,6 @@ export function sendErrorResponse(res: Response, error: ResponseError) {
res.status(httpStatusCode).json(response); res.status(httpStatusCode).json(response);
} }
/** /**
* A helper function which does not just allow to return Promises it also makes sure that * A helper function which does not just allow to return Promises it also makes sure that
* all the responses have the same format * all the responses have the same format
@ -133,8 +138,7 @@ export function sendErrorResponse(res: Response, error: ResponseError) {
* @returns * @returns
*/ */
export function send(processFunction: (req: Request, res: Response) => Promise<any>) { // tslint:disable-line:no-any export function send(processFunction: (req: Request, res: Response) => Promise<any>) {
return async (req: Request, res: Response) => { return async (req: Request, res: Response) => {
try { try {
const data = await processFunction(req, res); const data = await processFunction(req, res);
@ -148,7 +152,6 @@ export function send(processFunction: (req: Request, res: Response) => Promise<a
}; };
} }
/** /**
* Flattens the Execution data. * Flattens the Execution data.
* As it contains a lot of references which normally would be saved as duplicate data * As it contains a lot of references which normally would be saved as duplicate data
@ -160,32 +163,34 @@ export function send(processFunction: (req: Request, res: Response) => Promise<a
*/ */
export function flattenExecutionData(fullExecutionData: IExecutionDb): IExecutionFlatted { export function flattenExecutionData(fullExecutionData: IExecutionDb): IExecutionFlatted {
// Flatten the data // Flatten the data
const returnData: IExecutionFlatted = Object.assign({}, { const returnData: IExecutionFlatted = {
data: stringify(fullExecutionData.data), data: stringify(fullExecutionData.data),
mode: fullExecutionData.mode, mode: fullExecutionData.mode,
// @ts-ignore
waitTill: fullExecutionData.waitTill,
startedAt: fullExecutionData.startedAt, startedAt: fullExecutionData.startedAt,
stoppedAt: fullExecutionData.stoppedAt, stoppedAt: fullExecutionData.stoppedAt,
finished: fullExecutionData.finished ? fullExecutionData.finished : false, finished: fullExecutionData.finished ? fullExecutionData.finished : false,
workflowId: fullExecutionData.workflowId, workflowId: fullExecutionData.workflowId,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
workflowData: fullExecutionData.workflowData!, workflowData: fullExecutionData.workflowData!,
}); };
if (fullExecutionData.id !== undefined) { if (fullExecutionData.id !== undefined) {
returnData.id = fullExecutionData.id!.toString(); returnData.id = fullExecutionData.id.toString();
} }
if (fullExecutionData.retryOf !== undefined) { if (fullExecutionData.retryOf !== undefined) {
returnData.retryOf = fullExecutionData.retryOf!.toString(); returnData.retryOf = fullExecutionData.retryOf.toString();
} }
if (fullExecutionData.retrySuccessId !== undefined) { if (fullExecutionData.retrySuccessId !== undefined) {
returnData.retrySuccessId = fullExecutionData.retrySuccessId!.toString(); returnData.retrySuccessId = fullExecutionData.retrySuccessId.toString();
} }
return returnData; return returnData;
} }
/** /**
* Unflattens the Execution data. * Unflattens the Execution data.
* *
@ -194,17 +199,17 @@ export function flattenExecutionData(fullExecutionData: IExecutionDb): IExecutio
* @returns {IExecutionResponse} * @returns {IExecutionResponse}
*/ */
export function unflattenExecutionData(fullExecutionData: IExecutionFlattedDb): IExecutionResponse { export function unflattenExecutionData(fullExecutionData: IExecutionFlattedDb): IExecutionResponse {
const returnData: IExecutionResponse = {
const returnData: IExecutionResponse = Object.assign({}, {
id: fullExecutionData.id.toString(), id: fullExecutionData.id.toString(),
workflowData: fullExecutionData.workflowData as IWorkflowDb, workflowData: fullExecutionData.workflowData as IWorkflowDb,
data: parse(fullExecutionData.data), data: parse(fullExecutionData.data),
mode: fullExecutionData.mode, mode: fullExecutionData.mode,
waitTill: fullExecutionData.waitTill ? fullExecutionData.waitTill : undefined,
startedAt: fullExecutionData.startedAt, startedAt: fullExecutionData.startedAt,
stoppedAt: fullExecutionData.stoppedAt, stoppedAt: fullExecutionData.stoppedAt,
finished: fullExecutionData.finished ? fullExecutionData.finished : false, finished: fullExecutionData.finished ? fullExecutionData.finished : false,
workflowId: fullExecutionData.workflowId, workflowId: fullExecutionData.workflowId,
}); };
return returnData; return returnData;
} }

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -0,0 +1,186 @@
/* 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,
Db,
GenericHelpers,
IExecutionFlattedDb,
IExecutionsStopData,
IWorkflowExecutionDataProcess,
ResponseHelper,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
WorkflowCredentials,
WorkflowRunner,
} from '.';
export class WaitTrackerClass {
activeExecutionsInstance: ActiveExecutions.ActiveExecutions;
private waitingExecutions: {
[key: string]: {
executionId: string;
timer: NodeJS.Timeout;
};
} = {};
mainTimer: NodeJS.Timeout;
constructor() {
this.activeExecutionsInstance = ActiveExecutions.getInstance();
// Poll every 60 seconds a list of upcoming executions
this.mainTimer = setInterval(() => {
this.getwaitingExecutions();
}, 60000);
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
const findQuery: FindManyOptions<IExecutionFlattedDb> = {
select: ['id', 'waitTill'],
where: {
waitTill: LessThanOrEqual(new Date(Date.now() + 70000)),
},
order: {
waitTill: 'ASC',
},
};
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)),
);
}
const executions = await Db.collections.Execution!.find(findQuery);
if (executions.length === 0) {
return;
}
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) {
const triggerTime = execution.waitTill!.getTime() - new Date().getTime();
this.waitingExecutions[executionId] = {
executionId,
timer: setTimeout(() => {
this.startExecution(executionId);
}, triggerTime),
};
}
}
}
async stopExecution(executionId: string): Promise<IExecutionsStopData> {
if (this.waitingExecutions[executionId] !== undefined) {
// The waiting execution was already sheduled to execute.
// So stop timer and remove.
clearTimeout(this.waitingExecutions[executionId].timer);
delete this.waitingExecutions[executionId];
}
// Also check in database
const execution = await Db.collections.Execution!.findOne(executionId);
if (execution === undefined || !execution.waitTill) {
throw new Error(`The execution ID "${executionId}" could not be found.`);
}
const fullExecutionData = ResponseHelper.unflattenExecutionData(execution);
// Set in execution in DB as failed and remove waitTill time
const error = new WorkflowOperationError('Workflow-Execution has been canceled!');
fullExecutionData.data.resultData.error = {
...error,
message: error.message,
stack: error.stack,
};
fullExecutionData.stoppedAt = new Date();
fullExecutionData.waitTill = undefined;
await Db.collections.Execution!.update(
executionId,
ResponseHelper.flattenExecutionData(fullExecutionData),
);
return {
mode: fullExecutionData.mode,
startedAt: new Date(fullExecutionData.startedAt),
stoppedAt: fullExecutionData.stoppedAt ? new Date(fullExecutionData.stoppedAt) : undefined,
finished: fullExecutionData.finished,
};
}
startExecution(executionId: string) {
Logger.debug(`Wait tracker resuming execution ${executionId}`, { executionId });
delete this.waitingExecutions[executionId];
(async () => {
// Get the data to execute
const fullExecutionDataFlatted = await Db.collections.Execution!.findOne(executionId);
if (fullExecutionDataFlatted === undefined) {
throw new Error(`The execution with the id "${executionId}" does not exist.`);
}
const fullExecutionData = ResponseHelper.unflattenExecutionData(fullExecutionDataFlatted);
if (fullExecutionData.finished) {
throw new Error('The execution did succeed and can so not be started again.');
}
const data: IWorkflowExecutionDataProcess = {
executionMode: fullExecutionData.mode,
executionData: fullExecutionData.data,
workflowData: fullExecutionData.workflowData,
};
// Start the execution again
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 },
);
});
}
}
let waitTrackerInstance: WaitTrackerClass | undefined;
export function WaitTracker(): WaitTrackerClass {
if (waitTrackerInstance === undefined) {
waitTrackerInstance = new WaitTrackerClass();
}
return waitTrackerInstance;
}

View file

@ -0,0 +1,167 @@
/* 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,
IResponseCallbackData,
IWorkflowDb,
NodeTypes,
ResponseHelper,
WebhookHelpers,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
WorkflowCredentials,
WorkflowExecuteAdditionalData,
} from '.';
export class WaitingWebhooks {
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
req.params = {};
// Remove trailing slash
if (fullPath.endsWith('/')) {
fullPath = fullPath.slice(0, -1);
}
const pathParts = fullPath.split('/');
const executionId = pathParts.shift();
const path = pathParts.join('/');
const execution = await Db.collections.Execution?.findOne(executionId);
if (execution === undefined) {
throw new ResponseHelper.ResponseError(
`The execution "${executionId} does not exist.`,
404,
404,
);
}
const fullExecutionData = ResponseHelper.unflattenExecutionData(execution);
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> {
const executionId = fullExecutionData.id;
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;
// 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;
// Remove waitTill information else the execution would stop
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();
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 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
);
})[0];
if (webhookData === undefined) {
// If no data got found it means that the execution can not be started via a webhook.
// Return 404 because we do not want to give any data if the execution exists or not.
const errorMessage = `The execution "${executionId}" with webhook suffix path "${path}" is not known.`;
throw new ResponseHelper.ResponseError(errorMessage, 404, 404);
}
const workflowStartNode = workflow.getNode(lastNodeExecuted);
if (workflowStartNode === null) {
throw new ResponseHelper.ResponseError('Could not find node to process webhook.', 404, 404);
}
const runExecutionData = fullExecutionData.data;
return new Promise((resolve, reject) => {
const executionMode = 'webhook';
// 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,25 +1,21 @@
/* eslint-disable no-param-reassign */
/* eslint-disable @typescript-eslint/prefer-optional-chain */
/* eslint-disable @typescript-eslint/no-shadow */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable id-denylist */
/* eslint-disable prefer-spread */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable prefer-destructuring */
import * as express from 'express'; import * as express from 'express';
// eslint-disable-next-line import/no-extraneous-dependencies
import { get } from 'lodash'; import { get } from 'lodash';
import { import { BINARY_ENCODING, NodeExecuteFunctions } from 'n8n-core';
ActiveExecutions,
ExternalHooks,
GenericHelpers,
IExecutionDb,
IResponseCallbackData,
IWorkflowDb,
IWorkflowExecutionDataProcess,
ResponseHelper,
WorkflowCredentials,
WorkflowExecuteAdditionalData,
WorkflowHelpers,
WorkflowRunner,
} from './';
import {
BINARY_ENCODING,
NodeExecuteFunctions,
} from 'n8n-core';
import { import {
IBinaryKeyData, IBinaryKeyData,
@ -29,13 +25,28 @@ import {
IRunExecutionData, IRunExecutionData,
IWebhookData, IWebhookData,
IWebhookResponseData, IWebhookResponseData,
IWorkflowDataProxyAdditionalKeys,
IWorkflowExecuteAdditionalData, IWorkflowExecuteAdditionalData,
LoggerProxy as Logger, LoggerProxy as Logger,
NodeHelpers, NodeHelpers,
Workflow, Workflow,
WorkflowExecuteMode, WorkflowExecuteMode,
} from 'n8n-workflow'; } from 'n8n-workflow';
// eslint-disable-next-line import/no-cycle
import {
ActiveExecutions,
GenericHelpers,
IExecutionDb,
IResponseCallbackData,
IWorkflowDb,
IWorkflowExecutionDataProcess,
ResponseHelper,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
WorkflowCredentials,
WorkflowExecuteAdditionalData,
WorkflowHelpers,
WorkflowRunner,
} from '.';
const activeExecutions = ActiveExecutions.getInstance(); const activeExecutions = ActiveExecutions.getInstance();
@ -47,7 +58,12 @@ const activeExecutions = ActiveExecutions.getInstance();
* @param {Workflow} workflow * @param {Workflow} workflow
* @returns {IWebhookData[]} * @returns {IWebhookData[]}
*/ */
export function getWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, destinationNode?: string): IWebhookData[] { export function getWorkflowWebhooks(
workflow: Workflow,
additionalData: IWorkflowExecuteAdditionalData,
destinationNode?: string,
ignoreRestartWehbooks = false,
): IWebhookData[] {
// Check all the nodes in the workflow if they have webhooks // Check all the nodes in the workflow if they have webhooks
const returnData: IWebhookData[] = []; const returnData: IWebhookData[] = [];
@ -63,9 +79,13 @@ export function getWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflo
if (parentNodes !== undefined && !parentNodes.includes(node.name)) { if (parentNodes !== undefined && !parentNodes.includes(node.name)) {
// If parentNodes are given check only them if they have webhooks // If parentNodes are given check only them if they have webhooks
// and no other ones // and no other ones
// eslint-disable-next-line no-continue
continue; continue;
} }
returnData.push.apply(returnData, NodeHelpers.getNodeWebhooks(workflow, node, additionalData)); returnData.push.apply(
returnData,
NodeHelpers.getNodeWebhooks(workflow, node, additionalData, ignoreRestartWehbooks),
);
} }
return returnData; return returnData;
@ -91,22 +111,33 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
return returnData; return returnData;
} }
/**
/** * Executes a webhook
* Executes a webhook *
* * @export
* @export * @param {IWebhookData} webhookData
* @param {IWebhookData} webhookData * @param {IWorkflowDb} workflowData
* @param {IWorkflowDb} workflowData * @param {INode} workflowStartNode
* @param {INode} workflowStartNode * @param {WorkflowExecuteMode} executionMode
* @param {WorkflowExecuteMode} executionMode * @param {(string | undefined)} sessionId
* @param {(string | undefined)} sessionId * @param {express.Request} req
* @param {express.Request} req * @param {express.Response} res
* @param {express.Response} res * @param {((error: Error | null, data: IResponseCallbackData) => void)} responseCallback
* @param {((error: Error | null, data: IResponseCallbackData) => void)} responseCallback * @returns {(Promise<string | undefined>)}
* @returns {(Promise<string | undefined>)} */
*/ export async function executeWebhook(
export async function executeWebhook(workflow: Workflow, webhookData: IWebhookData, workflowData: IWorkflowDb, workflowStartNode: INode, executionMode: WorkflowExecuteMode, sessionId: string | undefined, req: express.Request, res: express.Response, responseCallback: (error: Error | null, data: IResponseCallbackData) => void): Promise<string | undefined> { workflow: Workflow,
webhookData: IWebhookData,
workflowData: IWorkflowDb,
workflowStartNode: INode,
executionMode: WorkflowExecuteMode,
sessionId: string | undefined,
runExecutionData: IRunExecutionData | undefined,
executionId: string | undefined,
req: express.Request,
res: express.Response,
responseCallback: (error: Error | null, data: IResponseCallbackData) => void,
): Promise<string | undefined> {
// Get the nodeType to know which responseMode is set // Get the nodeType to know which responseMode is set
const nodeType = workflow.nodeTypes.getByName(workflowStartNode.type); const nodeType = workflow.nodeTypes.getByName(workflowStartNode.type);
if (nodeType === undefined) { if (nodeType === undefined) {
@ -115,9 +146,25 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
throw new ResponseHelper.ResponseError(errorMessage, 500, 500); throw new ResponseHelper.ResponseError(errorMessage, 500, 500);
} }
const additionalKeys: IWorkflowDataProxyAdditionalKeys = {
$executionId: executionId,
};
// Get the responseMode // Get the responseMode
const responseMode = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseMode'], executionMode, 'onReceived'); const responseMode = workflow.expression.getSimpleParameterValue(
const responseCode = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseCode'], executionMode, 200) as number; workflowStartNode,
webhookData.webhookDescription.responseMode,
executionMode,
additionalKeys,
'onReceived',
);
const responseCode = workflow.expression.getSimpleParameterValue(
workflowStartNode,
webhookData.webhookDescription.responseCode,
executionMode,
additionalKeys,
200,
) as number;
if (!['onReceived', 'lastNode'].includes(responseMode as string)) { if (!['onReceived', 'lastNode'].includes(responseMode as string)) {
// If the mode is not known we error. Is probably best like that instead of using // If the mode is not known we error. Is probably best like that instead of using
@ -129,8 +176,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
} }
// Prepare everything that is needed to run the workflow // Prepare everything that is needed to run the workflow
const credentials = await WorkflowCredentials(workflowData.nodes); const additionalData = await WorkflowExecuteAdditionalData.getBase();
const additionalData = await WorkflowExecuteAdditionalData.getBase(credentials);
// Add the Response and Request so that this data can be accessed in the node // Add the Response and Request so that this data can be accessed in the node
additionalData.httpRequest = req; additionalData.httpRequest = req;
@ -144,7 +190,13 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
let webhookResultData: IWebhookResponseData; let webhookResultData: IWebhookResponseData;
try { try {
webhookResultData = await workflow.runWebhook(webhookData, workflowStartNode, additionalData, NodeExecuteFunctions, executionMode); webhookResultData = await workflow.runWebhook(
webhookData,
workflowStartNode,
additionalData,
NodeExecuteFunctions,
executionMode,
);
} catch (err) { } catch (err) {
// Send error response to webhook caller // Send error response to webhook caller
const errorMessage = 'Workflow Webhook Error: Workflow could not be started!'; const errorMessage = 'Workflow Webhook Error: Workflow could not be started!';
@ -168,29 +220,41 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
noWebhookResponse: true, noWebhookResponse: true,
// Add empty data that it at least tries to "execute" the webhook // Add empty data that it at least tries to "execute" the webhook
// which then so gets the chance to throw the error. // which then so gets the chance to throw the error.
workflowData: [[{json: {}}]], workflowData: [[{ json: {} }]],
}; };
} }
// Save static data if it changed // Save static data if it changed
await WorkflowHelpers.saveStaticData(workflow); await WorkflowHelpers.saveStaticData(workflow);
if (webhookData.webhookDescription['responseHeaders'] !== undefined) { const additionalKeys: IWorkflowDataProxyAdditionalKeys = {
const responseHeaders = workflow.expression.getComplexParameterValue(workflowStartNode, webhookData.webhookDescription['responseHeaders'], executionMode, undefined) as { $executionId: executionId,
entries?: Array<{ };
name: string;
value: string; if (webhookData.webhookDescription.responseHeaders !== undefined) {
}> | 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) { if (responseHeaders !== undefined && responseHeaders.entries !== undefined) {
for (const item of responseHeaders['entries']) { for (const item of responseHeaders.entries) {
res.setHeader(item['name'], item['value']); res.setHeader(item.name, item.value);
} }
} }
} }
if (webhookResultData.noWebhookResponse === true && didSendResponse === false) { if (webhookResultData.noWebhookResponse === true && !didSendResponse) {
// The response got already send // The response got already send
responseCallback(null, { responseCallback(null, {
noWebhookResponse: true, noWebhookResponse: true,
@ -202,7 +266,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
// Workflow should not run // Workflow should not run
if (webhookResultData.webhookResponse !== undefined) { if (webhookResultData.webhookResponse !== undefined) {
// Data to respond with is given // Data to respond with is given
if (didSendResponse === false) { if (!didSendResponse) {
responseCallback(null, { responseCallback(null, {
data: webhookResultData.webhookResponse, data: webhookResultData.webhookResponse,
responseCode, responseCode,
@ -211,7 +275,8 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
} }
} else { } else {
// Send default response // Send default response
if (didSendResponse === false) { // eslint-disable-next-line no-lonely-if
if (!didSendResponse) {
responseCallback(null, { responseCallback(null, {
data: { data: {
message: 'Webhook call got received.', message: 'Webhook call got received.',
@ -226,7 +291,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
// Now that we know that the workflow should run we can return the default response // Now that we know that the workflow should run we can return the default response
// directly if responseMode it set to "onReceived" and a respone should be sent // directly if responseMode it set to "onReceived" and a respone should be sent
if (responseMode === 'onReceived' && didSendResponse === false) { if (responseMode === 'onReceived' && !didSendResponse) {
// Return response directly and do not wait for the workflow to finish // Return response directly and do not wait for the workflow to finish
if (webhookResultData.webhookResponse !== undefined) { if (webhookResultData.webhookResponse !== undefined) {
// Data to respond with is given // Data to respond with is given
@ -248,27 +313,33 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
// Initialize the data of the webhook node // Initialize the data of the webhook node
const nodeExecutionStack: IExecuteData[] = []; const nodeExecutionStack: IExecuteData[] = [];
nodeExecutionStack.push( nodeExecutionStack.push({
{ node: workflowStartNode,
node: workflowStartNode, data: {
data: { main: webhookResultData.workflowData,
main: webhookResultData.workflowData, },
}, });
}
);
const runExecutionData: IRunExecutionData = { runExecutionData =
startData: { runExecutionData ||
}, ({
resultData: { startData: {},
runData: {}, resultData: {
}, runData: {},
executionData: { },
contextData: {}, executionData: {
nodeExecutionStack, contextData: {},
waitingExecution: {}, 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;
}
if (Object.keys(runExecutionDataMerge).length !== 0) { if (Object.keys(runExecutionDataMerge).length !== 0) {
// If data to merge got defined add it to the execution data // If data to merge got defined add it to the execution data
@ -276,7 +347,6 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
} }
const runData: IWorkflowExecutionDataProcess = { const runData: IWorkflowExecutionDataProcess = {
credentials,
executionMode, executionMode,
executionData: runExecutionData, executionData: runExecutionData,
sessionId, sessionId,
@ -285,161 +355,205 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
// Start now to run the workflow // Start now to run the workflow
const workflowRunner = new WorkflowRunner(); const workflowRunner = new WorkflowRunner();
const executionId = await workflowRunner.run(runData, true, !didSendResponse); executionId = await workflowRunner.run(runData, true, !didSendResponse, executionId);
Logger.verbose(`Started execution of workflow "${workflow.name}" from webhook with execution ID ${executionId}`, { executionId }); Logger.verbose(
`Started execution of workflow "${workflow.name}" from webhook with execution ID ${executionId}`,
{ executionId },
);
// Get a promise which resolves when the workflow did execute and send then response // Get a promise which resolves when the workflow did execute and send then response
const executePromise = activeExecutions.getPostExecutePromise(executionId) as Promise<IExecutionDb | undefined>; const executePromise = activeExecutions.getPostExecutePromise(executionId) as Promise<
executePromise.then((data) => { IExecutionDb | undefined
if (data === undefined) { >;
if (didSendResponse === false) { executePromise
responseCallback(null, { .then((data) => {
data: { if (data === undefined) {
message: 'Workflow did execute sucessfully but no data got returned.', if (!didSendResponse) {
}, responseCallback(null, {
responseCode, data: {
}); message: 'Workflow did execute sucessfully but no data got returned.',
didSendResponse = true; },
} responseCode,
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 responseData = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseData'], executionMode, '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.'), {});
didSendResponse = true; didSendResponse = true;
} }
return undefined;
}
data = returnData.data!.main[0]![0].json; const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data);
if (data.data.resultData.error || returnData?.error !== undefined) {
const responsePropertyName = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responsePropertyName'], executionMode, undefined); if (!didSendResponse) {
responseCallback(null, {
if (responsePropertyName !== undefined) { data: {
data = get(data, responsePropertyName as string) as IDataObject; message: 'Workflow did error.',
},
responseCode: 500,
});
} }
didSendResponse = true;
return data;
}
const responseContentType = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseContentType'], executionMode, 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) { const additionalKeys: IWorkflowDataProxyAdditionalKeys = {
// Send the webhook response manually to be able to set the content-type $executionId: executionId,
res.setHeader('Content-Type', responseContentType as string); };
// Returning an object, boolean, number, ... causes problems so make sure to stringify if needed const responseData = workflow.expression.getSimpleParameterValue(
if (data !== null && data !== undefined && ['Buffer', 'String'].includes(data.constructor.name)) { workflowStartNode,
res.end(data); webhookData.webhookDescription.responseData,
} else { executionMode,
res.end(JSON.stringify(data)); 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, { responseCallback(null, {
noWebhookResponse: true, data,
}); responseCode,
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, '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,
}); });
} }
}
didSendResponse = true;
} else { return data;
// Return the JSON data of all the entries })
data = []; .catch((e) => {
for (const entry of returnData.data!.main[0]!) { if (!didSendResponse) {
data.push(entry.json); responseCallback(new Error('There was a problem executing the workflow.'), {});
}
} }
if (didSendResponse === false) { throw new ResponseHelper.ResponseError(e.message, 500, 500);
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);
});
// eslint-disable-next-line consistent-return
return executionId; return executionId;
} catch (e) { } catch (e) {
if (didSendResponse === false) { if (!didSendResponse) {
responseCallback(new Error('There was a problem executing the workflow.'), {}); responseCallback(new Error('There was a problem executing the workflow.'), {});
} }
@ -447,7 +561,6 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
} }
} }
/** /**
* Returns the base URL of the webhooks * Returns the base URL of the webhooks
* *
@ -463,6 +576,9 @@ export function getWebhookBaseUrl() {
// @ts-ignore // @ts-ignore
urlBaseWebhook = process.env.WEBHOOK_TUNNEL_URL || process.env.WEBHOOK_URL; urlBaseWebhook = process.env.WEBHOOK_TUNNEL_URL || process.env.WEBHOOK_URL;
} }
if (!urlBaseWebhook.endsWith('/')) {
urlBaseWebhook += '/';
}
return urlBaseWebhook; return urlBaseWebhook;
} }

View file

@ -1,14 +1,22 @@
/* eslint-disable no-console */
/* eslint-disable consistent-return */
/* eslint-disable @typescript-eslint/restrict-plus-operands */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import * as express from 'express'; import * as express from 'express';
import { import { readFileSync } from 'fs';
readFileSync, import { getConnectionManager } from 'typeorm';
} from 'fs';
import {
getConnectionManager,
} from 'typeorm';
import * as bodyParser from 'body-parser'; import * as bodyParser from 'body-parser';
require('body-parser-xml')(bodyParser); // eslint-disable-next-line import/no-extraneous-dependencies, @typescript-eslint/no-unused-vars
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as compression from 'compression';
// eslint-disable-next-line import/no-extraneous-dependencies
import * as parseUrl from 'parseurl';
// eslint-disable-next-line import/no-cycle
import { import {
ActiveExecutions, ActiveExecutions,
ActiveWorkflowRunner, ActiveWorkflowRunner,
@ -19,123 +27,253 @@ import {
IExternalHooksClass, IExternalHooksClass,
IPackageVersions, IPackageVersions,
ResponseHelper, ResponseHelper,
} from './'; WaitingWebhooks,
} from '.';
import * as compression from 'compression';
import * as config from '../config'; import * as config from '../config';
import * as parseUrl from 'parseurl';
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-call
require('body-parser-xml')(bodyParser);
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function registerProductionWebhooks() { export function registerProductionWebhooks() {
// ----------------------------------------
// Regular Webhooks
// ----------------------------------------
// HEAD webhook requests // HEAD webhook requests
this.app.head(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => { this.app.head(
// Cut away the "/webhook/" to get the registred part of the url `/${this.endpointWebhook}/*`,
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhook.length + 2); 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; let response;
try { try {
response = await this.activeWorkflowRunner.executeWebhook('HEAD', requestUrl, req, res); // eslint-disable-next-line @typescript-eslint/no-unsafe-call
} catch (error) { response = await this.activeWorkflowRunner.executeWebhook('HEAD', requestUrl, req, res);
ResponseHelper.sendErrorResponse(res, error); } catch (error) {
return; ResponseHelper.sendErrorResponse(res, error);
} return;
}
if (response.noWebhookResponse === true) { if (response.noWebhookResponse === true) {
// Nothing else to do as the response got already sent // Nothing else to do as the response got already sent
return; return;
} }
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
}); },
);
// OPTIONS webhook requests // OPTIONS webhook requests
this.app.options(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => { this.app.options(
// Cut away the "/webhook/" to get the registred part of the url `/${this.endpointWebhook}/*`,
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhook.length + 2); 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[]; let allowedMethods: string[];
try { try {
allowedMethods = await this.activeWorkflowRunner.getWebhookMethods(requestUrl); allowedMethods = await this.activeWorkflowRunner.getWebhookMethods(requestUrl);
allowedMethods.push('OPTIONS'); allowedMethods.push('OPTIONS');
// Add custom "Allow" header to satisfy OPTIONS response. // Add custom "Allow" header to satisfy OPTIONS response.
res.append('Allow', allowedMethods); res.append('Allow', allowedMethods);
} catch (error) { } catch (error) {
ResponseHelper.sendErrorResponse(res, error); ResponseHelper.sendErrorResponse(res, error);
return; return;
} }
ResponseHelper.sendSuccessResponse(res, {}, true, 204); ResponseHelper.sendSuccessResponse(res, {}, true, 204);
}); },
);
// GET webhook requests // GET webhook requests
this.app.get(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => { this.app.get(
// Cut away the "/webhook/" to get the registred part of the url `/${this.endpointWebhook}/*`,
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhook.length + 2); 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; let response;
try { try {
response = await this.activeWorkflowRunner.executeWebhook('GET', requestUrl, req, res); response = await this.activeWorkflowRunner.executeWebhook('GET', requestUrl, req, res);
} catch (error) { } catch (error) {
ResponseHelper.sendErrorResponse(res, error); ResponseHelper.sendErrorResponse(res, error);
return; return;
} }
if (response.noWebhookResponse === true) { if (response.noWebhookResponse === true) {
// Nothing else to do as the response got already sent // Nothing else to do as the response got already sent
return; return;
} }
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
}); },
);
// POST webhook requests // POST webhook requests
this.app.post(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => { this.app.post(
// Cut away the "/webhook/" to get the registred part of the url `/${this.endpointWebhook}/*`,
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhook.length + 2); 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; let response;
try { try {
response = await this.activeWorkflowRunner.executeWebhook('POST', requestUrl, req, res); response = await this.activeWorkflowRunner.executeWebhook('POST', requestUrl, req, res);
} catch (error) { } catch (error) {
ResponseHelper.sendErrorResponse(res, error); ResponseHelper.sendErrorResponse(res, error);
return; return;
} }
if (response.noWebhookResponse === true) { if (response.noWebhookResponse === true) {
// Nothing else to do as the response got already sent // Nothing else to do as the response got already sent
return; return;
} }
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
}); },
);
// ----------------------------------------
// Waiting Webhooks
// ----------------------------------------
const waitingWebhooks = new WaitingWebhooks();
// HEAD webhook-waiting requests
this.app.head(
`/${this.endpointWebhookWaiting}/*`,
async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook-waiting/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
this.endpointWebhookWaiting.length + 2,
);
let response;
try {
response = await waitingWebhooks.executeWebhook('HEAD', requestUrl, req, res);
} catch (error) {
ResponseHelper.sendErrorResponse(res, error);
return;
}
if (response.noWebhookResponse === true) {
// Nothing else to do as the response got already sent
return;
}
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
},
);
// GET webhook-waiting requests
this.app.get(
`/${this.endpointWebhookWaiting}/*`,
async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook-waiting/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
this.endpointWebhookWaiting.length + 2,
);
let response;
try {
response = await waitingWebhooks.executeWebhook('GET', requestUrl, req, res);
} catch (error) {
ResponseHelper.sendErrorResponse(res, error);
return;
}
if (response.noWebhookResponse === true) {
// Nothing else to do as the response got already sent
return;
}
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
},
);
// POST webhook-waiting requests
this.app.post(
`/${this.endpointWebhookWaiting}/*`,
async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook-waiting/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
this.endpointWebhookWaiting.length + 2,
);
let response;
try {
response = await waitingWebhooks.executeWebhook('POST', requestUrl, req, res);
} catch (error) {
ResponseHelper.sendErrorResponse(res, error);
return;
}
if (response.noWebhookResponse === true) {
// Nothing else to do as the response got already sent
return;
}
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
},
);
} }
class App { class App {
app: express.Application; app: express.Application;
activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner; activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner;
endpointWebhook: string; endpointWebhook: string;
endpointWebhookWaiting: string;
endpointPresetCredentials: string; endpointPresetCredentials: string;
externalHooks: IExternalHooksClass; externalHooks: IExternalHooksClass;
saveDataErrorExecution: string; saveDataErrorExecution: string;
saveDataSuccessExecution: string; saveDataSuccessExecution: string;
saveManualExecutions: boolean; saveManualExecutions: boolean;
executionTimeout: number; executionTimeout: number;
maxExecutionTimeout: number; maxExecutionTimeout: number;
timezone: string; timezone: string;
activeExecutionsInstance: ActiveExecutions.ActiveExecutions; activeExecutionsInstance: ActiveExecutions.ActiveExecutions;
versions: IPackageVersions | undefined; versions: IPackageVersions | undefined;
restEndpoint: string; restEndpoint: string;
protocol: string; protocol: string;
sslKey: string; sslKey: string;
sslCert: string; sslCert: string;
presetCredentialsLoaded: boolean; presetCredentialsLoaded: boolean;
constructor() { constructor() {
this.app = express(); this.app = express();
this.endpointWebhook = config.get('endpoints.webhook') as string; this.endpointWebhook = config.get('endpoints.webhook') as string;
this.endpointWebhookWaiting = config.get('endpoints.webhookWaiting') as string;
this.saveDataErrorExecution = config.get('executions.saveDataOnError') as string; this.saveDataErrorExecution = config.get('executions.saveDataOnError') as string;
this.saveDataSuccessExecution = config.get('executions.saveDataOnSuccess') as string; this.saveDataSuccessExecution = config.get('executions.saveDataOnSuccess') as string;
this.saveManualExecutions = config.get('executions.saveDataManualExecutions') as boolean; this.saveManualExecutions = config.get('executions.saveDataManualExecutions') as boolean;
@ -143,22 +281,21 @@ class App {
this.maxExecutionTimeout = config.get('executions.maxTimeout') as number; this.maxExecutionTimeout = config.get('executions.maxTimeout') as number;
this.timezone = config.get('generic.timezone') as string; this.timezone = config.get('generic.timezone') as string;
this.restEndpoint = config.get('endpoints.rest') as string; this.restEndpoint = config.get('endpoints.rest') as string;
this.activeWorkflowRunner = ActiveWorkflowRunner.getInstance(); this.activeWorkflowRunner = ActiveWorkflowRunner.getInstance();
this.activeExecutionsInstance = ActiveExecutions.getInstance(); this.activeExecutionsInstance = ActiveExecutions.getInstance();
this.protocol = config.get('protocol'); this.protocol = config.get('protocol');
this.sslKey = config.get('ssl_key'); this.sslKey = config.get('ssl_key');
this.sslCert = config.get('ssl_cert'); this.sslCert = config.get('ssl_cert');
this.externalHooks = ExternalHooks(); this.externalHooks = ExternalHooks();
this.presetCredentialsLoaded = false; this.presetCredentialsLoaded = false;
this.endpointPresetCredentials = config.get('credentials.overwrite.endpoint') as string; this.endpointPresetCredentials = config.get('credentials.overwrite.endpoint') as string;
} }
/** /**
* Returns the current epoch time * Returns the current epoch time
* *
@ -168,15 +305,13 @@ class App {
getCurrentDate(): Date { getCurrentDate(): Date {
return new Date(); return new Date();
} }
async config(): Promise<void> { async config(): Promise<void> {
this.versions = await GenericHelpers.getVersions(); this.versions = await GenericHelpers.getVersions();
// Compress the response data // Compress the response data
this.app.use(compression()); this.app.use(compression());
// Make sure that each request has the "parsedUrl" parameter // Make sure that each request has the "parsedUrl" parameter
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
(req as ICustomRequest).parsedUrl = parseUrl(req); (req as ICustomRequest).parsedUrl = parseUrl(req);
@ -184,123 +319,135 @@ class App {
req.rawBody = Buffer.from('', 'base64'); req.rawBody = Buffer.from('', 'base64');
next(); next();
}); });
// Support application/json type post data // Support application/json type post data
this.app.use(bodyParser.json({ this.app.use(
limit: '16mb', verify: (req, res, buf) => { bodyParser.json({
// @ts-ignore limit: '16mb',
req.rawBody = buf; verify: (req, res, buf) => {
}, // @ts-ignore
})); req.rawBody = buf;
},
}),
);
// Support application/xml type post data // Support application/xml type post data
// @ts-ignore this.app.use(
this.app.use(bodyParser.xml({ // @ts-ignore
limit: '16mb', xmlParseOptions: { bodyParser.xml({
normalize: true, // Trim whitespace inside text nodes limit: '16mb',
normalizeTags: true, // Transform tags to lowercase xmlParseOptions: {
explicitArray: false, // Only put properties in array if length > 1 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) => {
//support application/x-www-form-urlencoded post data // @ts-ignore
this.app.use(bodyParser.urlencoded({ extended: false, req.rawBody = buf;
verify: (req, res, buf) => { },
// @ts-ignore }),
req.rawBody = buf; );
},
})); // support application/x-www-form-urlencoded post data
this.app.use(
if (process.env['NODE_ENV'] !== 'production') { bodyParser.urlencoded({
extended: false,
verify: (req, res, buf) => {
// @ts-ignore
req.rawBody = buf;
},
}),
);
if (process.env.NODE_ENV !== 'production') {
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
// Allow access also from frontend when developing // Allow access also from frontend when developing
res.header('Access-Control-Allow-Origin', 'http://localhost:8080'); res.header('Access-Control-Allow-Origin', 'http://localhost:8080');
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, sessionid'); res.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept, sessionid',
);
next(); next();
}); });
} }
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
if (Db.collections.Workflow === null) { if (Db.collections.Workflow === null) {
const error = new ResponseHelper.ResponseError('Database is not ready!', undefined, 503); const error = new ResponseHelper.ResponseError('Database is not ready!', undefined, 503);
return ResponseHelper.sendErrorResponse(res, error); return ResponseHelper.sendErrorResponse(res, error);
} }
next(); next();
}); });
// ---------------------------------------- // ----------------------------------------
// Healthcheck // Healthcheck
// ---------------------------------------- // ----------------------------------------
// Does very basic health check // Does very basic health check
this.app.get('/healthz', async (req: express.Request, res: express.Response) => { this.app.get('/healthz', async (req: express.Request, res: express.Response) => {
const connection = getConnectionManager().get();
const connectionManager = getConnectionManager();
try {
if (connectionManager.connections.length === 0) { if (!connection.isConnected) {
const error = new ResponseHelper.ResponseError('No Database connection found!', undefined, 503); // 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); return ResponseHelper.sendErrorResponse(res, error);
} }
if (connectionManager.connections[0].isConnected === false) {
// Connection is not active
const error = new ResponseHelper.ResponseError('Database connection not active!', undefined, 503);
return ResponseHelper.sendErrorResponse(res, error);
}
// Everything fine // Everything fine
const responseData = { const responseData = {
status: 'ok', status: 'ok',
}; };
ResponseHelper.sendSuccessResponse(res, responseData, true, 200); ResponseHelper.sendSuccessResponse(res, responseData, true, 200);
}); });
registerProductionWebhooks.apply(this); registerProductionWebhooks.apply(this);
} }
} }
export async function start(): Promise<void> { export async function start(): Promise<void> {
const PORT = config.get('port'); const PORT = config.get('port');
const ADDRESS = config.get('listen_address'); const ADDRESS = config.get('listen_address');
const app = new App(); const app = new App();
await app.config(); await app.config();
let server; let server;
if (app.protocol === 'https' && app.sslKey && app.sslCert) { if (app.protocol === 'https' && app.sslKey && app.sslCert) {
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
const https = require('https'); const https = require('https');
const privateKey = readFileSync(app.sslKey, 'utf8'); const privateKey = readFileSync(app.sslKey, 'utf8');
const cert = readFileSync(app.sslCert, 'utf8'); const cert = readFileSync(app.sslCert, 'utf8');
const credentials = { key: privateKey, cert }; const credentials = { key: privateKey, cert };
server = https.createServer(credentials, app.app); server = https.createServer(credentials, app.app);
} else { } else {
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
const http = require('http'); const http = require('http');
server = http.createServer(app.app); server = http.createServer(app.app);
} }
server.listen(PORT, ADDRESS, async () => { server.listen(PORT, ADDRESS, async () => {
const versions = await GenericHelpers.getVersions(); const versions = await GenericHelpers.getVersions();
console.log(`n8n ready on ${ADDRESS}, port ${PORT}`); console.log(`n8n ready on ${ADDRESS}, port ${PORT}`);
console.log(`Version: ${versions.cli}`); console.log(`Version: ${versions.cli}`);
await app.externalHooks.run('n8n.ready', [app]); await app.externalHooks.run('n8n.ready', [app]);
}); });
} }

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,3 +1,37 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable no-console */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-shadow */
/* eslint-disable @typescript-eslint/no-floating-promises */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/prefer-optional-chain */
/* eslint-disable no-param-reassign */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable import/no-cycle */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { IProcessMessage, WorkflowExecute } from 'n8n-core';
import {
ExecutionError,
IRun,
IWorkflowBase,
LoggerProxy as Logger,
Workflow,
WorkflowExecuteMode,
WorkflowHooks,
WorkflowOperationError,
} from 'n8n-workflow';
// eslint-disable-next-line import/no-extraneous-dependencies
import * as PCancelable from 'p-cancelable';
import { join as pathJoin } from 'path';
import { fork } from 'child_process';
import * as Bull from 'bull';
import * as config from '../config';
// eslint-disable-next-line import/no-cycle
import { import {
ActiveExecutions, ActiveExecutions,
CredentialsOverwrites, CredentialsOverwrites,
@ -20,38 +54,17 @@ import {
ResponseHelper, ResponseHelper,
WorkflowExecuteAdditionalData, WorkflowExecuteAdditionalData,
WorkflowHelpers, WorkflowHelpers,
} from './'; } from '.';
import {
IProcessMessage,
WorkflowExecute,
} from 'n8n-core';
import {
ExecutionError,
IRun,
IWorkflowBase,
LoggerProxy as Logger,
Workflow,
WorkflowExecuteMode,
WorkflowHooks,
WorkflowOperationError,
} from 'n8n-workflow';
import * as config from '../config';
import * as PCancelable from 'p-cancelable';
import { join as pathJoin } from 'path';
import { fork } from 'child_process';
import * as Bull from 'bull';
import * as Queue from './Queue'; import * as Queue from './Queue';
export class WorkflowRunner { export class WorkflowRunner {
activeExecutions: ActiveExecutions.ActiveExecutions; activeExecutions: ActiveExecutions.ActiveExecutions;
credentialsOverwrites: ICredentialsOverwrite;
push: Push.Push;
jobQueue: Bull.Queue;
credentialsOverwrites: ICredentialsOverwrite;
push: Push.Push;
jobQueue: Bull.Queue;
constructor() { constructor() {
this.push = Push.getInstance(); this.push = Push.getInstance();
@ -65,7 +78,6 @@ export class WorkflowRunner {
} }
} }
/** /**
* The process did send a hook message so execute the appropiate hook * The process did send a hook message so execute the appropiate hook
* *
@ -74,10 +86,10 @@ export class WorkflowRunner {
* @memberof WorkflowRunner * @memberof WorkflowRunner
*/ */
processHookMessage(workflowHooks: WorkflowHooks, hookData: IProcessMessageDataHook) { processHookMessage(workflowHooks: WorkflowHooks, hookData: IProcessMessageDataHook) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
workflowHooks.executeHookFunctions(hookData.hook, hookData.parameters); workflowHooks.executeHookFunctions(hookData.hook, hookData.parameters);
} }
/** /**
* The process did error * The process did error
* *
@ -87,7 +99,13 @@ export class WorkflowRunner {
* @param {string} executionId * @param {string} executionId
* @memberof WorkflowRunner * @memberof WorkflowRunner
*/ */
async processError(error: ExecutionError, startedAt: Date, executionMode: WorkflowExecuteMode, executionId: string, hooks?: WorkflowHooks) { async processError(
error: ExecutionError,
startedAt: Date,
executionMode: WorkflowExecuteMode,
executionId: string,
hooks?: WorkflowHooks,
) {
const fullRunData: IRun = { const fullRunData: IRun = {
data: { data: {
resultData: { resultData: {
@ -123,28 +141,33 @@ export class WorkflowRunner {
* @returns {Promise<string>} * @returns {Promise<string>}
* @memberof WorkflowRunner * @memberof WorkflowRunner
*/ */
async run(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean, realtime?: boolean): Promise<string> { async run(
data: IWorkflowExecutionDataProcess,
loadStaticData?: boolean,
realtime?: boolean,
executionId?: string,
): Promise<string> {
const executionsProcess = config.get('executions.process') as string; const executionsProcess = config.get('executions.process') as string;
const executionsMode = config.get('executions.mode') as string; const executionsMode = config.get('executions.mode') as string;
let executionId: string;
if (executionsMode === 'queue' && data.executionMode !== 'manual') { if (executionsMode === 'queue' && data.executionMode !== 'manual') {
// Do not run "manual" executions in bull because sending events to the // Do not run "manual" executions in bull because sending events to the
// frontend would not be possible // frontend would not be possible
executionId = await this.runBull(data, loadStaticData, realtime); executionId = await this.runBull(data, loadStaticData, realtime, executionId);
} else if (executionsProcess === 'main') { } else if (executionsProcess === 'main') {
executionId = await this.runMainProcess(data, loadStaticData); executionId = await this.runMainProcess(data, loadStaticData, executionId);
} else { } else {
executionId = await this.runSubprocess(data, loadStaticData); executionId = await this.runSubprocess(data, loadStaticData, executionId);
} }
const externalHooks = ExternalHooks(); const externalHooks = ExternalHooks();
if (externalHooks.exists('workflow.postExecute')) { if (externalHooks.exists('workflow.postExecute')) {
this.activeExecutions.getPostExecutePromise(executionId) this.activeExecutions
.getPostExecutePromise(executionId)
.then(async (executionData) => { .then(async (executionData) => {
await externalHooks.run('workflow.postExecute', [executionData, data.workflowData]); await externalHooks.run('workflow.postExecute', [executionData, data.workflowData]);
}) })
.catch(error => { .catch((error) => {
console.error('There was a problem running hook "workflow.postExecute"', error); console.error('There was a problem running hook "workflow.postExecute"', error);
}); });
} }
@ -152,7 +175,6 @@ export class WorkflowRunner {
return executionId; return executionId;
} }
/** /**
* Run the workflow in current process * Run the workflow in current process
* *
@ -162,9 +184,15 @@ export class WorkflowRunner {
* @returns {Promise<string>} * @returns {Promise<string>}
* @memberof WorkflowRunner * @memberof WorkflowRunner
*/ */
async runMainProcess(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean): Promise<string> { async runMainProcess(
data: IWorkflowExecutionDataProcess,
loadStaticData?: boolean,
restartExecutionId?: string,
): Promise<string> {
if (loadStaticData === true && data.workflowData.id) { if (loadStaticData === true && data.workflowData.id) {
data.workflowData.staticData = await WorkflowHelpers.getStaticDataById(data.workflowData.id as string); data.workflowData.staticData = await WorkflowHelpers.getStaticDataById(
data.workflowData.id as string,
);
} }
const nodeTypes = NodeTypes(); const nodeTypes = NodeTypes();
@ -175,64 +203,120 @@ export class WorkflowRunner {
let executionTimeout: NodeJS.Timeout; let executionTimeout: NodeJS.Timeout;
let workflowTimeout = config.get('executions.timeout') as number; // initialize with default let workflowTimeout = config.get('executions.timeout') as number; // initialize with default
if (data.workflowData.settings && data.workflowData.settings.executionTimeout) { if (data.workflowData.settings && data.workflowData.settings.executionTimeout) {
workflowTimeout = data.workflowData.settings!.executionTimeout as number; // preference on workflow setting workflowTimeout = data.workflowData.settings.executionTimeout as number; // preference on workflow setting
} }
if (workflowTimeout > 0) { if (workflowTimeout > 0) {
workflowTimeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number); workflowTimeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number);
} }
const workflow = new Workflow({ id: data.workflowData.id as string | undefined, name: data.workflowData.name, nodes: data.workflowData!.nodes, connections: data.workflowData!.connections, active: data.workflowData!.active, nodeTypes, staticData: data.workflowData!.staticData }); const workflow = new Workflow({
const additionalData = await WorkflowExecuteAdditionalData.getBase(data.credentials, undefined, workflowTimeout <= 0 ? undefined : Date.now() + workflowTimeout * 1000); id: data.workflowData.id as string | undefined,
name: data.workflowData.name,
nodes: data.workflowData.nodes,
connections: data.workflowData.connections,
active: data.workflowData.active,
nodeTypes,
staticData: data.workflowData.staticData,
});
const additionalData = await WorkflowExecuteAdditionalData.getBase(
undefined,
workflowTimeout <= 0 ? undefined : Date.now() + workflowTimeout * 1000,
);
// Register the active execution // Register the active execution
const executionId = await this.activeExecutions.add(data, undefined); 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 },
);
let workflowExecution: PCancelable<IRun>; let workflowExecution: PCancelable<IRun>;
try { try {
Logger.verbose(`Execution for workflow ${data.workflowData.name} was assigned id ${executionId}`, { executionId }); Logger.verbose(
additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId, true); `Execution for workflow ${data.workflowData.name} was assigned id ${executionId}`,
additionalData.sendMessageToUI = WorkflowExecuteAdditionalData.sendMessageToUI.bind({sessionId: data.sessionId}); { executionId },
);
additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(
data,
executionId,
true,
);
additionalData.sendMessageToUI = WorkflowExecuteAdditionalData.sendMessageToUI.bind({
sessionId: data.sessionId,
});
if (data.executionData !== undefined) { if (data.executionData !== undefined) {
Logger.debug(`Execution ID ${executionId} had Execution data. Running with payload.`, {executionId}); Logger.debug(`Execution ID ${executionId} had Execution data. Running with payload.`, {
const workflowExecute = new WorkflowExecute(additionalData, data.executionMode, data.executionData); executionId,
});
const workflowExecute = new WorkflowExecute(
additionalData,
data.executionMode,
data.executionData,
);
workflowExecution = workflowExecute.processRunExecutionData(workflow); workflowExecution = workflowExecute.processRunExecutionData(workflow);
} else if (data.runData === undefined || data.startNodes === undefined || data.startNodes.length === 0 || data.destinationNode === undefined) { } else if (
Logger.debug(`Execution ID ${executionId} will run executing all nodes.`, {executionId}); data.runData === undefined ||
data.startNodes === undefined ||
data.startNodes.length === 0 ||
data.destinationNode === undefined
) {
Logger.debug(`Execution ID ${executionId} will run executing all nodes.`, { executionId });
// Execute all nodes // Execute all nodes
// Can execute without webhook so go on // Can execute without webhook so go on
const workflowExecute = new WorkflowExecute(additionalData, data.executionMode); const workflowExecute = new WorkflowExecute(additionalData, data.executionMode);
workflowExecution = workflowExecute.run(workflow, undefined, data.destinationNode); workflowExecution = workflowExecute.run(workflow, undefined, data.destinationNode);
} else { } else {
Logger.debug(`Execution ID ${executionId} is a partial execution.`, {executionId}); Logger.debug(`Execution ID ${executionId} is a partial execution.`, { executionId });
// Execute only the nodes between start and destination nodes // Execute only the nodes between start and destination nodes
const workflowExecute = new WorkflowExecute(additionalData, data.executionMode); const workflowExecute = new WorkflowExecute(additionalData, data.executionMode);
workflowExecution = workflowExecute.runPartialWorkflow(workflow, data.runData, data.startNodes, data.destinationNode); workflowExecution = workflowExecute.runPartialWorkflow(
workflow,
data.runData,
data.startNodes,
data.destinationNode,
);
} }
this.activeExecutions.attachWorkflowExecution(executionId, workflowExecution); this.activeExecutions.attachWorkflowExecution(executionId, workflowExecution);
if (workflowTimeout > 0) { if (workflowTimeout > 0) {
const timeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number) * 1000; // as seconds const timeout =
Math.min(workflowTimeout, config.get('executions.maxTimeout') as number) * 1000; // as seconds
executionTimeout = setTimeout(() => { executionTimeout = setTimeout(() => {
this.activeExecutions.stopExecution(executionId, 'timeout'); this.activeExecutions.stopExecution(executionId, 'timeout');
}, timeout); }, timeout);
} }
workflowExecution.then((fullRunData) => { workflowExecution
clearTimeout(executionTimeout); .then((fullRunData) => {
if (workflowExecution.isCanceled) { clearTimeout(executionTimeout);
fullRunData.finished = false; if (workflowExecution.isCanceled) {
} fullRunData.finished = false;
this.activeExecutions.remove(executionId, fullRunData); }
}).catch((error) => { this.activeExecutions.remove(executionId, fullRunData);
this.processError(error, new Date(), data.executionMode, executionId, additionalData.hooks); })
}); .catch((error) => {
this.processError(
error,
new Date(),
data.executionMode,
executionId,
additionalData.hooks,
);
});
} catch (error) { } catch (error) {
await this.processError(error, new Date(), data.executionMode, executionId, additionalData.hooks); await this.processError(
error,
new Date(),
data.executionMode,
executionId,
additionalData.hooks,
);
throw error; throw error;
} }
@ -240,12 +324,16 @@ export class WorkflowRunner {
return executionId; return executionId;
} }
async runBull(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean, realtime?: boolean): Promise<string> { async runBull(
data: IWorkflowExecutionDataProcess,
loadStaticData?: boolean,
realtime?: boolean,
restartExecutionId?: string,
): Promise<string> {
// TODO: If "loadStaticData" is set to true it has to load data new on worker // TODO: If "loadStaticData" is set to true it has to load data new on worker
// Register the active execution // Register the active execution
const executionId = await this.activeExecutions.add(data, undefined); const executionId = await this.activeExecutions.add(data, undefined, restartExecutionId);
const jobData: IBullJobData = { const jobData: IBullJobData = {
executionId, executionId,
@ -269,9 +357,14 @@ export class WorkflowRunner {
try { try {
job = await this.jobQueue.add(jobData, jobOptions); job = await this.jobQueue.add(jobData, jobOptions);
console.log('Started with ID: ' + job.id.toString()); console.log(`Started with ID: ${job.id.toString()}`);
hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerMain(data.executionMode, executionId, data.workflowData, { retryOf: data.retryOf ? data.retryOf.toString() : undefined }); hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerMain(
data.executionMode,
executionId,
data.workflowData,
{ retryOf: data.retryOf ? data.retryOf.toString() : undefined },
);
// Normally also workflow should be supplied here but as it only used for sending // Normally also workflow should be supplied here but as it only used for sending
// data to editor-UI is not needed. // data to editor-UI is not needed.
@ -279,130 +372,154 @@ export class WorkflowRunner {
} catch (error) { } catch (error) {
// We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the // We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the
// "workflowExecuteAfter" which we require. // "workflowExecuteAfter" which we require.
const hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(data.executionMode, executionId, data.workflowData, { retryOf: data.retryOf ? data.retryOf.toString() : undefined }); const hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(
data.executionMode,
executionId,
data.workflowData,
{ retryOf: data.retryOf ? data.retryOf.toString() : undefined },
);
await this.processError(error, new Date(), data.executionMode, executionId, hooks); await this.processError(error, new Date(), data.executionMode, executionId, hooks);
throw error; throw error;
} }
const workflowExecution: PCancelable<IRun> = new PCancelable(async (resolve, reject, onCancel) => { const workflowExecution: PCancelable<IRun> = new PCancelable(
onCancel.shouldReject = false; async (resolve, reject, onCancel) => {
onCancel(async () => { onCancel.shouldReject = false;
await Queue.getInstance().stopJob(job); onCancel(async () => {
await Queue.getInstance().stopJob(job);
// We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the // We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the
// "workflowExecuteAfter" which we require. // "workflowExecuteAfter" which we require.
const hooksWorker = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(data.executionMode, executionId, data.workflowData, { retryOf: data.retryOf ? data.retryOf.toString() : undefined }); const hooksWorker = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(
data.executionMode,
executionId,
data.workflowData,
{ retryOf: data.retryOf ? data.retryOf.toString() : undefined },
);
const error = new WorkflowOperationError('Workflow-Execution has been canceled!'); const error = new WorkflowOperationError('Workflow-Execution has been canceled!');
await this.processError(error, new Date(), data.executionMode, executionId, hooksWorker); await this.processError(error, new Date(), data.executionMode, executionId, hooksWorker);
reject(error); 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);
}); });
racingPromises.push(watchDog); const jobData: Promise<IBullJobResponse> = job.finished();
clearWatchdogInterval = () => { const queueRecoveryInterval = config.get('queue.bull.queueRecoveryInterval') as number;
if (watchDogInterval) {
clearInterval(watchDogInterval); const racingPromises: Array<Promise<IBullJobResponse | object>> = [jobData];
watchDogInterval = undefined;
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 { reject(error);
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;
} }
const workflowDidSucceed = !runData.data.resultData.error; const executionDb = (await Db.collections.Execution!.findOne(
if (workflowDidSucceed === true && saveDataSuccessExecution === 'none' || executionId,
workflowDidSucceed === false && saveDataErrorExecution === 'none' )) as IExecutionFlattedDb;
) { const fullExecutionData = ResponseHelper.unflattenExecutionData(executionDb);
await Db.collections.Execution!.delete(executionId); const runData = {
} data: fullExecutionData.data,
} catch (err) { finished: fullExecutionData.finished,
// We don't want errors here to crash n8n. Just log and proceed. mode: fullExecutionData.mode,
console.log('Error removing saved execution from database. More details: ', err); 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); this.activeExecutions.attachWorkflowExecution(executionId, workflowExecution);
return executionId; return executionId;
} }
/** /**
* Run the workflow * Run the workflow
* *
@ -412,16 +529,22 @@ export class WorkflowRunner {
* @returns {Promise<string>} * @returns {Promise<string>}
* @memberof WorkflowRunner * @memberof WorkflowRunner
*/ */
async runSubprocess(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean): Promise<string> { async runSubprocess(
data: IWorkflowExecutionDataProcess,
loadStaticData?: boolean,
restartExecutionId?: string,
): Promise<string> {
let startedAt = new Date(); let startedAt = new Date();
const subprocess = fork(pathJoin(__dirname, 'WorkflowRunnerProcess.js')); const subprocess = fork(pathJoin(__dirname, 'WorkflowRunnerProcess.js'));
if (loadStaticData === true && data.workflowData.id) { if (loadStaticData === true && data.workflowData.id) {
data.workflowData.staticData = await WorkflowHelpers.getStaticDataById(data.workflowData.id as string); data.workflowData.staticData = await WorkflowHelpers.getStaticDataById(
data.workflowData.id as string,
);
} }
// Register the active execution // Register the active execution
const executionId = await this.activeExecutions.add(data, subprocess); const executionId = await this.activeExecutions.add(data, subprocess, restartExecutionId);
// Check if workflow contains a "executeWorkflow" Node as in this // Check if workflow contains a "executeWorkflow" Node as in this
// case we can not know which nodeTypes and credentialTypes will // case we can not know which nodeTypes and credentialTypes will
@ -433,12 +556,11 @@ export class WorkflowRunner {
break; break;
} }
} }
let nodeTypeData: ITransferNodeTypes; let nodeTypeData: ITransferNodeTypes;
let credentialTypeData: ICredentialsTypeData; let credentialTypeData: ICredentialsTypeData;
// eslint-disable-next-line prefer-destructuring
let credentialsOverwrites = this.credentialsOverwrites; let credentialsOverwrites = this.credentialsOverwrites;
if (loadAllNodeTypes) {
if (loadAllNodeTypes === true) {
// Supply all nodeTypes and credentialTypes // Supply all nodeTypes and credentialTypes
nodeTypeData = WorkflowHelpers.getAllNodeTypeData(); nodeTypeData = WorkflowHelpers.getAllNodeTypeData();
const credentialTypes = CredentialTypes(); const credentialTypes = CredentialTypes();
@ -446,7 +568,7 @@ export class WorkflowRunner {
} else { } else {
// Supply only nodeTypes, credentialTypes and overwrites that the workflow needs // Supply only nodeTypes, credentialTypes and overwrites that the workflow needs
nodeTypeData = WorkflowHelpers.getNodeTypeData(data.workflowData.nodes); nodeTypeData = WorkflowHelpers.getNodeTypeData(data.workflowData.nodes);
credentialTypeData = WorkflowHelpers.getCredentialsData(data.credentials); credentialTypeData = WorkflowHelpers.getCredentialsDataByNodes(data.workflowData.nodes);
credentialsOverwrites = {}; credentialsOverwrites = {};
for (const credentialName of Object.keys(credentialTypeData)) { for (const credentialName of Object.keys(credentialTypeData)) {
@ -458,8 +580,10 @@ export class WorkflowRunner {
(data as unknown as IWorkflowExecutionDataProcessWithExecution).executionId = executionId; (data as unknown as IWorkflowExecutionDataProcessWithExecution).executionId = executionId;
(data as unknown as IWorkflowExecutionDataProcessWithExecution).nodeTypeData = nodeTypeData; (data as unknown as IWorkflowExecutionDataProcessWithExecution).nodeTypeData = nodeTypeData;
(data as unknown as IWorkflowExecutionDataProcessWithExecution).credentialsOverwrite = credentialsOverwrites; (data as unknown as IWorkflowExecutionDataProcessWithExecution).credentialsOverwrite =
(data as unknown as IWorkflowExecutionDataProcessWithExecution).credentialsTypeData = credentialTypeData; // TODO: Still needs correct value this.credentialsOverwrites;
(data as unknown as IWorkflowExecutionDataProcessWithExecution).credentialsTypeData =
credentialTypeData;
const workflowHooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId); const workflowHooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId);
@ -475,7 +599,7 @@ export class WorkflowRunner {
let executionTimeout: NodeJS.Timeout; let executionTimeout: NodeJS.Timeout;
let workflowTimeout = config.get('executions.timeout') as number; // initialize with default let workflowTimeout = config.get('executions.timeout') as number; // initialize with default
if (data.workflowData.settings && data.workflowData.settings.executionTimeout) { if (data.workflowData.settings && data.workflowData.settings.executionTimeout) {
workflowTimeout = data.workflowData.settings!.executionTimeout as number; // preference on workflow setting workflowTimeout = data.workflowData.settings.executionTimeout as number; // preference on workflow setting
} }
const processTimeoutFunction = (timeout: number) => { const processTimeoutFunction = (timeout: number) => {
@ -484,11 +608,16 @@ export class WorkflowRunner {
}; };
if (workflowTimeout > 0) { if (workflowTimeout > 0) {
workflowTimeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number) * 1000; // as seconds workflowTimeout =
Math.min(workflowTimeout, config.get('executions.maxTimeout') as number) * 1000; // as seconds
// Start timeout already now but give process at least 5 seconds to start. // Start timeout already now but give process at least 5 seconds to start.
// Without it could would it be possible that the workflow executions times out before it even got started if // Without it could would it be possible that the workflow executions times out before it even got started if
// the timeout time is very short as the process start time can be quite long. // the timeout time is very short as the process start time can be quite long.
executionTimeout = setTimeout(processTimeoutFunction, Math.max(5000, workflowTimeout), workflowTimeout); executionTimeout = setTimeout(
processTimeoutFunction,
Math.max(5000, workflowTimeout),
workflowTimeout,
);
} }
// Create a list of child spawned executions // Create a list of child spawned executions
@ -498,7 +627,10 @@ export class WorkflowRunner {
// Listen to data from the subprocess // Listen to data from the subprocess
subprocess.on('message', async (message: IProcessMessage) => { subprocess.on('message', async (message: IProcessMessage) => {
Logger.debug(`Received child process message of type ${message.type} for execution ID ${executionId}.`, {executionId}); Logger.debug(
`Received child process message of type ${message.type} for execution ID ${executionId}.`,
{ executionId },
);
if (message.type === 'start') { if (message.type === 'start') {
// Now that the execution actually started set the timeout again so that does not time out to early. // Now that the execution actually started set the timeout again so that does not time out to early.
startedAt = new Date(); startedAt = new Date();
@ -506,18 +638,25 @@ export class WorkflowRunner {
clearTimeout(executionTimeout); clearTimeout(executionTimeout);
executionTimeout = setTimeout(processTimeoutFunction, workflowTimeout, workflowTimeout); executionTimeout = setTimeout(processTimeoutFunction, workflowTimeout, workflowTimeout);
} }
} else if (message.type === 'end') { } else if (message.type === 'end') {
clearTimeout(executionTimeout); clearTimeout(executionTimeout);
this.activeExecutions.remove(executionId!, message.data.runData); this.activeExecutions.remove(executionId, message.data.runData);
} else if (message.type === 'sendMessageToUI') { } else if (message.type === 'sendMessageToUI') {
WorkflowExecuteAdditionalData.sendMessageToUI.bind({ sessionId: data.sessionId })(message.data.source, message.data.message); // eslint-disable-next-line @typescript-eslint/no-unsafe-call
WorkflowExecuteAdditionalData.sendMessageToUI.bind({ sessionId: data.sessionId })(
message.data.source,
message.data.message,
);
} else if (message.type === 'processError') { } else if (message.type === 'processError') {
clearTimeout(executionTimeout); clearTimeout(executionTimeout);
const executionError = message.data.executionError as ExecutionError; const executionError = message.data.executionError as ExecutionError;
await this.processError(executionError, startedAt, data.executionMode, executionId, workflowHooks); await this.processError(
executionError,
startedAt,
data.executionMode,
executionId,
workflowHooks,
);
} else if (message.type === 'processHook') { } else if (message.type === 'processHook') {
this.processHookMessage(workflowHooks, message.data as IProcessMessageDataHook); this.processHookMessage(workflowHooks, message.data as IProcessMessageDataHook);
} else if (message.type === 'timeout') { } else if (message.type === 'timeout') {
@ -529,43 +668,61 @@ export class WorkflowRunner {
} else if (message.type === 'startExecution') { } else if (message.type === 'startExecution') {
const executionId = await this.activeExecutions.add(message.data.runData); const executionId = await this.activeExecutions.add(message.data.runData);
childExecutionIds.push(executionId); childExecutionIds.push(executionId);
subprocess.send({ type: 'executionId', data: {executionId} } as IProcessMessage); subprocess.send({ type: 'executionId', data: { executionId } } as IProcessMessage);
} else if (message.type === 'finishExecution') { } else if (message.type === 'finishExecution') {
const executionIdIndex = childExecutionIds.indexOf(message.data.executionId); const executionIdIndex = childExecutionIds.indexOf(message.data.executionId);
if (executionIdIndex !== -1) { if (executionIdIndex !== -1) {
childExecutionIds.splice(executionIdIndex, 1); childExecutionIds.splice(executionIdIndex, 1);
} }
// eslint-disable-next-line @typescript-eslint/await-thenable
await this.activeExecutions.remove(message.data.executionId, message.data.result); await this.activeExecutions.remove(message.data.executionId, message.data.result);
} }
}); });
// Also get informed when the processes does exit especially when it did crash or timed out // Also get informed when the processes does exit especially when it did crash or timed out
subprocess.on('exit', async (code, signal) => { subprocess.on('exit', async (code, signal) => {
if (signal === 'SIGTERM'){ if (signal === 'SIGTERM') {
Logger.debug(`Subprocess for execution ID ${executionId} timed out.`, {executionId}); Logger.debug(`Subprocess for execution ID ${executionId} timed out.`, { executionId });
// Execution timed out and its process has been terminated // Execution timed out and its process has been terminated
const timeoutError = new WorkflowOperationError('Workflow execution timed out!'); const timeoutError = new WorkflowOperationError('Workflow execution timed out!');
await this.processError(timeoutError, startedAt, data.executionMode, executionId, workflowHooks); await this.processError(
timeoutError,
startedAt,
data.executionMode,
executionId,
workflowHooks,
);
} else if (code !== 0) { } else if (code !== 0) {
Logger.debug(`Subprocess for execution ID ${executionId} finished with error code ${code}.`, {executionId}); Logger.debug(
`Subprocess for execution ID ${executionId} finished with error code ${code}.`,
{ executionId },
);
// Process did exit with error code, so something went wrong. // Process did exit with error code, so something went wrong.
const executionError = new WorkflowOperationError('Workflow execution process did crash for an unknown reason!'); const executionError = new WorkflowOperationError(
'Workflow execution process did crash for an unknown reason!',
);
await this.processError(executionError, startedAt, data.executionMode, executionId, workflowHooks); await this.processError(
executionError,
startedAt,
data.executionMode,
executionId,
workflowHooks,
);
} }
for(const executionId of childExecutionIds) { for (const executionId of childExecutionIds) {
// When the child process exits, if we still have // When the child process exits, if we still have
// pending child executions, we mark them as finished // pending child executions, we mark them as finished
// They will display as unknown to the user // They will display as unknown to the user
// Instead of pending forever as executing when it // Instead of pending forever as executing when it
// actually isn't anymore. // actually isn't anymore.
// eslint-disable-next-line @typescript-eslint/await-thenable, no-await-in-loop
await this.activeExecutions.remove(executionId); await this.activeExecutions.remove(executionId);
} }
clearTimeout(executionTimeout); clearTimeout(executionTimeout);
}); });

View file

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

View file

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

View file

@ -1,27 +1,13 @@
import { /* eslint-disable import/no-cycle */
WorkflowExecuteMode, import { WorkflowExecuteMode } from 'n8n-workflow';
} from 'n8n-workflow';
import { import { Column, ColumnOptions, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
IExecutionFlattedDb, import { IExecutionFlattedDb, IWorkflowDb } from '../..';
IWorkflowDb,
} from '../../';
import { import { resolveDataType } from '../utils';
resolveDataType
} from '../utils';
import {
Column,
ColumnOptions,
Entity,
Index,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity() @Entity()
export class ExecutionEntity implements IExecutionFlattedDb { export class ExecutionEntity implements IExecutionFlattedDb {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;
@ -53,4 +39,8 @@ export class ExecutionEntity implements IExecutionFlattedDb {
@Index() @Index()
@Column({ nullable: true }) @Column({ nullable: true })
workflowId: string; workflowId: string;
@Index()
@Column({ type: resolveDataType('datetime') as ColumnOptions['type'], nullable: true })
waitTill: Date;
} }

View file

@ -1,4 +1,15 @@
import { BeforeUpdate, Column, CreateDateColumn, Entity, Index, ManyToMany, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable import/no-cycle */
import {
BeforeUpdate,
Column,
CreateDateColumn,
Entity,
Index,
ManyToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { IsDate, IsOptional, IsString, Length } from 'class-validator'; import { IsDate, IsOptional, IsString, Length } from 'class-validator';
import { ITagDb } from '../../Interfaces'; import { ITagDb } from '../../Interfaces';
@ -7,7 +18,6 @@ import { getTimestampSyntax } from '../utils';
@Entity() @Entity()
export class TagEntity implements ITagDb { export class TagEntity implements ITagDb {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;
@ -22,12 +32,16 @@ export class TagEntity implements ITagDb {
@IsDate() @IsDate()
createdAt: Date; createdAt: Date;
@UpdateDateColumn({ precision: 3, default: () => getTimestampSyntax(), onUpdate: getTimestampSyntax() }) @UpdateDateColumn({
precision: 3,
default: () => getTimestampSyntax(),
onUpdate: getTimestampSyntax(),
})
@IsOptional() // ignored by validation because set at DB level @IsOptional() // ignored by validation because set at DB level
@IsDate() @IsDate()
updatedAt: Date; updatedAt: Date;
@ManyToMany(() => WorkflowEntity, workflow => workflow.tags) @ManyToMany(() => WorkflowEntity, (workflow) => workflow.tags)
workflows: WorkflowEntity[]; workflows: WorkflowEntity[];
@BeforeUpdate() @BeforeUpdate()

View file

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

View file

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

View file

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

View file

@ -0,0 +1,23 @@
import {MigrationInterface, QueryRunner} from "typeorm";
import * as config from '../../../../config';
export class AddWaitColumnId1626183952959 implements MigrationInterface {
name = 'AddWaitColumnId1626183952959';
async up(queryRunner: QueryRunner): Promise<void> {
const tablePrefix = config.get('database.tablePrefix');
console.log('\n\nINFO: Started with migration for wait functionality.\n Depending on the number of saved executions, that may take a little bit.\n\n');
await queryRunner.query('ALTER TABLE `' + tablePrefix + 'execution_entity` ADD `waitTill` DATETIME NULL');
await queryRunner.query('CREATE INDEX `IDX_' + tablePrefix + 'ca4a71b47f28ac6ea88293a8e2` ON `' + tablePrefix + 'execution_entity` (`waitTill`)');
}
async down(queryRunner: QueryRunner): Promise<void> {
const tablePrefix = config.get('database.tablePrefix');
await queryRunner.query(
'DROP INDEX `IDX_' + tablePrefix + 'ca4a71b47f28ac6ea88293a8e2` ON `' + tablePrefix + 'execution_entity`'
);
await queryRunner.query('ALTER TABLE `' + tablePrefix + 'execution_entity` DROP COLUMN `waitTill`');
}
}

View file

@ -8,6 +8,7 @@ import { ChangeCredentialDataSize1620729500000 } from './1620729500000-ChangeCre
import { CreateTagEntity1617268711084 } from './1617268711084-CreateTagEntity'; import { CreateTagEntity1617268711084 } from './1617268711084-CreateTagEntity';
import { UniqueWorkflowNames1620826335440 } from './1620826335440-UniqueWorkflowNames'; import { UniqueWorkflowNames1620826335440 } from './1620826335440-UniqueWorkflowNames';
import { CertifyCorrectCollation1623936588000 } from './1623936588000-CertifyCorrectCollation'; import { CertifyCorrectCollation1623936588000 } from './1623936588000-CertifyCorrectCollation';
import { AddWaitColumnId1626183952959 } from './1626183952959-AddWaitColumn';
export const mysqlMigrations = [ export const mysqlMigrations = [
InitialMigration1588157391238, InitialMigration1588157391238,
@ -20,4 +21,5 @@ export const mysqlMigrations = [
CreateTagEntity1617268711084, CreateTagEntity1617268711084,
UniqueWorkflowNames1620826335440, UniqueWorkflowNames1620826335440,
CertifyCorrectCollation1623936588000, CertifyCorrectCollation1623936588000,
AddWaitColumnId1626183952959,
]; ];

View file

@ -0,0 +1,32 @@
import {MigrationInterface, QueryRunner} from "typeorm";
import * as config from '../../../../config';
export class AddwaitTill1626176912946 implements MigrationInterface {
name = 'AddwaitTill1626176912946';
async up(queryRunner: QueryRunner): Promise<void> {
let tablePrefix = config.get('database.tablePrefix');
const tablePrefixPure = tablePrefix;
const schema = config.get('database.postgresdb.schema');
if (schema) {
tablePrefix = schema + '.' + tablePrefix;
}
console.log('\n\nINFO: Started with migration for wait functionality.\n Depending on the number of saved executions, that may take a little bit.\n\n');
await queryRunner.query(`ALTER TABLE ${tablePrefix}execution_entity ADD "waitTill" TIMESTAMP`);
await queryRunner.query(`CREATE INDEX IF NOT EXISTS IDX_${tablePrefixPure}ca4a71b47f28ac6ea88293a8e2 ON ${tablePrefix}execution_entity ("waitTill")`);
}
async down(queryRunner: QueryRunner): Promise<void> {
let tablePrefix = config.get('database.tablePrefix');
const tablePrefixPure = tablePrefix;
const schema = config.get('database.postgresdb.schema');
if (schema) {
tablePrefix = schema + '.' + tablePrefix;
}
await queryRunner.query(`DROP INDEX IDX_${tablePrefixPure}ca4a71b47f28ac6ea88293a8e2`);
await queryRunner.query(`ALTER TABLE ${tablePrefix}webhook_entity DROP COLUMN "waitTill"`);
}
}

View file

@ -5,6 +5,7 @@ import { AddWebhookId1611144599516 } from './1611144599516-AddWebhookId';
import { MakeStoppedAtNullable1607431743768 } from './1607431743768-MakeStoppedAtNullable'; import { MakeStoppedAtNullable1607431743768 } from './1607431743768-MakeStoppedAtNullable';
import { CreateTagEntity1617270242566 } from './1617270242566-CreateTagEntity'; import { CreateTagEntity1617270242566 } from './1617270242566-CreateTagEntity';
import { UniqueWorkflowNames1620824779533 } from './1620824779533-UniqueWorkflowNames'; import { UniqueWorkflowNames1620824779533 } from './1620824779533-UniqueWorkflowNames';
import { AddwaitTill1626176912946 } from './1626176912946-AddwaitTill';
export const postgresMigrations = [ export const postgresMigrations = [
InitialMigration1587669153312, InitialMigration1587669153312,
@ -14,4 +15,5 @@ export const postgresMigrations = [
MakeStoppedAtNullable1607431743768, MakeStoppedAtNullable1607431743768,
CreateTagEntity1617270242566, CreateTagEntity1617270242566,
UniqueWorkflowNames1620824779533, UniqueWorkflowNames1620824779533,
AddwaitTill1626176912946,
]; ];

View file

@ -0,0 +1,33 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import * as config from '../../../../config';
export class AddWaitColumn1621707690587 implements MigrationInterface {
name = 'AddWaitColumn1621707690587';
async up(queryRunner: QueryRunner): Promise<void> {
const tablePrefix = config.get('database.tablePrefix');
console.log('\n\nINFO: Started with migration for wait functionality.\n Depending on the number of saved executions, that may take a little bit.\n\n');
await queryRunner.query(`DROP TABLE IF EXISTS "${tablePrefix}temporary_execution_entity"`);
await queryRunner.query(`CREATE TABLE "${tablePrefix}temporary_execution_entity" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "data" text NOT NULL, "finished" boolean NOT NULL, "mode" varchar NOT NULL, "retryOf" varchar, "retrySuccessId" varchar, "startedAt" datetime NOT NULL, "stoppedAt" datetime, "workflowData" text NOT NULL, "workflowId" varchar, "waitTill" DATETIME)`, undefined);
await queryRunner.query(`INSERT INTO "${tablePrefix}temporary_execution_entity"("id", "data", "finished", "mode", "retryOf", "retrySuccessId", "startedAt", "stoppedAt", "workflowData", "workflowId") SELECT "id", "data", "finished", "mode", "retryOf", "retrySuccessId", "startedAt", "stoppedAt", "workflowData", "workflowId" FROM "${tablePrefix}execution_entity"`);
await queryRunner.query(`DROP TABLE "${tablePrefix}execution_entity"`);
await queryRunner.query(`ALTER TABLE "${tablePrefix}temporary_execution_entity" RENAME TO "${tablePrefix}execution_entity"`);
await queryRunner.query(`CREATE INDEX "IDX_${tablePrefix}cefb067df2402f6aed0638a6c1" ON "${tablePrefix}execution_entity" ("stoppedAt")`);
await queryRunner.query(`CREATE INDEX "IDX_${tablePrefix}ca4a71b47f28ac6ea88293a8e2" ON "${tablePrefix}execution_entity" ("waitTill")`);
await queryRunner.query(`VACUUM;`);
}
async down(queryRunner: QueryRunner): Promise<void> {
const tablePrefix = config.get('database.tablePrefix');
await queryRunner.query(`CREATE TABLE IF NOT EXISTS "${tablePrefix}temporary_execution_entity" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "data" text NOT NULL, "finished" boolean NOT NULL, "mode" varchar NOT NULL, "retryOf" varchar, "retrySuccessId" varchar, "startedAt" datetime NOT NULL, "stoppedAt" datetime, "workflowData" text NOT NULL, "workflowId" varchar)`, undefined);
await queryRunner.query(`INSERT INTO "${tablePrefix}temporary_execution_entity"("id", "data", "finished", "mode", "retryOf", "retrySuccessId", "startedAt", "stoppedAt", "workflowData", "workflowId") SELECT "id", "data", "finished", "mode", "retryOf", "retrySuccessId", "startedAt", "stoppedAt", "workflowData", "workflowId" FROM "${tablePrefix}execution_entity"`);
await queryRunner.query(`DROP TABLE "${tablePrefix}execution_entity"`);
await queryRunner.query(`ALTER TABLE "${tablePrefix}temporary_execution_entity" RENAME TO "${tablePrefix}execution_entity"`);
await queryRunner.query(`CREATE INDEX "IDX_${tablePrefix}cefb067df2402f6aed0638a6c1" ON "${tablePrefix}execution_entity" ("stoppedAt")`);
await queryRunner.query(`VACUUM;`);
}
}

View file

@ -5,6 +5,7 @@ import { AddWebhookId1611071044839 } from './1611071044839-AddWebhookId';
import { MakeStoppedAtNullable1607431743769 } from './1607431743769-MakeStoppedAtNullable'; import { MakeStoppedAtNullable1607431743769 } from './1607431743769-MakeStoppedAtNullable';
import { CreateTagEntity1617213344594 } from './1617213344594-CreateTagEntity'; import { CreateTagEntity1617213344594 } from './1617213344594-CreateTagEntity';
import { UniqueWorkflowNames1620821879465 } from './1620821879465-UniqueWorkflowNames'; import { UniqueWorkflowNames1620821879465 } from './1620821879465-UniqueWorkflowNames';
import { AddWaitColumn1621707690587 } from './1621707690587-AddWaitColumn';
export const sqliteMigrations = [ export const sqliteMigrations = [
InitialMigration1588102412422, InitialMigration1588102412422,
@ -14,4 +15,5 @@ export const sqliteMigrations = [
MakeStoppedAtNullable1607431743769, MakeStoppedAtNullable1607431743769,
CreateTagEntity1617213344594, CreateTagEntity1617213344594,
UniqueWorkflowNames1620821879465, UniqueWorkflowNames1620821879465,
AddWaitColumn1621707690587,
]; ];

View file

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

View file

@ -1,3 +1,5 @@
/* eslint-disable import/first */
/* eslint-disable import/no-cycle */
export * from './CredentialsHelper'; export * from './CredentialsHelper';
export * from './CredentialTypes'; export * from './CredentialTypes';
export * from './CredentialsOverwrites'; export * from './CredentialsOverwrites';
@ -5,6 +7,8 @@ export * from './ExternalHooks';
export * from './Interfaces'; export * from './Interfaces';
export * from './LoadNodesAndCredentials'; export * from './LoadNodesAndCredentials';
export * from './NodeTypes'; export * from './NodeTypes';
export * from './WaitTracker';
export * from './WaitingWebhooks';
export * from './WorkflowCredentials'; export * from './WorkflowCredentials';
export * from './WorkflowRunner'; export * from './WorkflowRunner';
@ -20,6 +24,7 @@ import * as WebhookHelpers from './WebhookHelpers';
import * as WebhookServer from './WebhookServer'; import * as WebhookServer from './WebhookServer';
import * as WorkflowExecuteAdditionalData from './WorkflowExecuteAdditionalData'; import * as WorkflowExecuteAdditionalData from './WorkflowExecuteAdditionalData';
import * as WorkflowHelpers from './WorkflowHelpers'; import * as WorkflowHelpers from './WorkflowHelpers';
export { export {
ActiveExecutions, ActiveExecutions,
ActiveWorkflowRunner, ActiveWorkflowRunner,

View file

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

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n-core", "name": "n8n-core",
"version": "0.78.0", "version": "0.81.0",
"description": "Core functionality of n8n", "description": "Core functionality of n8n",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
@ -17,8 +17,9 @@
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"dev": "npm run watch", "dev": "npm run watch",
"tslint": "tslint -p tsconfig.json -c tslint.json", "format": "cd ../.. && node_modules/prettier/bin-prettier.js packages/core/**/**.ts --write",
"tslintfix": "tslint --fix -p tsconfig.json -c tslint.json", "lint": "cd ../.. && node_modules/eslint/bin/eslint.js packages/core",
"lintfix": "cd ../.. && node_modules/eslint/bin/eslint.js packages/core --fix",
"watch": "tsc --watch", "watch": "tsc --watch",
"test": "jest" "test": "jest"
}, },
@ -38,16 +39,16 @@
"source-map-support": "^0.5.9", "source-map-support": "^0.5.9",
"ts-jest": "^26.3.0", "ts-jest": "^26.3.0",
"tslint": "^6.1.2", "tslint": "^6.1.2",
"typescript": "~3.9.7" "typescript": "~4.3.5"
}, },
"dependencies": { "dependencies": {
"client-oauth2": "^4.2.5", "client-oauth2": "^4.2.5",
"cron": "^1.7.2", "cron": "^1.7.2",
"crypto-js": "4.0.0", "crypto-js": "~4.1.1",
"file-type": "^14.6.2", "file-type": "^14.6.2",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"mime-types": "^2.1.27", "mime-types": "^2.1.27",
"n8n-workflow": "~0.64.0", "n8n-workflow": "~0.66.0",
"oauth-1.0a": "^2.2.6", "oauth-1.0a": "^2.2.6",
"p-cancelable": "^2.0.0", "p-cancelable": "^2.0.0",
"request": "^2.88.2", "request": "^2.88.2",

View file

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

View file

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

View file

@ -5,4 +5,6 @@ export const EXTENSIONS_SUBDIRECTORY = 'custom';
export const USER_FOLDER_ENV_OVERWRITE = 'N8N_USER_FOLDER'; export const USER_FOLDER_ENV_OVERWRITE = 'N8N_USER_FOLDER';
export const USER_SETTINGS_FILE_NAME = 'config'; export const USER_SETTINGS_FILE_NAME = 'config';
export const USER_SETTINGS_SUBFOLDER = '.n8n'; export const USER_SETTINGS_SUBFOLDER = '.n8n';
export const PLACEHOLDER_EMPTY_EXECUTION_ID = '__UNKOWN__';
export const TUNNEL_SUBDOMAIN_ENV = 'N8N_TUNNEL_SUBDOMAIN'; export const TUNNEL_SUBDOMAIN_ENV = 'N8N_TUNNEL_SUBDOMAIN';
export const WAIT_TIME_UNLIMITED = '3000-01-01T00:00:00.000Z';

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

@ -1,88 +1,83 @@
import { Credentials } from '../src'; import { Credentials } from '../src';
describe('Credentials', () => { 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', () => { credentials.setDataKey(key, newData, password);
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);
});
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 = [ const credentials = new Credentials('testName', 'testType', [], initialDataEncoded);
{
nodeType: 'base.noOp',
user: 'userName',
date: new Date(),
},
];
const credentials = new Credentials('testName', 'testType', nodeAccess); const newData = 1234;
const key = 'key1'; // Set and read new data
const password = 'password'; credentials.setDataKey(key, newData, password);
const nodeType = 'base.noOp'; expect(credentials.getDataKey(key, password)).toEqual(newData);
const newData = 1234;
credentials.setDataKey(key, newData, password); // Read the data which got provided encrypted on init
expect(credentials.getDataKey('key1', password)).toEqual(initialData);
// 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));
});
}); });
});
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,28 +18,27 @@ import {
WorkflowHooks, WorkflowHooks,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import { Credentials, IDeferredPromise, IExecuteFunctions } from '../src';
Credentials,
IDeferredPromise,
IExecuteFunctions,
} from '../src';
export class CredentialsHelper extends ICredentialsHelper { export class CredentialsHelper extends ICredentialsHelper {
getDecrypted(name: string, type: string): ICredentialDataDecryptedObject { getDecrypted(name: string, type: string): Promise<ICredentialDataDecryptedObject> {
return {}; return new Promise((res) => res({}));
} }
getCredentials(name: string, type: string): Credentials { getCredentials(name: string, type: string): Promise<Credentials> {
return new Credentials('', '', [], ''); 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 { class NodeTypesClass implements INodeTypes {
nodeTypes: INodeTypeData = { nodeTypes: INodeTypeData = {
'n8n-nodes-base.if': { 'n8n-nodes-base.if': {
sourcePath: '', sourcePath: '',
@ -159,9 +158,7 @@ class NodeTypesClass implements INodeTypes {
type: 'number', type: 'number',
displayOptions: { displayOptions: {
hide: { hide: {
operation: [ operation: ['isEmpty'],
'isEmpty',
],
}, },
}, },
default: 0, default: 0,
@ -227,10 +224,7 @@ class NodeTypesClass implements INodeTypes {
type: 'string', type: 'string',
displayOptions: { displayOptions: {
hide: { hide: {
operation: [ operation: ['isEmpty', 'regex'],
'isEmpty',
'regex',
],
}, },
}, },
default: '', default: '',
@ -242,9 +236,7 @@ class NodeTypesClass implements INodeTypes {
type: 'string', type: 'string',
displayOptions: { displayOptions: {
show: { show: {
operation: [ operation: ['regex'],
'regex',
],
}, },
}, },
default: '', default: '',
@ -272,7 +264,8 @@ class NodeTypesClass implements INodeTypes {
}, },
], ],
default: 'all', default: 'all',
description: 'If multiple rules got set this settings decides if it is true as soon as ANY condition matches or only if ALL get meet.', description:
'If multiple rules got set this settings decides if it is true as soon as ANY condition matches or only if ALL get meet.',
}, },
], ],
}, },
@ -289,19 +282,30 @@ class NodeTypesClass implements INodeTypes {
const compareOperationFunctions: { const compareOperationFunctions: {
[key: string]: (value1: NodeParameterValue, value2: NodeParameterValue) => boolean; [key: string]: (value1: NodeParameterValue, value2: NodeParameterValue) => boolean;
} = { } = {
contains: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || '').toString().includes((value2 || '').toString()), contains: (value1: NodeParameterValue, value2: NodeParameterValue) =>
notContains: (value1: NodeParameterValue, value2: NodeParameterValue) => !(value1 || '').toString().includes((value2 || '').toString()), (value1 || '').toString().includes((value2 || '').toString()),
endsWith: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 as string).endsWith(value2 as string), notContains: (value1: NodeParameterValue, value2: NodeParameterValue) =>
!(value1 || '').toString().includes((value2 || '').toString()),
endsWith: (value1: NodeParameterValue, value2: NodeParameterValue) =>
(value1 as string).endsWith(value2 as string),
equal: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 === value2, equal: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 === value2,
notEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 !== value2, notEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 !== value2,
larger: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) > (value2 || 0), larger: (value1: NodeParameterValue, value2: NodeParameterValue) =>
largerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) >= (value2 || 0), (value1 || 0) > (value2 || 0),
smaller: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) < (value2 || 0), largerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) =>
smallerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) <= (value2 || 0), (value1 || 0) >= (value2 || 0),
startsWith: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 as string).startsWith(value2 as string), smaller: (value1: NodeParameterValue, value2: NodeParameterValue) =>
isEmpty: (value1: NodeParameterValue) => [undefined, null, ''].includes(value1 as string), (value1 || 0) < (value2 || 0),
smallerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) =>
(value1 || 0) <= (value2 || 0),
startsWith: (value1: NodeParameterValue, value2: NodeParameterValue) =>
(value1 as string).startsWith(value2 as string),
isEmpty: (value1: NodeParameterValue) =>
[undefined, null, ''].includes(value1 as string),
regex: (value1: NodeParameterValue, value2: NodeParameterValue) => { regex: (value1: NodeParameterValue, value2: NodeParameterValue) => {
const regexMatch = (value2 || '').toString().match(new RegExp('^/(.*?)/([gimusy]*)$')); const regexMatch = (value2 || '')
.toString()
.match(new RegExp('^/(.*?)/([gimusy]*)$'));
let regex: RegExp; let regex: RegExp;
if (!regexMatch) { if (!regexMatch) {
@ -317,18 +321,13 @@ class NodeTypesClass implements INodeTypes {
}; };
// The different dataTypes to check the values in // The different dataTypes to check the values in
const dataTypes = [ const dataTypes = ['boolean', 'number', 'string'];
'boolean',
'number',
'string',
];
// Itterate over all items to check which ones should be output as via output "true" and // Itterate over all items to check which ones should be output as via output "true" and
// which ones via output "false" // which ones via output "false"
let dataType: string; let dataType: string;
let compareOperationResult: boolean; let compareOperationResult: boolean;
itemLoop: itemLoop: for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
item = items[itemIndex]; item = items[itemIndex];
let compareData: INodeParameters; let compareData: INodeParameters;
@ -338,9 +337,16 @@ class NodeTypesClass implements INodeTypes {
// Check all the values of the different dataTypes // Check all the values of the different dataTypes
for (dataType of dataTypes) { for (dataType of dataTypes) {
// Check all the values of the current dataType // Check all the values of the current dataType
for (compareData of this.getNodeParameter(`conditions.${dataType}`, itemIndex, []) as INodeParameters[]) { for (compareData of this.getNodeParameter(
`conditions.${dataType}`,
itemIndex,
[],
) as INodeParameters[]) {
// Check if the values passes // Check if the values passes
compareOperationResult = compareOperationFunctions[compareData.operation as string](compareData.value1 as NodeParameterValue, compareData.value2 as NodeParameterValue); compareOperationResult = compareOperationFunctions[compareData.operation as string](
compareData.value1 as NodeParameterValue,
compareData.value2 as NodeParameterValue,
);
if (compareOperationResult === true && combineOperation === 'any') { if (compareOperationResult === true && combineOperation === 'any') {
// If it passes and the operation is "any" we do not have to check any // If it passes and the operation is "any" we do not have to check any
@ -395,21 +401,25 @@ class NodeTypesClass implements INodeTypes {
{ {
name: 'Append', name: 'Append',
value: 'append', value: 'append',
description: 'Combines data of both inputs. The output will contain items of input 1 and input 2.', description:
'Combines data of both inputs. The output will contain items of input 1 and input 2.',
}, },
{ {
name: 'Pass-through', name: 'Pass-through',
value: 'passThrough', value: 'passThrough',
description: 'Passes through data of one input. The output will conain only items of the defined input.', description:
'Passes through data of one input. The output will conain only items of the defined input.',
}, },
{ {
name: 'Wait', name: 'Wait',
value: 'wait', value: 'wait',
description: 'Waits till data of both inputs is available and will then output a single empty item.', description:
'Waits till data of both inputs is available and will then output a single empty item.',
}, },
], ],
default: 'append', default: 'append',
description: 'How data should be merged. If it should simply<br />be appended or merged depending on a property.', description:
'How data should be merged. If it should simply<br />be appended or merged depending on a property.',
}, },
{ {
displayName: 'Output Data', displayName: 'Output Data',
@ -417,9 +427,7 @@ class NodeTypesClass implements INodeTypes {
type: 'options', type: 'options',
displayOptions: { displayOptions: {
show: { show: {
mode: [ mode: ['passThrough'],
'passThrough',
],
}, },
}, },
options: [ options: [
@ -510,7 +518,8 @@ class NodeTypesClass implements INodeTypes {
name: 'keepOnlySet', name: 'keepOnlySet',
type: 'boolean', type: 'boolean',
default: false, default: false,
description: 'If only the values set on this node should be<br />kept and all others removed.', description:
'If only the values set on this node should be<br />kept and all others removed.',
}, },
{ {
displayName: 'Values to Set', displayName: 'Values to Set',
@ -532,7 +541,8 @@ class NodeTypesClass implements INodeTypes {
name: 'name', name: 'name',
type: 'string', type: 'string',
default: 'propertyName', default: 'propertyName',
description: 'Name of the property to write data to.<br />Supports dot-notation.<br />Example: "data.person[0].name"', description:
'Name of the property to write data to.<br />Supports dot-notation.<br />Example: "data.person[0].name"',
}, },
{ {
displayName: 'Value', displayName: 'Value',
@ -552,7 +562,8 @@ class NodeTypesClass implements INodeTypes {
name: 'name', name: 'name',
type: 'string', type: 'string',
default: 'propertyName', default: 'propertyName',
description: 'Name of the property to write data to.<br />Supports dot-notation.<br />Example: "data.person[0].name"', description:
'Name of the property to write data to.<br />Supports dot-notation.<br />Example: "data.person[0].name"',
}, },
{ {
displayName: 'Value', displayName: 'Value',
@ -572,7 +583,8 @@ class NodeTypesClass implements INodeTypes {
name: 'name', name: 'name',
type: 'string', type: 'string',
default: 'propertyName', default: 'propertyName',
description: 'Name of the property to write data to.<br />Supports dot-notation.<br />Example: "data.person[0].name"', description:
'Name of the property to write data to.<br />Supports dot-notation.<br />Example: "data.person[0].name"',
}, },
{ {
displayName: 'Value', displayName: 'Value',
@ -608,7 +620,6 @@ class NodeTypesClass implements INodeTypes {
], ],
}, },
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData(); const items = this.getInputData();
if (items.length === 0) { if (items.length === 0) {
@ -641,31 +652,37 @@ class NodeTypesClass implements INodeTypes {
} }
// Add boolean values // Add boolean values
(this.getNodeParameter('values.boolean', itemIndex, []) as INodeParameters[]).forEach((setItem) => { (this.getNodeParameter('values.boolean', itemIndex, []) as INodeParameters[]).forEach(
if (options.dotNotation === false) { (setItem) => {
newItem.json[setItem.name as string] = !!setItem.value; if (options.dotNotation === false) {
} else { newItem.json[setItem.name as string] = !!setItem.value;
set(newItem.json, setItem.name as string, !!setItem.value); } else {
} set(newItem.json, setItem.name as string, !!setItem.value);
}); }
},
);
// Add number values // Add number values
(this.getNodeParameter('values.number', itemIndex, []) as INodeParameters[]).forEach((setItem) => { (this.getNodeParameter('values.number', itemIndex, []) as INodeParameters[]).forEach(
if (options.dotNotation === false) { (setItem) => {
newItem.json[setItem.name as string] = setItem.value; if (options.dotNotation === false) {
} else { newItem.json[setItem.name as string] = setItem.value;
set(newItem.json, setItem.name as string, setItem.value); } else {
} set(newItem.json, setItem.name as string, setItem.value);
}); }
},
);
// Add string values // Add string values
(this.getNodeParameter('values.string', itemIndex, []) as INodeParameters[]).forEach((setItem) => { (this.getNodeParameter('values.string', itemIndex, []) as INodeParameters[]).forEach(
if (options.dotNotation === false) { (setItem) => {
newItem.json[setItem.name as string] = setItem.value; if (options.dotNotation === false) {
} else { newItem.json[setItem.name as string] = setItem.value;
set(newItem.json, setItem.name as string, setItem.value); } else {
} set(newItem.json, setItem.name as string, setItem.value);
}); }
},
);
returnData.push(newItem); returnData.push(newItem);
} }
@ -700,7 +717,7 @@ class NodeTypesClass implements INodeTypes {
}, },
}; };
async init(nodeTypes: INodeTypeData): Promise<void> { } async init(nodeTypes: INodeTypeData): Promise<void> {}
getAll(): INodeType[] { getAll(): INodeType[] {
return Object.values(this.nodeTypes).map((data) => data.type); return Object.values(this.nodeTypes).map((data) => data.type);
@ -713,7 +730,6 @@ class NodeTypesClass implements INodeTypes {
let nodeTypesInstance: NodeTypesClass | undefined; let nodeTypesInstance: NodeTypesClass | undefined;
export function NodeTypes(): NodeTypesClass { export function NodeTypes(): NodeTypesClass {
if (nodeTypesInstance === undefined) { if (nodeTypesInstance === undefined) {
nodeTypesInstance = new NodeTypesClass(); nodeTypesInstance = new NodeTypesClass();
@ -723,8 +739,10 @@ export function NodeTypes(): NodeTypesClass {
return nodeTypesInstance; return nodeTypesInstance;
} }
export function WorkflowExecuteAdditionalData(
export function WorkflowExecuteAdditionalData(waitPromise: IDeferredPromise<IRun>, nodeExecutionOrder: string[]): IWorkflowExecuteAdditionalData { waitPromise: IDeferredPromise<IRun>,
nodeExecutionOrder: string[],
): IWorkflowExecuteAdditionalData {
const hookFunctions = { const hookFunctions = {
nodeExecuteAfter: [ nodeExecuteAfter: [
async (nodeName: string, data: ITaskData): Promise<void> => { async (nodeName: string, data: ITaskData): Promise<void> => {
@ -748,15 +766,15 @@ export function WorkflowExecuteAdditionalData(waitPromise: IDeferredPromise<IRun
}; };
return { return {
credentials: {}, credentialsHelper: new CredentialsHelper(''),
credentialsHelper: new CredentialsHelper({}, ''),
hooks: new WorkflowHooks(hookFunctions, 'trigger', '1', workflowData), hooks: new WorkflowHooks(hookFunctions, 'trigger', '1', workflowData),
executeWorkflow: async (workflowInfo: IExecuteWorkflowInfo): Promise<any> => {}, // tslint:disable-line:no-any executeWorkflow: async (workflowInfo: IExecuteWorkflowInfo): Promise<any> => {},
sendMessageToUI: (message: string) => {}, sendMessageToUI: (message: string) => {},
restApiUrl: '', restApiUrl: '',
encryptionKey: 'test', encryptionKey: 'test',
timezone: 'America/New_York', timezone: 'America/New_York',
webhookBaseUrl: 'webhook', webhookBaseUrl: 'webhook',
webhookWaitingBaseUrl: 'webhook-waiting',
webhookTestBaseUrl: 'webhook-test', webhookTestBaseUrl: 'webhook-test',
}; };
} }

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,3 @@
> 1%
last 2 versions
not ie <= 8

View file

@ -0,0 +1,19 @@
module.exports = {
root: true,
env: {
node: true,
},
extends: ['plugin:vue/essential', '@vue/typescript'],
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
semi: [2, 'always'],
indent: ['error', 'tab'],
'comma-dangle': ['error', 'always-multiline'],
'no-tabs': 0,
'no-labels': 0,
},
parserOptions: {
parser: '@typescript-eslint/parser',
},
};

View file

@ -0,0 +1,26 @@
.DS_Store
storybook-static
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.md
*.stories.js
*.mdx

View file

@ -0,0 +1,169 @@
/**
* These icons are only defined for storybook build
* Editor icons are defined seperately
*/
import { library } from '@fortawesome/fontawesome-svg-core';
import {
faAngleDoubleLeft,
faAngleDown,
faAngleRight,
faAngleUp,
faArrowLeft,
faArrowRight,
faAt,
faBook,
faBug,
faCalendar,
faCheck,
faChevronDown,
faChevronUp,
faCode,
faCodeBranch,
faCog,
faCogs,
faClone,
faCloud,
faCloudDownloadAlt,
faCopy,
faCut,
faDotCircle,
faEdit,
faEnvelope,
faEye,
faExclamationTriangle,
faExpand,
faExternalLinkAlt,
faExchangeAlt,
faFile,
faFileArchive,
faFileCode,
faFileDownload,
faFileExport,
faFileImport,
faFilePdf,
faFolderOpen,
faGift,
faHdd,
faHome,
faHourglass,
faImage,
faInbox,
faInfo,
faInfoCircle,
faKey,
faMapSigns,
faNetworkWired,
faPause,
faPen,
faPlay,
faPlayCircle,
faPlus,
faPlusCircle,
faQuestion,
faQuestionCircle,
faRedo,
faRss,
faSave,
faSearch,
faSearchMinus,
faSearchPlus,
faServer,
faSignInAlt,
faSlidersH,
faSpinner,
faStop,
faSun,
faSync,
faSyncAlt,
faTable,
faTasks,
faTerminal,
faThLarge,
faTimes,
faTrash,
faUndo,
faUsers,
faClock,
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
library.add(faAngleDoubleLeft);
library.add(faAngleDown);
library.add(faAngleRight);
library.add(faAngleUp);
library.add(faArrowLeft);
library.add(faArrowRight);
library.add(faAt);
library.add(faBook);
library.add(faBug);
library.add(faCalendar);
library.add(faCheck);
library.add(faChevronDown);
library.add(faChevronUp);
library.add(faCode);
library.add(faCodeBranch);
library.add(faCog);
library.add(faCogs);
library.add(faClone);
library.add(faCloud);
library.add(faCloudDownloadAlt);
library.add(faCopy);
library.add(faCut);
library.add(faDotCircle);
library.add(faEdit);
library.add(faEnvelope);
library.add(faEye);
library.add(faExclamationTriangle);
library.add(faExpand);
library.add(faExternalLinkAlt);
library.add(faExchangeAlt);
library.add(faFile);
library.add(faFileArchive);
library.add(faFileCode);
library.add(faFileDownload);
library.add(faFileExport);
library.add(faFileImport);
library.add(faFilePdf);
library.add(faFolderOpen);
library.add(faGift);
library.add(faHdd);
library.add(faHome);
library.add(faHourglass);
library.add(faImage);
library.add(faInbox);
library.add(faInfo);
library.add(faInfoCircle);
library.add(faKey);
library.add(faMapSigns);
library.add(faNetworkWired);
library.add(faPause);
library.add(faPen);
library.add(faPlay);
library.add(faPlayCircle);
library.add(faPlus);
library.add(faPlusCircle);
library.add(faQuestion);
library.add(faQuestionCircle);
library.add(faRedo);
library.add(faRss);
library.add(faSave);
library.add(faSearch);
library.add(faSearchMinus);
library.add(faSearchPlus);
library.add(faServer);
library.add(faSignInAlt);
library.add(faSlidersH);
library.add(faSpinner);
library.add(faStop);
library.add(faSun);
library.add(faSync);
library.add(faSyncAlt);
library.add(faTable);
library.add(faTasks);
library.add(faTerminal);
library.add(faThLarge);
library.add(faTimes);
library.add(faTrash);
library.add(faUndo);
library.add(faUsers);
library.add(faClock);

View file

@ -0,0 +1 @@
@import url('https://fonts.googleapis.com/css?family=Open+Sans&display=swap');

View file

@ -0,0 +1,38 @@
const path = require('path');
module.exports = {
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'storybook-addon-designs',
'storybook-addon-themes',
],
webpackFinal: async (config, { configType }) => {
config.module.rules.push({
test: /\.scss$/,
oneOf: [
{
resourceQuery: /module/,
use: [
'vue-style-loader',
{
loader: 'css-loader',
options: {
modules: true,
},
},
'sass-loader',
],
include: path.resolve(__dirname, '../'),
},
{
use: ['vue-style-loader', 'css-loader', 'sass-loader'],
include: path.resolve(__dirname, '../'),
},
],
});
return config;
},
};

View file

@ -0,0 +1,65 @@
import './font-awesome-icons';
import './storybook.scss';
import lang from 'element-ui/lib/locale/lang/en';
import locale from 'element-ui/lib/locale';
import Vue from 'vue';
locale.use(lang);
// https://github.com/storybookjs/storybook/issues/6153
Vue.prototype.toJSON = function () {
return this;
};
export const parameters = {
actions: {
argTypesRegex: '^on[A-Z].*',
},
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
backgrounds: {
default: '--color-background-xlight',
values: [
{
name: '--color-background-dark',
value: 'var(--color-background-dark)',
},
{
name: '--color-background-base',
value: 'var(--color-background-base)',
},
{
name: '--color-background-light',
value: 'var(--color-background-light)',
},
{
name: '--color-background-lighter',
value: 'var(--color-background-lighter)',
},
{
name: '--color-background-xlight',
value: 'var(--color-background-xlight)',
},
],
},
themes: {
list: [
{
name: 'dark',
class: 'theme-dark',
color: '#000',
},
],
},
options: {
storySort: {
order: ['Docs', 'Styleguide', 'Atoms'],
},
},
};

View file

@ -0,0 +1,12 @@
@use "./fonts.scss";
@use "~/theme/src/base.scss" with (
$font-path: '~element-ui/lib/theme-chalk/fonts',
);
@use "~/theme/src/reset.scss";
@use "~/theme/src/index.scss";
.multi-container > * {
margin-bottom: 10px;
}

View file

@ -0,0 +1,228 @@
“Commons Clause” License Condition v1.0
The Software is provided to you by the Licensor under the
License, as defined below, subject to the following condition.
Without limiting other conditions in the License, the grant
of rights under the License will not include, and the License
does not grant to you, the right to Sell the Software.
For purposes of the foregoing, “Sell” means practicing any or
all of the rights granted to you under the License to provide
to third parties, for a fee or other consideration (including
without limitation fees for hosting or consulting/ support
services related to the Software), a product or service whose
value derives, entirely or substantially, from the functionality
of the Software. Any license notice or attribution required by
the License must also include this Commons Clause License
Condition notice.
Software: n8n
License: Apache 2.0 with Commons Clause
Licensor: n8n GmbH
---
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2021 n8n GmbH
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -0,0 +1,45 @@
# n8n-design-system
A component system for [n8n](https://n8n.io) using Storybook to preview.
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run storybook
```
### Build static pages
```
npm run build:storybook
```
### Run your unit tests
```
npm run test:unit
```
### Lints and fixes files
```
npm run lint
```
### Build css files
```
npm run build:theme
```
### Monitor theme files and build any changes
```
npm run watch:theme
```

View file

@ -0,0 +1,6 @@
module.exports = {
presets: ['@vue/cli-plugin-babel/preset'],
plugins: [
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
],
};

View file

@ -0,0 +1,36 @@
'use strict';
const gulp = require('gulp');
const sass = require('gulp-dart-sass');
const autoprefixer = require('gulp-autoprefixer');
const cleanCSS = require('gulp-clean-css');
gulp.task('build:theme', gulp.series([compileTheme, copyThemeFonts]));
gulp.task(
'watch:theme',
gulp.series([
'build:theme',
() => {
gulp.watch('./theme/src/**/*.scss', gulp.series(['build:theme']));
},
]),
);
function compileTheme() {
return gulp
.src('./theme/src/index.scss')
.pipe(sass.sync())
.pipe(
autoprefixer({
browsers: ['ie > 9', 'last 2 versions'],
cascade: false,
}),
)
.pipe(cleanCSS())
.pipe(gulp.dest('./theme/dist'));
}
function copyThemeFonts() {
return gulp.src('./theme/src/fonts/**').pipe(gulp.dest('./theme/dist/fonts'));
}

View file

@ -0,0 +1,3 @@
module.exports = {
preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',
};

View file

@ -0,0 +1,80 @@
{
"name": "n8n-design-system",
"version": "0.1.0",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
"author": {
"name": "Mutasem Aldmour",
"email": "mutasem@n8n.io"
},
"main": "src/main.js",
"repository": {
"type": "git",
"url": "git+https://github.com/n8n-io/n8n.git"
},
"scripts": {
"build": "npm run build:theme",
"build:vue": "vue-cli-service build --target lib ./src/main.js --report",
"dev": "npm run watch:theme",
"test": "npm run test:unit",
"build:storybook": "build-storybook",
"storybook": "start-storybook -p 6006",
"test:unit": "vue-cli-service test:unit --passWithNoTests",
"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"
},
"peerDependencies": {
"@fortawesome/fontawesome-svg-core": "1.x",
"@fortawesome/free-solid-svg-icons": "5.x",
"@fortawesome/vue-fontawesome": "2.x",
"core-js": "3.x",
"element-ui": "2.13.x"
},
"devDependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.35",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@fortawesome/vue-fontawesome": "^2.0.2",
"core-js": "^3.6.5",
"element-ui": "~2.13.0",
"storybook-addon-themes": "^6.1.0",
"vue": "^2.6.11",
"vue-class-component": "^7.2.3",
"vue-property-decorator": "^9.1.2",
"@babel/core": "^7.14.6",
"@storybook/addon-actions": "^6.3.6",
"@storybook/addon-essentials": "^6.3.6",
"@storybook/addon-links": "^6.3.6",
"@storybook/vue": "^6.3.6",
"@types/jest": "^26.0.13",
"@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.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": "^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.3.2",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",
"storybook-addon-designs": "^6.0.1",
"typescript": "~4.3.5",
"vue-loader": "^15.9.7",
"vue-template-compiler": "^2.6.11",
"gulp-autoprefixer": "^4.0.0",
"gulp-clean-css": "^4.3.0",
"gulp-dart-sass": "^1.0.2",
"node-notifier": ">=8.0.1",
"trim": ">=0.0.3"
}
}

View file

@ -0,0 +1,122 @@
import N8nButton from './Button.vue';
import { action } from '@storybook/addon-actions';
export default {
title: 'Atoms/Button',
component: N8nButton,
argTypes: {
label: {
control: 'text',
},
title: {
control: 'text',
},
type: {
control: 'select',
options: ['primary', 'outline', 'light', 'text'],
},
size: {
control: {
type: 'select',
options: ['small', 'medium', 'large'],
},
},
loading: {
control: {
type: 'boolean',
},
},
icon: {
control: {
type: 'text',
},
},
iconSize: {
control: {
type: 'select',
options: ['small', 'medium', 'large'],
},
},
circle: {
control: {
type: 'boolean',
},
},
fullWidth: {
type: 'boolean',
},
theme: {
type: 'select',
options: ['success', 'danger', 'warning'],
},
float: {
type: 'select',
options: ['left', 'right'],
},
},
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/DxLbnIyMK8X0uLkUguFV4n/n8n-design-system_v1?node-id=5%3A1147',
},
},
};
const methods = {
onClick: action('click'),
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: {
N8nButton,
},
template: '<n8n-button v-bind="$props" @click="onClick" />',
methods,
});
export const Button = Template.bind({});
Button.args = {
label: 'Button',
};
const ManyTemplate = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: {
N8nButton,
},
template:
'<div> <n8n-button v-bind="$props" size="large" @click="onClick" /> <n8n-button v-bind="$props" size="medium" @click="onClick" /> <n8n-button v-bind="$props" size="small" @click="onClick" /> <n8n-button v-bind="$props" :loading="true" @click="onClick" /> <n8n-button v-bind="$props" :disabled="true" @click="onClick" /></div>',
methods,
});
export const Primary = ManyTemplate.bind({});
Primary.args = {
type: 'primary',
label: 'Button',
};
export const Outline = ManyTemplate.bind({});
Outline.args = {
type: 'outline',
label: 'Button',
};
export const Light = ManyTemplate.bind({});
Light.args = {
type: 'light',
label: 'Button',
};
export const WithIcon = ManyTemplate.bind({});
WithIcon.args = {
label: 'Button',
icon: 'plus-circle',
};
export const Text = ManyTemplate.bind({});
Text.args = {
type: 'text',
label: 'Button',
icon: 'plus-circle',
};

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