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

5
.gitignore vendored
View file

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

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
execution, active webhooks and
workflows
- [/packages/design-system](/packages/design-system) - Vue frontend components
- [/packages/editor-ui](/packages/editor-ui) - Vue frontend workflow editor
- [/packages/node-dev](/packages/node-dev) - CLI to create new n8n-nodes
- [/packages/nodes-base](/packages/nodes-base) - Base n8n nodes

View file

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

View file

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

View file

@ -2,6 +2,49 @@
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
### What changed?

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -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';
// eslint-disable-next-line import/no-extraneous-dependencies
import { get } from 'lodash';
import {
ActiveExecutions,
ExternalHooks,
GenericHelpers,
IExecutionDb,
IResponseCallbackData,
IWorkflowDb,
IWorkflowExecutionDataProcess,
ResponseHelper,
WorkflowCredentials,
WorkflowExecuteAdditionalData,
WorkflowHelpers,
WorkflowRunner,
} from './';
import {
BINARY_ENCODING,
NodeExecuteFunctions,
} from 'n8n-core';
import { BINARY_ENCODING, NodeExecuteFunctions } from 'n8n-core';
import {
IBinaryKeyData,
@ -29,13 +25,28 @@ import {
IRunExecutionData,
IWebhookData,
IWebhookResponseData,
IWorkflowDataProxyAdditionalKeys,
IWorkflowExecuteAdditionalData,
LoggerProxy as Logger,
NodeHelpers,
Workflow,
WorkflowExecuteMode,
} from 'n8n-workflow';
// eslint-disable-next-line import/no-cycle
import {
ActiveExecutions,
GenericHelpers,
IExecutionDb,
IResponseCallbackData,
IWorkflowDb,
IWorkflowExecutionDataProcess,
ResponseHelper,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
WorkflowCredentials,
WorkflowExecuteAdditionalData,
WorkflowHelpers,
WorkflowRunner,
} from '.';
const activeExecutions = ActiveExecutions.getInstance();
@ -47,7 +58,12 @@ const activeExecutions = ActiveExecutions.getInstance();
* @param {Workflow} workflow
* @returns {IWebhookData[]}
*/
export function getWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, destinationNode?: string): IWebhookData[] {
export function getWorkflowWebhooks(
workflow: Workflow,
additionalData: IWorkflowExecuteAdditionalData,
destinationNode?: string,
ignoreRestartWehbooks = false,
): IWebhookData[] {
// Check all the nodes in the workflow if they have webhooks
const returnData: IWebhookData[] = [];
@ -63,9 +79,13 @@ export function getWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflo
if (parentNodes !== undefined && !parentNodes.includes(node.name)) {
// If parentNodes are given check only them if they have webhooks
// and no other ones
// eslint-disable-next-line no-continue
continue;
}
returnData.push.apply(returnData, NodeHelpers.getNodeWebhooks(workflow, node, additionalData));
returnData.push.apply(
returnData,
NodeHelpers.getNodeWebhooks(workflow, node, additionalData, ignoreRestartWehbooks),
);
}
return returnData;
@ -91,7 +111,6 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
return returnData;
}
/**
* Executes a webhook
*
@ -106,7 +125,19 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
* @param {((error: Error | null, data: IResponseCallbackData) => void)} responseCallback
* @returns {(Promise<string | undefined>)}
*/
export async function executeWebhook(workflow: Workflow, webhookData: IWebhookData, workflowData: IWorkflowDb, workflowStartNode: INode, executionMode: WorkflowExecuteMode, sessionId: string | undefined, req: express.Request, res: express.Response, responseCallback: (error: Error | null, data: IResponseCallbackData) => void): Promise<string | undefined> {
export async function executeWebhook(
workflow: Workflow,
webhookData: IWebhookData,
workflowData: IWorkflowDb,
workflowStartNode: INode,
executionMode: WorkflowExecuteMode,
sessionId: string | undefined,
runExecutionData: IRunExecutionData | undefined,
executionId: string | undefined,
req: express.Request,
res: express.Response,
responseCallback: (error: Error | null, data: IResponseCallbackData) => void,
): Promise<string | undefined> {
// Get the nodeType to know which responseMode is set
const nodeType = workflow.nodeTypes.getByName(workflowStartNode.type);
if (nodeType === undefined) {
@ -115,9 +146,25 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
throw new ResponseHelper.ResponseError(errorMessage, 500, 500);
}
const additionalKeys: IWorkflowDataProxyAdditionalKeys = {
$executionId: executionId,
};
// Get the responseMode
const responseMode = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseMode'], executionMode, 'onReceived');
const responseCode = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseCode'], executionMode, 200) as number;
const responseMode = workflow.expression.getSimpleParameterValue(
workflowStartNode,
webhookData.webhookDescription.responseMode,
executionMode,
additionalKeys,
'onReceived',
);
const responseCode = workflow.expression.getSimpleParameterValue(
workflowStartNode,
webhookData.webhookDescription.responseCode,
executionMode,
additionalKeys,
200,
) as number;
if (!['onReceived', 'lastNode'].includes(responseMode as string)) {
// If the mode is not known we error. Is probably best like that instead of using
@ -129,8 +176,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
}
// Prepare everything that is needed to run the workflow
const credentials = await WorkflowCredentials(workflowData.nodes);
const additionalData = await WorkflowExecuteAdditionalData.getBase(credentials);
const additionalData = await WorkflowExecuteAdditionalData.getBase();
// Add the Response and Request so that this data can be accessed in the node
additionalData.httpRequest = req;
@ -144,7 +190,13 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
let webhookResultData: IWebhookResponseData;
try {
webhookResultData = await workflow.runWebhook(webhookData, workflowStartNode, additionalData, NodeExecuteFunctions, executionMode);
webhookResultData = await workflow.runWebhook(
webhookData,
workflowStartNode,
additionalData,
NodeExecuteFunctions,
executionMode,
);
} catch (err) {
// Send error response to webhook caller
const errorMessage = 'Workflow Webhook Error: Workflow could not be started!';
@ -175,22 +227,34 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
// Save static data if it changed
await WorkflowHelpers.saveStaticData(workflow);
if (webhookData.webhookDescription['responseHeaders'] !== undefined) {
const responseHeaders = workflow.expression.getComplexParameterValue(workflowStartNode, webhookData.webhookDescription['responseHeaders'], executionMode, undefined) as {
entries?: Array<{
name: string;
value: string;
}> | undefined;
const additionalKeys: IWorkflowDataProxyAdditionalKeys = {
$executionId: executionId,
};
if (responseHeaders !== undefined && responseHeaders['entries'] !== undefined) {
for (const item of responseHeaders['entries']) {
res.setHeader(item['name'], item['value']);
if (webhookData.webhookDescription.responseHeaders !== undefined) {
const responseHeaders = workflow.expression.getComplexParameterValue(
workflowStartNode,
webhookData.webhookDescription.responseHeaders,
executionMode,
additionalKeys,
undefined,
) as {
entries?:
| Array<{
name: string;
value: string;
}>
| undefined;
};
if (responseHeaders !== undefined && responseHeaders.entries !== undefined) {
for (const item of responseHeaders.entries) {
res.setHeader(item.name, item.value);
}
}
}
if (webhookResultData.noWebhookResponse === true && didSendResponse === false) {
if (webhookResultData.noWebhookResponse === true && !didSendResponse) {
// The response got already send
responseCallback(null, {
noWebhookResponse: true,
@ -202,7 +266,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
// Workflow should not run
if (webhookResultData.webhookResponse !== undefined) {
// Data to respond with is given
if (didSendResponse === false) {
if (!didSendResponse) {
responseCallback(null, {
data: webhookResultData.webhookResponse,
responseCode,
@ -211,7 +275,8 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
}
} else {
// Send default response
if (didSendResponse === false) {
// eslint-disable-next-line no-lonely-if
if (!didSendResponse) {
responseCallback(null, {
data: {
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
// directly if responseMode it set to "onReceived" and a respone should be sent
if (responseMode === 'onReceived' && didSendResponse === false) {
if (responseMode === 'onReceived' && !didSendResponse) {
// Return response directly and do not wait for the workflow to finish
if (webhookResultData.webhookResponse !== undefined) {
// Data to respond with is given
@ -248,18 +313,17 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
// Initialize the data of the webhook node
const nodeExecutionStack: IExecuteData[] = [];
nodeExecutionStack.push(
{
nodeExecutionStack.push({
node: workflowStartNode,
data: {
main: webhookResultData.workflowData,
},
}
);
});
const runExecutionData: IRunExecutionData = {
startData: {
},
runExecutionData =
runExecutionData ||
({
startData: {},
resultData: {
runData: {},
},
@ -268,7 +332,14 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
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 data to merge got defined add it to the execution data
@ -276,7 +347,6 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
}
const runData: IWorkflowExecutionDataProcess = {
credentials,
executionMode,
executionData: runExecutionData,
sessionId,
@ -285,15 +355,21 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
// Start now to run the workflow
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
const executePromise = activeExecutions.getPostExecutePromise(executionId) as Promise<IExecutionDb | undefined>;
executePromise.then((data) => {
const executePromise = activeExecutions.getPostExecutePromise(executionId) as Promise<
IExecutionDb | undefined
>;
executePromise
.then((data) => {
if (data === undefined) {
if (didSendResponse === false) {
if (!didSendResponse) {
responseCallback(null, {
data: {
message: 'Workflow did execute sucessfully but no data got returned.',
@ -307,7 +383,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data);
if (data.data.resultData.error || returnData?.error !== undefined) {
if (didSendResponse === false) {
if (!didSendResponse) {
responseCallback(null, {
data: {
message: 'Workflow did error.',
@ -320,10 +396,11 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
}
if (returnData === undefined) {
if (didSendResponse === false) {
if (!didSendResponse) {
responseCallback(null, {
data: {
message: 'Workflow did execute sucessfully but the last node did not return any data.',
message:
'Workflow did execute sucessfully but the last node did not return any data.',
},
responseCode,
});
@ -332,9 +409,19 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
return data;
}
const responseData = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseData'], executionMode, 'firstEntryJson');
const additionalKeys: IWorkflowDataProxyAdditionalKeys = {
$executionId: executionId,
};
if (didSendResponse === false) {
const responseData = workflow.expression.getSimpleParameterValue(
workflowStartNode,
webhookData.webhookDescription.responseData,
executionMode,
additionalKeys,
'firstEntryJson',
);
if (!didSendResponse) {
let data: IDataObject | IDataObject[];
if (responseData === 'firstEntryJson') {
@ -347,20 +434,36 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
data = returnData.data!.main[0]![0].json;
const responsePropertyName = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responsePropertyName'], executionMode, undefined);
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, undefined);
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)) {
if (
data !== null &&
data !== undefined &&
['Buffer', 'String'].includes(data.constructor.name)
) {
res.end(data);
} else {
res.end(JSON.stringify(data));
@ -371,7 +474,6 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
});
didSendResponse = true;
}
} else if (responseData === 'firstEntryBinary') {
// Return the binary data of the first entry
data = returnData.data!.main[0]![0];
@ -386,20 +488,33 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
didSendResponse = true;
}
const responseBinaryPropertyName = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseBinaryPropertyName'], executionMode, 'data');
const responseBinaryPropertyName = workflow.expression.getSimpleParameterValue(
workflowStartNode,
webhookData.webhookDescription.responseBinaryPropertyName,
executionMode,
additionalKeys,
'data',
);
if (responseBinaryPropertyName === undefined && didSendResponse === false) {
if (responseBinaryPropertyName === undefined && !didSendResponse) {
responseCallback(new Error('No "responseBinaryPropertyName" is set.'), {});
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.`), {});
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 === false) {
if (!didSendResponse) {
// Send the webhook response manually
res.setHeader('Content-Type', binaryData.mimeType);
res.end(Buffer.from(binaryData.data, BINARY_ENCODING));
@ -408,7 +523,6 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
noWebhookResponse: true,
});
}
} else {
// Return the JSON data of all the entries
data = [];
@ -417,7 +531,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
}
}
if (didSendResponse === false) {
if (!didSendResponse) {
responseCallback(null, {
data,
responseCode,
@ -429,17 +543,17 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
return data;
})
.catch((e) => {
if (didSendResponse === false) {
if (!didSendResponse) {
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;
} catch (e) {
if (didSendResponse === false) {
if (!didSendResponse) {
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
*
@ -463,6 +576,9 @@ export function getWebhookBaseUrl() {
// @ts-ignore
urlBaseWebhook = process.env.WEBHOOK_TUNNEL_URL || process.env.WEBHOOK_URL;
}
if (!urlBaseWebhook.endsWith('/')) {
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 {
readFileSync,
} from 'fs';
import {
getConnectionManager,
} from 'typeorm';
import { readFileSync } from 'fs';
import { getConnectionManager } from 'typeorm';
import * as bodyParser from 'body-parser';
require('body-parser-xml')(bodyParser);
// eslint-disable-next-line import/no-extraneous-dependencies, @typescript-eslint/no-unused-vars
import * as _ from 'lodash';
import * as compression from 'compression';
// eslint-disable-next-line import/no-extraneous-dependencies
import * as parseUrl from 'parseurl';
// eslint-disable-next-line import/no-cycle
import {
ActiveExecutions,
ActiveWorkflowRunner,
@ -19,20 +27,32 @@ import {
IExternalHooksClass,
IPackageVersions,
ResponseHelper,
} from './';
WaitingWebhooks,
} from '.';
import * as compression from 'compression';
import * as config from '../config';
import * as parseUrl from 'parseurl';
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-call
require('body-parser-xml')(bodyParser);
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function registerProductionWebhooks() {
// ----------------------------------------
// Regular Webhooks
// ----------------------------------------
// HEAD webhook requests
this.app.head(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => {
this.app.head(
`/${this.endpointWebhook}/*`,
async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhook.length + 2);
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
this.endpointWebhook.length + 2,
);
let response;
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
response = await this.activeWorkflowRunner.executeWebhook('HEAD', requestUrl, req, res);
} catch (error) {
ResponseHelper.sendErrorResponse(res, error);
@ -45,12 +65,17 @@ export function registerProductionWebhooks() {
}
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
});
},
);
// OPTIONS webhook requests
this.app.options(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => {
this.app.options(
`/${this.endpointWebhook}/*`,
async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhook.length + 2);
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
this.endpointWebhook.length + 2,
);
let allowedMethods: string[];
try {
@ -65,12 +90,17 @@ export function registerProductionWebhooks() {
}
ResponseHelper.sendSuccessResponse(res, {}, true, 204);
});
},
);
// GET webhook requests
this.app.get(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => {
this.app.get(
`/${this.endpointWebhook}/*`,
async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhook.length + 2);
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
this.endpointWebhook.length + 2,
);
let response;
try {
@ -86,12 +116,17 @@ export function registerProductionWebhooks() {
}
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
});
},
);
// POST webhook requests
this.app.post(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => {
this.app.post(
`/${this.endpointWebhook}/*`,
async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhook.length + 2);
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
this.endpointWebhook.length + 2,
);
let response;
try {
@ -107,27 +142,129 @@ export function registerProductionWebhooks() {
}
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 {
app: express.Application;
activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner;
endpointWebhook: string;
endpointWebhookWaiting: string;
endpointPresetCredentials: string;
externalHooks: IExternalHooksClass;
saveDataErrorExecution: string;
saveDataSuccessExecution: string;
saveManualExecutions: boolean;
executionTimeout: number;
maxExecutionTimeout: number;
timezone: string;
activeExecutionsInstance: ActiveExecutions.ActiveExecutions;
versions: IPackageVersions | undefined;
restEndpoint: string;
protocol: string;
sslKey: string;
sslCert: string;
presetCredentialsLoaded: boolean;
@ -136,6 +273,7 @@ class App {
this.app = express();
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.saveDataSuccessExecution = config.get('executions.saveDataOnSuccess') as string;
this.saveManualExecutions = config.get('executions.saveDataManualExecutions') as boolean;
@ -158,7 +296,6 @@ class App {
this.endpointPresetCredentials = config.get('credentials.overwrite.endpoint') as string;
}
/**
* Returns the current epoch time
*
@ -169,9 +306,7 @@ class App {
return new Date();
}
async config(): Promise<void> {
this.versions = await GenericHelpers.getVersions();
// Compress the response data
@ -186,49 +321,63 @@ class App {
});
// Support application/json type post data
this.app.use(bodyParser.json({
limit: '16mb', verify: (req, res, buf) => {
// @ts-ignore
req.rawBody = buf;
},
}));
// Support application/xml type post data
// @ts-ignore
this.app.use(bodyParser.xml({
limit: '16mb', xmlParseOptions: {
normalize: true, // Trim whitespace inside text nodes
normalizeTags: true, // Transform tags to lowercase
explicitArray: false, // Only put properties in array if length > 1
},
}));
this.app.use(bodyParser.text({
limit: '16mb', verify: (req, res, buf) => {
// @ts-ignore
req.rawBody = buf;
},
}));
//support application/x-www-form-urlencoded post data
this.app.use(bodyParser.urlencoded({ extended: false,
this.app.use(
bodyParser.json({
limit: '16mb',
verify: (req, res, buf) => {
// @ts-ignore
req.rawBody = buf;
},
}));
}),
);
if (process.env['NODE_ENV'] !== 'production') {
// Support application/xml type post data
this.app.use(
// @ts-ignore
bodyParser.xml({
limit: '16mb',
xmlParseOptions: {
normalize: true, // Trim whitespace inside text nodes
normalizeTags: true, // Transform tags to lowercase
explicitArray: false, // Only put properties in array if length > 1
},
}),
);
this.app.use(
bodyParser.text({
limit: '16mb',
verify: (req, res, buf) => {
// @ts-ignore
req.rawBody = buf;
},
}),
);
// support application/x-www-form-urlencoded post data
this.app.use(
bodyParser.urlencoded({
extended: false,
verify: (req, res, buf) => {
// @ts-ignore
req.rawBody = buf;
},
}),
);
if (process.env.NODE_ENV !== 'production') {
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
// Allow access also from frontend when developing
res.header('Access-Control-Allow-Origin', 'http://localhost:8080');
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, sessionid');
res.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept, sessionid',
);
next();
});
}
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
if (Db.collections.Workflow === null) {
const error = new ResponseHelper.ResponseError('Database is not ready!', undefined, 503);
@ -238,26 +387,24 @@ class App {
next();
});
// ----------------------------------------
// Healthcheck
// ----------------------------------------
// Does very basic health check
this.app.get('/healthz', async (req: express.Request, res: express.Response) => {
const connection = getConnectionManager().get();
const connectionManager = getConnectionManager();
if (connectionManager.connections.length === 0) {
const error = new ResponseHelper.ResponseError('No Database connection found!', undefined, 503);
return ResponseHelper.sendErrorResponse(res, error);
}
if (connectionManager.connections[0].isConnected === false) {
try {
if (!connection.isConnected) {
// Connection is not active
const error = new ResponseHelper.ResponseError('Database connection not active!', undefined, 503);
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);
}
@ -270,9 +417,7 @@ class App {
});
registerProductionWebhooks.apply(this);
}
}
export async function start(): Promise<void> {
@ -286,12 +431,14 @@ export async function start(): Promise<void> {
let server;
if (app.protocol === 'https' && app.sslKey && app.sslCert) {
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
const https = require('https');
const privateKey = readFileSync(app.sslKey, 'utf8');
const cert = readFileSync(app.sslCert, 'utf8');
const credentials = { key: privateKey, cert };
server = https.createServer(credentials, app.app);
} else {
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
const http = require('http');
server = http.createServer(app.app);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,27 +1,13 @@
import {
WorkflowExecuteMode,
} from 'n8n-workflow';
/* eslint-disable import/no-cycle */
import { WorkflowExecuteMode } from 'n8n-workflow';
import {
IExecutionFlattedDb,
IWorkflowDb,
} from '../../';
import { Column, ColumnOptions, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
import { IExecutionFlattedDb, IWorkflowDb } from '../..';
import {
resolveDataType
} from '../utils';
import {
Column,
ColumnOptions,
Entity,
Index,
PrimaryGeneratedColumn,
} from 'typeorm';
import { resolveDataType } from '../utils';
@Entity()
export class ExecutionEntity implements IExecutionFlattedDb {
@PrimaryGeneratedColumn()
id: number;
@ -53,4 +39,8 @@ export class ExecutionEntity implements IExecutionFlattedDb {
@Index()
@Column({ nullable: true })
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 { ITagDb } from '../../Interfaces';
@ -7,7 +18,6 @@ import { getTimestampSyntax } from '../utils';
@Entity()
export class TagEntity implements ITagDb {
@PrimaryGeneratedColumn()
id: number;
@ -22,12 +32,16 @@ export class TagEntity implements ITagDb {
@IsDate()
createdAt: Date;
@UpdateDateColumn({ precision: 3, default: () => getTimestampSyntax(), onUpdate: getTimestampSyntax() })
@UpdateDateColumn({
precision: 3,
default: () => getTimestampSyntax(),
onUpdate: getTimestampSyntax(),
})
@IsOptional() // ignored by validation because set at DB level
@IsDate()
updatedAt: Date;
@ManyToMany(() => WorkflowEntity, workflow => workflow.tags)
@ManyToMany(() => WorkflowEntity, (workflow) => workflow.tags)
workflows: WorkflowEntity[];
@BeforeUpdate()

View file

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

View file

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

View file

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

View file

@ -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 { UniqueWorkflowNames1620826335440 } from './1620826335440-UniqueWorkflowNames';
import { CertifyCorrectCollation1623936588000 } from './1623936588000-CertifyCorrectCollation';
import { AddWaitColumnId1626183952959 } from './1626183952959-AddWaitColumn';
export const mysqlMigrations = [
InitialMigration1588157391238,
@ -20,4 +21,5 @@ export const mysqlMigrations = [
CreateTagEntity1617268711084,
UniqueWorkflowNames1620826335440,
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 { CreateTagEntity1617270242566 } from './1617270242566-CreateTagEntity';
import { UniqueWorkflowNames1620824779533 } from './1620824779533-UniqueWorkflowNames';
import { AddwaitTill1626176912946 } from './1626176912946-AddwaitTill';
export const postgresMigrations = [
InitialMigration1587669153312,
@ -14,4 +15,5 @@ export const postgresMigrations = [
MakeStoppedAtNullable1607431743768,
CreateTagEntity1617270242566,
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 { CreateTagEntity1617213344594 } from './1617213344594-CreateTagEntity';
import { UniqueWorkflowNames1620821879465 } from './1620821879465-UniqueWorkflowNames';
import { AddWaitColumn1621707690587 } from './1621707690587-AddWaitColumn';
export const sqliteMigrations = [
InitialMigration1588102412422,
@ -14,4 +15,5 @@ export const sqliteMigrations = [
MakeStoppedAtNullable1607431743769,
CreateTagEntity1617213344594,
UniqueWorkflowNames1620821879465,
AddWaitColumn1621707690587,
];

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,4 +5,6 @@ export const EXTENSIONS_SUBDIRECTORY = 'custom';
export const USER_FOLDER_ENV_OVERWRITE = 'N8N_USER_FOLDER';
export const USER_SETTINGS_FILE_NAME = 'config';
export const USER_SETTINGS_SUBFOLDER = '.n8n';
export const PLACEHOLDER_EMPTY_EXECUTION_ID = '__UNKOWN__';
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';
export class Credentials extends ICredentials {
/**
* Returns if the given nodeType has access to data
*/
hasNodeAccess(nodeType: string): boolean {
// eslint-disable-next-line no-restricted-syntax
for (const accessData of this.nodesAccess) {
if (accessData.nodeType === nodeType) {
return true;
}
@ -25,7 +22,6 @@ export class Credentials extends ICredentials {
return false;
}
/**
* Sets new credential object
*/
@ -33,7 +29,6 @@ export class Credentials extends ICredentials {
this.data = AES.encrypt(JSON.stringify(data), encryptionKey).toString();
}
/**
* Sets new credentials for given key
*/
@ -50,13 +45,14 @@ export class Credentials extends ICredentials {
return this.setData(fullData, encryptionKey);
}
/**
* Returns the decrypted credential object
*/
getData(encryptionKey: string, nodeType?: string): ICredentialDataDecryptedObject {
if (nodeType && !this.hasNodeAccess(nodeType)) {
throw new Error(`The node of type "${nodeType}" does not have access to credentials "${this.name}" of type "${this.type}".`);
throw new Error(
`The node of type "${nodeType}" does not have access to credentials "${this.name}" of type "${this.type}".`,
);
}
if (this.data === undefined) {
@ -66,13 +62,15 @@ export class Credentials extends ICredentials {
const decryptedData = AES.decrypt(this.data, encryptionKey);
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return JSON.parse(decryptedData.toString(enc.Utf8));
} catch (e) {
throw new Error('Credentials could not be decrypted. The 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
*/
@ -80,9 +78,10 @@ export class Credentials extends ICredentials {
const fullData = this.getData(encryptionKey, nodeType);
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)) {
throw new Error(`No data for key "${key}" exists.`);
}
@ -90,13 +89,12 @@ export class Credentials extends ICredentials {
return fullData[key];
}
/**
* Returns the encrypted credentials to be saved
*/
getDataToSave(): ICredentialsEncrypted {
if (this.data === undefined) {
throw new Error(`No credentials got set to save.`);
throw new Error(`No credentials were set to save.`);
}
return {

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

@ -1,12 +1,8 @@
import { Credentials } from '../src';
describe('Credentials', () => {
describe('without nodeType set', () => {
test('should be able to set and read key data without initial data set', () => {
const credentials = new Credentials('testName', 'testType', []);
const key = 'key1';
@ -20,7 +16,6 @@ describe('Credentials', () => {
});
test('should be able to set and read key data with initial data set', () => {
const key = 'key2';
const password = 'password';
@ -39,13 +34,10 @@ describe('Credentials', () => {
// Read the data which got provided encrypted on init
expect(credentials.getDataKey('key1', password)).toEqual(initialData);
});
});
describe('with nodeType set', () => {
test('should be able to set and read key data without initial data set', () => {
const nodeAccess = [
{
nodeType: 'base.noOp',
@ -72,7 +64,9 @@ describe('Credentials', () => {
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".');
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
@ -81,8 +75,9 @@ describe('Credentials', () => {
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));
expect(dbData.data!.slice(0, 6)).toEqual(
'U2FsdGVkX1+wpQWkj+YTzaPSNTFATjnlmFKIsUTZdhk='.slice(0, 6),
);
});
});
});

View file

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