mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-10 06:34:05 -08:00
Merge branch 'n8n-io:master' into Add-schema-registry-into-kafka
This commit is contained in:
commit
fa7ab27c94
|
@ -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
3
.eslintignore
Normal file
|
@ -0,0 +1,3 @@
|
|||
packages/nodes-base
|
||||
packages/editor-ui
|
||||
packages/design-system
|
354
.eslintrc.js
Normal file
354
.eslintrc.js
Normal 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',
|
||||
},
|
||||
};
|
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
|
@ -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
5
.gitignore
vendored
|
@ -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
3
.prettierignore
Normal file
|
@ -0,0 +1,3 @@
|
|||
packages/nodes-base
|
||||
packages/editor-ui
|
||||
packages/design-system
|
51
.prettierrc.js
Normal file
51
.prettierrc.js
Normal 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
8
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"EditorConfig.EditorConfig",
|
||||
"esbenp.prettier-vscode",
|
||||
"octref.vetur"
|
||||
]
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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/
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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?
|
||||
|
|
14
packages/cli/commands/Interfaces.d.ts
vendored
14
packages/cli/commands/Interfaces.d.ts
vendored
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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('====================================');
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
})();
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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.`);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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]]);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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()
|
||||
|
|
|
@ -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);
|
||||
|
|
186
packages/cli/src/WaitTracker.ts
Normal file
186
packages/cli/src/WaitTracker.ts
Normal 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;
|
||||
}
|
167
packages/cli/src/WaitingWebhooks.ts
Normal file
167
packages/cli/src/WaitingWebhooks.ts
Normal 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);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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`');
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
];
|
||||
|
|
|
@ -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"`);
|
||||
}
|
||||
|
||||
}
|
|
@ -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,
|
||||
];
|
||||
|
|
|
@ -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;`);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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,
|
||||
];
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
describe('Placeholder', () => {
|
||||
|
||||
test('example', () => {
|
||||
expect(1 + 1).toEqual(2);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -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
3
packages/design-system/.browserslistrc
Normal file
3
packages/design-system/.browserslistrc
Normal file
|
@ -0,0 +1,3 @@
|
|||
> 1%
|
||||
last 2 versions
|
||||
not ie <= 8
|
19
packages/design-system/.eslintrc.js
Normal file
19
packages/design-system/.eslintrc.js
Normal 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',
|
||||
},
|
||||
};
|
26
packages/design-system/.npmignore
Normal file
26
packages/design-system/.npmignore
Normal 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
|
169
packages/design-system/.storybook/font-awesome-icons.js
Normal file
169
packages/design-system/.storybook/font-awesome-icons.js
Normal 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);
|
1
packages/design-system/.storybook/fonts.scss
Normal file
1
packages/design-system/.storybook/fonts.scss
Normal file
|
@ -0,0 +1 @@
|
|||
@import url('https://fonts.googleapis.com/css?family=Open+Sans&display=swap');
|
38
packages/design-system/.storybook/main.js
Normal file
38
packages/design-system/.storybook/main.js
Normal 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;
|
||||
},
|
||||
};
|
65
packages/design-system/.storybook/preview.js
Normal file
65
packages/design-system/.storybook/preview.js
Normal 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'],
|
||||
},
|
||||
},
|
||||
};
|
12
packages/design-system/.storybook/storybook.scss
Normal file
12
packages/design-system/.storybook/storybook.scss
Normal 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;
|
||||
}
|
228
packages/design-system/LICENSE.md
Normal file
228
packages/design-system/LICENSE.md
Normal 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.
|
45
packages/design-system/README.md
Normal file
45
packages/design-system/README.md
Normal 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
|
||||
```
|
6
packages/design-system/babel.config.js
Normal file
6
packages/design-system/babel.config.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
presets: ['@vue/cli-plugin-babel/preset'],
|
||||
plugins: [
|
||||
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
|
||||
],
|
||||
};
|
36
packages/design-system/gulpfile.js
Normal file
36
packages/design-system/gulpfile.js
Normal 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'));
|
||||
}
|
3
packages/design-system/jest.config.js
Normal file
3
packages/design-system/jest.config.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',
|
||||
};
|
80
packages/design-system/package.json
Normal file
80
packages/design-system/package.json
Normal 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"
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Reference in a new issue