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_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
[*.ts]
|
|
||||||
quote_type = single
|
|
||||||
|
|
||||||
[*.yml]
|
[*.yml]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
3
.eslintignore
Normal file
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 bootstrap
|
||||||
npm run build --if-present
|
npm run build --if-present
|
||||||
npm test
|
npm test
|
||||||
npm run tslint
|
npm run lint
|
||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
|
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -10,7 +10,8 @@ yarn.lock
|
||||||
google-generated-credentials.json
|
google-generated-credentials.json
|
||||||
_START_PACKAGE
|
_START_PACKAGE
|
||||||
.env
|
.env
|
||||||
.vscode
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
.idea
|
.idea
|
||||||
.prettierrc.js
|
|
||||||
vetur.config.js
|
vetur.config.js
|
||||||
|
nodelinter.config.json
|
||||||
|
|
3
.prettierignore
Normal file
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
|
- [/packages/core](/packages/core) - Core code which handles workflow
|
||||||
execution, active webhooks and
|
execution, active webhooks and
|
||||||
workflows
|
workflows
|
||||||
|
- [/packages/design-system](/packages/design-system) - Vue frontend components
|
||||||
- [/packages/editor-ui](/packages/editor-ui) - Vue frontend workflow editor
|
- [/packages/editor-ui](/packages/editor-ui) - Vue frontend workflow editor
|
||||||
- [/packages/node-dev](/packages/node-dev) - CLI to create new n8n-nodes
|
- [/packages/node-dev](/packages/node-dev) - CLI to create new n8n-nodes
|
||||||
- [/packages/nodes-base](/packages/nodes-base) - Base n8n nodes
|
- [/packages/nodes-base](/packages/nodes-base) - Base n8n nodes
|
||||||
|
|
|
@ -14,6 +14,7 @@ COPY lerna.json .
|
||||||
COPY package.json .
|
COPY package.json .
|
||||||
COPY packages/cli/ ./packages/cli/
|
COPY packages/cli/ ./packages/cli/
|
||||||
COPY packages/core/ ./packages/core/
|
COPY packages/core/ ./packages/core/
|
||||||
|
COPY packages/design-system/ ./packages/design-system/
|
||||||
COPY packages/editor-ui/ ./packages/editor-ui/
|
COPY packages/editor-ui/ ./packages/editor-ui/
|
||||||
COPY packages/nodes-base/ ./packages/nodes-base/
|
COPY packages/nodes-base/ ./packages/nodes-base/
|
||||||
COPY packages/workflow/ ./packages/workflow/
|
COPY packages/workflow/ ./packages/workflow/
|
||||||
|
|
|
@ -7,12 +7,14 @@
|
||||||
"build": "lerna exec npm run build",
|
"build": "lerna exec npm run build",
|
||||||
"dev": "lerna exec npm run dev --parallel",
|
"dev": "lerna exec npm run dev --parallel",
|
||||||
"clean:dist": "lerna exec -- rimraf ./dist",
|
"clean:dist": "lerna exec -- rimraf ./dist",
|
||||||
|
"format": "lerna exec npm run format",
|
||||||
|
"lint": "lerna exec npm run lint",
|
||||||
|
"lintfix": "lerna exec npm run lintfix",
|
||||||
"optimize-svg": "find ./packages -name '*.svg' ! -name 'pipedrive.svg' -print0 | xargs -0 -P16 -L20 npx svgo",
|
"optimize-svg": "find ./packages -name '*.svg' ! -name 'pipedrive.svg' -print0 | xargs -0 -P16 -L20 npx svgo",
|
||||||
"start": "run-script-os",
|
"start": "run-script-os",
|
||||||
"start:default": "cd packages/cli/bin && ./n8n",
|
"start:default": "cd packages/cli/bin && ./n8n",
|
||||||
"start:windows": "cd packages/cli/bin && n8n",
|
"start:windows": "cd packages/cli/bin && n8n",
|
||||||
"test": "lerna run test",
|
"test": "lerna run test",
|
||||||
"tslint": "lerna exec npm run tslint",
|
|
||||||
"watch": "lerna run --parallel watch",
|
"watch": "lerna run --parallel watch",
|
||||||
"webhook": "./packages/cli/bin/n8n webhook",
|
"webhook": "./packages/cli/bin/n8n webhook",
|
||||||
"worker": "./packages/cli/bin/n8n worker"
|
"worker": "./packages/cli/bin/n8n worker"
|
||||||
|
|
|
@ -2,6 +2,49 @@
|
||||||
|
|
||||||
This list shows all the versions which include breaking changes and how to upgrade.
|
This list shows all the versions which include breaking changes and how to upgrade.
|
||||||
|
|
||||||
|
## 0.135.0
|
||||||
|
|
||||||
|
### What changed?
|
||||||
|
|
||||||
|
The in-node core methods for credentials and binary data have changed.
|
||||||
|
|
||||||
|
### When is action necessary?
|
||||||
|
|
||||||
|
If you are using custom n8n nodes.
|
||||||
|
|
||||||
|
### How to upgrade:
|
||||||
|
|
||||||
|
1. The method `this.getCredentials(myNodeCredentials)` is now async. So `await` has to be added in front of it.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Before 0.135.0:
|
||||||
|
const credentials = this.getCredentials(credentialTypeName);
|
||||||
|
|
||||||
|
// From 0.135.0:
|
||||||
|
const credentials = await this.getCredentials(myNodeCredentials);
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Binary data should not get accessed directly anymore, instead the method `await this.helpers.getBinaryDataBuffer(itemIndex, binaryPropertyName)` has to be used.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
|
||||||
|
const items = this.getInputData();
|
||||||
|
|
||||||
|
for (const i = 0; i < items.length; i++) {
|
||||||
|
const item = items[i].binary as IBinaryKeyData;
|
||||||
|
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string;
|
||||||
|
const binaryData = item[binaryPropertyName] as IBinaryData;
|
||||||
|
// Before 0.135.0:
|
||||||
|
const binaryDataBuffer = Buffer.from(binaryData.data, BINARY_ENCODING);
|
||||||
|
// From 0.135.0:
|
||||||
|
const binaryDataBuffer = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## 0.131.0
|
## 0.131.0
|
||||||
|
|
||||||
### What changed?
|
### What changed?
|
||||||
|
|
14
packages/cli/commands/Interfaces.d.ts
vendored
14
packages/cli/commands/Interfaces.d.ts
vendored
|
@ -1,14 +1,14 @@
|
||||||
interface IResult {
|
interface IResult {
|
||||||
totalWorkflows: number;
|
totalWorkflows: number;
|
||||||
summary: {
|
summary: {
|
||||||
failedExecutions: number,
|
failedExecutions: number;
|
||||||
successfulExecutions: number,
|
successfulExecutions: number;
|
||||||
warningExecutions: number,
|
warningExecutions: number;
|
||||||
errors: IExecutionError[],
|
errors: IExecutionError[];
|
||||||
warnings: IExecutionError[],
|
warnings: IExecutionError[];
|
||||||
};
|
};
|
||||||
coveredNodes: {
|
coveredNodes: {
|
||||||
[nodeType: string]: number
|
[nodeType: string]: number;
|
||||||
};
|
};
|
||||||
executions: IExecutionResult[];
|
executions: IExecutionResult[];
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ interface IExecutionResult {
|
||||||
error?: string;
|
error?: string;
|
||||||
changes?: string;
|
changes?: string;
|
||||||
coveredNodes: {
|
coveredNodes: {
|
||||||
[nodeType: string]: number
|
[nodeType: string]: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
/* eslint-disable no-console */
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import { Command, flags } from '@oclif/command';
|
import { Command, flags } from '@oclif/command';
|
||||||
import {
|
import { UserSettings } from 'n8n-core';
|
||||||
UserSettings,
|
import { INode, LoggerProxy } from 'n8n-workflow';
|
||||||
} from 'n8n-core';
|
|
||||||
import {
|
|
||||||
INode,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ActiveExecutions,
|
ActiveExecutions,
|
||||||
|
@ -17,26 +15,18 @@ import {
|
||||||
IWorkflowExecutionDataProcess,
|
IWorkflowExecutionDataProcess,
|
||||||
LoadNodesAndCredentials,
|
LoadNodesAndCredentials,
|
||||||
NodeTypes,
|
NodeTypes,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
WorkflowCredentials,
|
WorkflowCredentials,
|
||||||
WorkflowHelpers,
|
WorkflowHelpers,
|
||||||
WorkflowRunner,
|
WorkflowRunner,
|
||||||
} from '../src';
|
} from '../src';
|
||||||
|
|
||||||
import {
|
import { getLogger } from '../src/Logger';
|
||||||
getLogger,
|
|
||||||
} from '../src/Logger';
|
|
||||||
|
|
||||||
import {
|
|
||||||
LoggerProxy,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
export class Execute extends Command {
|
export class Execute extends Command {
|
||||||
static description = '\nExecutes a given workflow';
|
static description = '\nExecutes a given workflow';
|
||||||
|
|
||||||
static examples = [
|
static examples = [`$ n8n execute --id=5`, `$ n8n execute --file=workflow.json`];
|
||||||
`$ n8n execute --id=5`,
|
|
||||||
`$ n8n execute --file=workflow.json`,
|
|
||||||
];
|
|
||||||
|
|
||||||
static flags = {
|
static flags = {
|
||||||
help: flags.help({ char: 'h' }),
|
help: flags.help({ char: 'h' }),
|
||||||
|
@ -51,11 +41,12 @@ export class Execute extends Command {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
async run() {
|
async run() {
|
||||||
const logger = getLogger();
|
const logger = getLogger();
|
||||||
LoggerProxy.init(logger);
|
LoggerProxy.init(logger);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
const { flags } = this.parse(Execute);
|
const { flags } = this.parse(Execute);
|
||||||
|
|
||||||
// Start directly with the init of the database to improve startup time
|
// Start directly with the init of the database to improve startup time
|
||||||
|
@ -76,12 +67,14 @@ export class Execute extends Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
let workflowId: string | undefined;
|
let workflowId: string | undefined;
|
||||||
let workflowData: IWorkflowBase | undefined = undefined;
|
let workflowData: IWorkflowBase | undefined;
|
||||||
if (flags.file) {
|
if (flags.file) {
|
||||||
// Path to workflow is given
|
// Path to workflow is given
|
||||||
try {
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
workflowData = JSON.parse(await fs.readFile(flags.file, 'utf8'));
|
workflowData = JSON.parse(await fs.readFile(flags.file, 'utf8'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
if (error.code === 'ENOENT') {
|
if (error.code === 'ENOENT') {
|
||||||
console.info(`The file "${flags.file}" could not be found.`);
|
console.info(`The file "${flags.file}" could not be found.`);
|
||||||
return;
|
return;
|
||||||
|
@ -92,10 +85,15 @@ export class Execute extends Command {
|
||||||
|
|
||||||
// Do a basic check if the data in the file looks right
|
// Do a basic check if the data in the file looks right
|
||||||
// TODO: Later check with the help of TypeScript data if it is valid or not
|
// TODO: Later check with the help of TypeScript data if it is valid or not
|
||||||
if (workflowData === undefined || workflowData.nodes === undefined || workflowData.connections === undefined) {
|
if (
|
||||||
|
workflowData === undefined ||
|
||||||
|
workflowData.nodes === undefined ||
|
||||||
|
workflowData.connections === undefined
|
||||||
|
) {
|
||||||
console.info(`The file "${flags.file}" does not contain valid workflow data.`);
|
console.info(`The file "${flags.file}" does not contain valid workflow data.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
workflowId = workflowData.id!.toString();
|
workflowId = workflowData.id!.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,7 +103,8 @@ export class Execute extends Command {
|
||||||
if (flags.id) {
|
if (flags.id) {
|
||||||
// Id of workflow is given
|
// Id of workflow is given
|
||||||
workflowId = flags.id;
|
workflowId = flags.id;
|
||||||
workflowData = await Db.collections!.Workflow!.findOne(workflowId);
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
workflowData = await Db.collections.Workflow!.findOne(workflowId);
|
||||||
if (workflowData === undefined) {
|
if (workflowData === undefined) {
|
||||||
console.info(`The workflow with the id "${workflowId}" does not exist.`);
|
console.info(`The workflow with the id "${workflowId}" does not exist.`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
@ -139,7 +138,8 @@ export class Execute extends Command {
|
||||||
// Check if the workflow contains the required "Start" node
|
// Check if the workflow contains the required "Start" node
|
||||||
// "requiredNodeTypes" are also defined in editor-ui/views/NodeView.vue
|
// "requiredNodeTypes" are also defined in editor-ui/views/NodeView.vue
|
||||||
const requiredNodeTypes = ['n8n-nodes-base.start'];
|
const requiredNodeTypes = ['n8n-nodes-base.start'];
|
||||||
let startNode: INode | undefined = undefined;
|
let startNode: INode | undefined;
|
||||||
|
// eslint-disable-next-line no-restricted-syntax, @typescript-eslint/no-non-null-assertion
|
||||||
for (const node of workflowData!.nodes) {
|
for (const node of workflowData!.nodes) {
|
||||||
if (requiredNodeTypes.includes(node.type)) {
|
if (requiredNodeTypes.includes(node.type)) {
|
||||||
startNode = node;
|
startNode = node;
|
||||||
|
@ -151,16 +151,15 @@ export class Execute extends Command {
|
||||||
// If the workflow does not contain a start-node we can not know what
|
// If the workflow does not contain a start-node we can not know what
|
||||||
// should be executed and with which data to start.
|
// should be executed and with which data to start.
|
||||||
console.info(`The workflow does not contain a "Start" node. So it can not be executed.`);
|
console.info(`The workflow does not contain a "Start" node. So it can not be executed.`);
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const credentials = await WorkflowCredentials(workflowData!.nodes);
|
|
||||||
|
|
||||||
const runData: IWorkflowExecutionDataProcess = {
|
const runData: IWorkflowExecutionDataProcess = {
|
||||||
credentials,
|
|
||||||
executionMode: 'cli',
|
executionMode: 'cli',
|
||||||
startNodes: [startNode.name],
|
startNodes: [startNode.name],
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
workflowData: workflowData!,
|
workflowData: workflowData!,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -181,6 +180,7 @@ export class Execute extends Command {
|
||||||
logger.info(JSON.stringify(data, null, 2));
|
logger.info(JSON.stringify(data, null, 2));
|
||||||
|
|
||||||
const { error } = data.data.resultData;
|
const { error } = data.data.resultData;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-throw-literal
|
||||||
throw {
|
throw {
|
||||||
...error,
|
...error,
|
||||||
stack: error.stack,
|
stack: error.stack,
|
||||||
|
|
|
@ -1,18 +1,26 @@
|
||||||
|
/* eslint-disable @typescript-eslint/prefer-optional-chain */
|
||||||
|
/* eslint-disable array-callback-return */
|
||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
|
/* eslint-disable no-async-promise-executor */
|
||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
/* eslint-disable @typescript-eslint/unbound-method */
|
||||||
|
/* eslint-disable no-console */
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import {
|
import { Command, flags } from '@oclif/command';
|
||||||
Command,
|
|
||||||
flags,
|
|
||||||
} from '@oclif/command';
|
|
||||||
|
|
||||||
import {
|
import { UserSettings } from 'n8n-core';
|
||||||
UserSettings,
|
|
||||||
} from 'n8n-core';
|
|
||||||
|
|
||||||
import {
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
INode,
|
import { INode, INodeExecutionData, ITaskData, LoggerProxy } from 'n8n-workflow';
|
||||||
INodeExecutionData,
|
|
||||||
ITaskData,
|
import { sep } from 'path';
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
import { diff } from 'json-diff';
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
import { pick } from 'lodash';
|
||||||
|
import { getLogger } from '../src/Logger';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ActiveExecutions,
|
ActiveExecutions,
|
||||||
|
@ -20,35 +28,17 @@ import {
|
||||||
CredentialTypes,
|
CredentialTypes,
|
||||||
Db,
|
Db,
|
||||||
ExternalHooks,
|
ExternalHooks,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
IExecutionsCurrentSummary,
|
IExecutionsCurrentSummary,
|
||||||
IWorkflowDb,
|
IWorkflowDb,
|
||||||
IWorkflowExecutionDataProcess,
|
IWorkflowExecutionDataProcess,
|
||||||
LoadNodesAndCredentials,
|
LoadNodesAndCredentials,
|
||||||
NodeTypes,
|
NodeTypes,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
WorkflowCredentials,
|
WorkflowCredentials,
|
||||||
WorkflowRunner,
|
WorkflowRunner,
|
||||||
} from '../src';
|
} from '../src';
|
||||||
|
|
||||||
import {
|
|
||||||
sep,
|
|
||||||
} from 'path';
|
|
||||||
|
|
||||||
import {
|
|
||||||
diff,
|
|
||||||
} from 'json-diff';
|
|
||||||
|
|
||||||
import {
|
|
||||||
getLogger,
|
|
||||||
} from '../src/Logger';
|
|
||||||
|
|
||||||
import {
|
|
||||||
LoggerProxy,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
import {
|
|
||||||
pick,
|
|
||||||
} from 'lodash';
|
|
||||||
|
|
||||||
export class ExecuteBatch extends Command {
|
export class ExecuteBatch extends Command {
|
||||||
static description = '\nExecutes multiple workflows once';
|
static description = '\nExecutes multiple workflows once';
|
||||||
|
|
||||||
|
@ -87,19 +77,24 @@ export class ExecuteBatch extends Command {
|
||||||
}),
|
}),
|
||||||
concurrency: flags.integer({
|
concurrency: flags.integer({
|
||||||
default: 1,
|
default: 1,
|
||||||
description: 'How many workflows can run in parallel. Defaults to 1 which means no concurrency.',
|
description:
|
||||||
|
'How many workflows can run in parallel. Defaults to 1 which means no concurrency.',
|
||||||
}),
|
}),
|
||||||
output: flags.string({
|
output: flags.string({
|
||||||
description: 'Enable execution saving, You must inform an existing folder to save execution via this param',
|
description:
|
||||||
|
'Enable execution saving, You must inform an existing folder to save execution via this param',
|
||||||
}),
|
}),
|
||||||
snapshot: flags.string({
|
snapshot: flags.string({
|
||||||
description: 'Enables snapshot saving. You must inform an existing folder to save snapshots via this param.',
|
description:
|
||||||
|
'Enables snapshot saving. You must inform an existing folder to save snapshots via this param.',
|
||||||
}),
|
}),
|
||||||
compare: flags.string({
|
compare: flags.string({
|
||||||
description: 'Compares current execution with an existing snapshot. You must inform an existing folder where the snapshots are saved.',
|
description:
|
||||||
|
'Compares current execution with an existing snapshot. You must inform an existing folder where the snapshots are saved.',
|
||||||
}),
|
}),
|
||||||
shallow: flags.boolean({
|
shallow: flags.boolean({
|
||||||
description: 'Compares only if attributes output from node are the same, with no regards to neste JSON objects.',
|
description:
|
||||||
|
'Compares only if attributes output from node are the same, with no regards to neste JSON objects.',
|
||||||
}),
|
}),
|
||||||
skipList: flags.string({
|
skipList: flags.string({
|
||||||
description: 'File containing a comma separated list of workflow IDs to skip.',
|
description: 'File containing a comma separated list of workflow IDs to skip.',
|
||||||
|
@ -117,15 +112,16 @@ export class ExecuteBatch extends Command {
|
||||||
* Gracefully handles exit.
|
* Gracefully handles exit.
|
||||||
* @param {boolean} skipExit Whether to skip exit or number according to received signal
|
* @param {boolean} skipExit Whether to skip exit or number according to received signal
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
static async stopProcess(skipExit: boolean | number = false) {
|
static async stopProcess(skipExit: boolean | number = false) {
|
||||||
|
if (ExecuteBatch.cancelled) {
|
||||||
if (ExecuteBatch.cancelled === true) {
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
ExecuteBatch.cancelled = true;
|
ExecuteBatch.cancelled = true;
|
||||||
const activeExecutionsInstance = ActiveExecutions.getInstance();
|
const activeExecutionsInstance = ActiveExecutions.getInstance();
|
||||||
const stopPromises = activeExecutionsInstance.getActiveExecutions().map(async execution => {
|
const stopPromises = activeExecutionsInstance.getActiveExecutions().map(async (execution) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
activeExecutionsInstance.stopExecution(execution.id);
|
activeExecutionsInstance.stopExecution(execution.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -135,16 +131,17 @@ export class ExecuteBatch extends Command {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}, 30000);
|
}, 30000);
|
||||||
|
|
||||||
let executingWorkflows = activeExecutionsInstance.getActiveExecutions() as IExecutionsCurrentSummary[];
|
let executingWorkflows = activeExecutionsInstance.getActiveExecutions();
|
||||||
|
|
||||||
let count = 0;
|
let count = 0;
|
||||||
while (executingWorkflows.length !== 0) {
|
while (executingWorkflows.length !== 0) {
|
||||||
if (count++ % 4 === 0) {
|
if (count++ % 4 === 0) {
|
||||||
console.log(`Waiting for ${executingWorkflows.length} active executions to finish...`);
|
console.log(`Waiting for ${executingWorkflows.length} active executions to finish...`);
|
||||||
executingWorkflows.map(execution => {
|
executingWorkflows.map((execution) => {
|
||||||
console.log(` - Execution ID ${execution.id}, workflow ID: ${execution.workflowId}`);
|
console.log(` - Execution ID ${execution.id}, workflow ID: ${execution.workflowId}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
setTimeout(resolve, 500);
|
setTimeout(resolve, 500);
|
||||||
});
|
});
|
||||||
|
@ -157,12 +154,13 @@ export class ExecuteBatch extends Command {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
formatJsonOutput(data: object) {
|
formatJsonOutput(data: object) {
|
||||||
return JSON.stringify(data, null, 2);
|
return JSON.stringify(data, null, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
shouldBeConsideredAsWarning(errorMessage: string) {
|
shouldBeConsideredAsWarning(errorMessage: string) {
|
||||||
|
|
||||||
const warningStrings = [
|
const warningStrings = [
|
||||||
'refresh token is invalid',
|
'refresh token is invalid',
|
||||||
'unable to connect to',
|
'unable to connect to',
|
||||||
|
@ -174,6 +172,7 @@ export class ExecuteBatch extends Command {
|
||||||
'request timed out',
|
'request timed out',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
errorMessage = errorMessage.toLowerCase();
|
errorMessage = errorMessage.toLowerCase();
|
||||||
|
|
||||||
for (let i = 0; i < warningStrings.length; i++) {
|
for (let i = 0; i < warningStrings.length; i++) {
|
||||||
|
@ -185,18 +184,18 @@ export class ExecuteBatch extends Command {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
async run() {
|
async run() {
|
||||||
|
|
||||||
process.on('SIGTERM', ExecuteBatch.stopProcess);
|
process.on('SIGTERM', ExecuteBatch.stopProcess);
|
||||||
process.on('SIGINT', ExecuteBatch.stopProcess);
|
process.on('SIGINT', ExecuteBatch.stopProcess);
|
||||||
|
|
||||||
const logger = getLogger();
|
const logger = getLogger();
|
||||||
LoggerProxy.init(logger);
|
LoggerProxy.init(logger);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
const { flags } = this.parse(ExecuteBatch);
|
const { flags } = this.parse(ExecuteBatch);
|
||||||
|
|
||||||
ExecuteBatch.debug = flags.debug === true;
|
ExecuteBatch.debug = flags.debug;
|
||||||
ExecuteBatch.concurrency = flags.concurrency || 1;
|
ExecuteBatch.concurrency = flags.concurrency || 1;
|
||||||
|
|
||||||
const ids: number[] = [];
|
const ids: number[] = [];
|
||||||
|
@ -241,7 +240,7 @@ export class ExecuteBatch extends Command {
|
||||||
if (flags.ids !== undefined) {
|
if (flags.ids !== undefined) {
|
||||||
const paramIds = flags.ids.split(',');
|
const paramIds = flags.ids.split(',');
|
||||||
const re = /\d+/;
|
const re = /\d+/;
|
||||||
const matchedIds = paramIds.filter(id => id.match(re)).map(id => parseInt(id.trim(), 10));
|
const matchedIds = paramIds.filter((id) => re.exec(id)).map((id) => parseInt(id.trim(), 10));
|
||||||
|
|
||||||
if (matchedIds.length === 0) {
|
if (matchedIds.length === 0) {
|
||||||
console.log(`The parameter --ids must be a list of numeric IDs separated by a comma.`);
|
console.log(`The parameter --ids must be a list of numeric IDs separated by a comma.`);
|
||||||
|
@ -254,18 +253,17 @@ export class ExecuteBatch extends Command {
|
||||||
if (flags.skipList !== undefined) {
|
if (flags.skipList !== undefined) {
|
||||||
if (fs.existsSync(flags.skipList)) {
|
if (fs.existsSync(flags.skipList)) {
|
||||||
const contents = fs.readFileSync(flags.skipList, { encoding: 'utf-8' });
|
const contents = fs.readFileSync(flags.skipList, { encoding: 'utf-8' });
|
||||||
skipIds.push(...contents.split(',').map(id => parseInt(id.trim(), 10)));
|
skipIds.push(...contents.split(',').map((id) => parseInt(id.trim(), 10)));
|
||||||
} else {
|
} else {
|
||||||
console.log('Skip list file not found. Exiting.');
|
console.log('Skip list file not found. Exiting.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flags.shallow === true) {
|
if (flags.shallow) {
|
||||||
ExecuteBatch.shallow = true;
|
ExecuteBatch.shallow = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Start directly with the init of the database to improve startup time
|
// Start directly with the init of the database to improve startup time
|
||||||
const startDbInitPromise = Db.init();
|
const startDbInitPromise = Db.init();
|
||||||
|
|
||||||
|
@ -281,7 +279,7 @@ export class ExecuteBatch extends Command {
|
||||||
|
|
||||||
let allWorkflows;
|
let allWorkflows;
|
||||||
|
|
||||||
const query = Db.collections!.Workflow!.createQueryBuilder('workflows');
|
const query = Db.collections.Workflow!.createQueryBuilder('workflows');
|
||||||
|
|
||||||
if (ids.length > 0) {
|
if (ids.length > 0) {
|
||||||
query.andWhere(`workflows.id in (:...ids)`, { ids });
|
query.andWhere(`workflows.id in (:...ids)`, { ids });
|
||||||
|
@ -291,9 +289,10 @@ export class ExecuteBatch extends Command {
|
||||||
query.andWhere(`workflows.id not in (:...skipIds)`, { skipIds });
|
query.andWhere(`workflows.id not in (:...skipIds)`, { skipIds });
|
||||||
}
|
}
|
||||||
|
|
||||||
allWorkflows = await query.getMany() as IWorkflowDb[];
|
// eslint-disable-next-line prefer-const
|
||||||
|
allWorkflows = (await query.getMany()) as IWorkflowDb[];
|
||||||
|
|
||||||
if (ExecuteBatch.debug === true) {
|
if (ExecuteBatch.debug) {
|
||||||
process.stdout.write(`Found ${allWorkflows.length} workflows to execute.\n`);
|
process.stdout.write(`Found ${allWorkflows.length} workflows to execute.\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -318,12 +317,19 @@ export class ExecuteBatch extends Command {
|
||||||
|
|
||||||
let { retries } = flags;
|
let { retries } = flags;
|
||||||
|
|
||||||
while (retries > 0 && (results.summary.warningExecutions + results.summary.failedExecutions > 0) && ExecuteBatch.cancelled === false) {
|
while (
|
||||||
const failedWorkflowIds = results.summary.errors.map(execution => execution.workflowId);
|
retries > 0 &&
|
||||||
failedWorkflowIds.push(...results.summary.warnings.map(execution => execution.workflowId));
|
results.summary.warningExecutions + results.summary.failedExecutions > 0 &&
|
||||||
|
!ExecuteBatch.cancelled
|
||||||
|
) {
|
||||||
|
const failedWorkflowIds = results.summary.errors.map((execution) => execution.workflowId);
|
||||||
|
failedWorkflowIds.push(...results.summary.warnings.map((execution) => execution.workflowId));
|
||||||
|
|
||||||
const newWorkflowList = allWorkflows.filter(workflow => failedWorkflowIds.includes(workflow.id));
|
const newWorkflowList = allWorkflows.filter((workflow) =>
|
||||||
|
failedWorkflowIds.includes(workflow.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
const retryResults = await this.runTests(newWorkflowList);
|
const retryResults = await this.runTests(newWorkflowList);
|
||||||
|
|
||||||
this.mergeResults(results, retryResults);
|
this.mergeResults(results, retryResults);
|
||||||
|
@ -343,12 +349,17 @@ export class ExecuteBatch extends Command {
|
||||||
console.log(`\t${nodeName}: ${nodeCount}`);
|
console.log(`\t${nodeName}: ${nodeCount}`);
|
||||||
});
|
});
|
||||||
console.log('\nCheck the JSON file for more details.');
|
console.log('\nCheck the JSON file for more details.');
|
||||||
|
} else if (flags.shortOutput) {
|
||||||
|
console.log(
|
||||||
|
this.formatJsonOutput({
|
||||||
|
...results,
|
||||||
|
executions: results.executions.filter(
|
||||||
|
(execution) => execution.executionStatus !== 'success',
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
if (flags.shortOutput === true) {
|
console.log(this.formatJsonOutput(results));
|
||||||
console.log(this.formatJsonOutput({ ...results, executions: results.executions.filter(execution => execution.executionStatus !== 'success') }));
|
|
||||||
} else {
|
|
||||||
console.log(this.formatJsonOutput(results));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await ExecuteBatch.stopProcess(true);
|
await ExecuteBatch.stopProcess(true);
|
||||||
|
@ -357,23 +368,26 @@ export class ExecuteBatch extends Command {
|
||||||
this.exit(1);
|
this.exit(1);
|
||||||
}
|
}
|
||||||
this.exit(0);
|
this.exit(0);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
mergeResults(results: IResult, retryResults: IResult) {
|
mergeResults(results: IResult, retryResults: IResult) {
|
||||||
|
|
||||||
if (retryResults.summary.successfulExecutions === 0) {
|
if (retryResults.summary.successfulExecutions === 0) {
|
||||||
// Nothing to replace.
|
// Nothing to replace.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find successful executions and replace them on previous result.
|
// Find successful executions and replace them on previous result.
|
||||||
retryResults.executions.forEach(newExecution => {
|
retryResults.executions.forEach((newExecution) => {
|
||||||
if (newExecution.executionStatus === 'success') {
|
if (newExecution.executionStatus === 'success') {
|
||||||
// Remove previous execution from list.
|
// Remove previous execution from list.
|
||||||
results.executions = results.executions.filter(previousExecutions => previousExecutions.workflowId !== newExecution.workflowId);
|
results.executions = results.executions.filter(
|
||||||
|
(previousExecutions) => previousExecutions.workflowId !== newExecution.workflowId,
|
||||||
|
);
|
||||||
|
|
||||||
const errorIndex = results.summary.errors.findIndex(summaryInformation => summaryInformation.workflowId === newExecution.workflowId);
|
const errorIndex = results.summary.errors.findIndex(
|
||||||
|
(summaryInformation) => summaryInformation.workflowId === newExecution.workflowId,
|
||||||
|
);
|
||||||
if (errorIndex !== -1) {
|
if (errorIndex !== -1) {
|
||||||
// This workflow errored previously. Decrement error count.
|
// This workflow errored previously. Decrement error count.
|
||||||
results.summary.failedExecutions--;
|
results.summary.failedExecutions--;
|
||||||
|
@ -381,7 +395,9 @@ export class ExecuteBatch extends Command {
|
||||||
results.summary.errors.splice(errorIndex, 1);
|
results.summary.errors.splice(errorIndex, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const warningIndex = results.summary.warnings.findIndex(summaryInformation => summaryInformation.workflowId === newExecution.workflowId);
|
const warningIndex = results.summary.warnings.findIndex(
|
||||||
|
(summaryInformation) => summaryInformation.workflowId === newExecution.workflowId,
|
||||||
|
);
|
||||||
if (warningIndex !== -1) {
|
if (warningIndex !== -1) {
|
||||||
// This workflow errored previously. Decrement error count.
|
// This workflow errored previously. Decrement error count.
|
||||||
results.summary.warningExecutions--;
|
results.summary.warningExecutions--;
|
||||||
|
@ -420,7 +436,7 @@ export class ExecuteBatch extends Command {
|
||||||
let workflow: IWorkflowDb | undefined;
|
let workflow: IWorkflowDb | undefined;
|
||||||
while (allWorkflows.length > 0) {
|
while (allWorkflows.length > 0) {
|
||||||
workflow = allWorkflows.shift();
|
workflow = allWorkflows.shift();
|
||||||
if (ExecuteBatch.cancelled === true) {
|
if (ExecuteBatch.cancelled) {
|
||||||
process.stdout.write(`Thread ${i + 1} resolving and quitting.`);
|
process.stdout.write(`Thread ${i + 1} resolving and quitting.`);
|
||||||
resolve(true);
|
resolve(true);
|
||||||
break;
|
break;
|
||||||
|
@ -440,6 +456,7 @@ export class ExecuteBatch extends Command {
|
||||||
this.updateStatus();
|
this.updateStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||||
await this.startThread(workflow).then((executionResult) => {
|
await this.startThread(workflow).then((executionResult) => {
|
||||||
if (ExecuteBatch.debug) {
|
if (ExecuteBatch.debug) {
|
||||||
ExecuteBatch.workflowExecutionsProgress[i].pop();
|
ExecuteBatch.workflowExecutionsProgress[i].pop();
|
||||||
|
@ -456,7 +473,7 @@ export class ExecuteBatch extends Command {
|
||||||
result.summary.successfulExecutions++;
|
result.summary.successfulExecutions++;
|
||||||
const nodeNames = Object.keys(executionResult.coveredNodes);
|
const nodeNames = Object.keys(executionResult.coveredNodes);
|
||||||
|
|
||||||
nodeNames.map(nodeName => {
|
nodeNames.map((nodeName) => {
|
||||||
if (result.coveredNodes[nodeName] === undefined) {
|
if (result.coveredNodes[nodeName] === undefined) {
|
||||||
result.coveredNodes[nodeName] = 0;
|
result.coveredNodes[nodeName] = 0;
|
||||||
}
|
}
|
||||||
|
@ -506,19 +523,18 @@ export class ExecuteBatch extends Command {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
updateStatus() {
|
updateStatus() {
|
||||||
|
if (ExecuteBatch.cancelled) {
|
||||||
if (ExecuteBatch.cancelled === true) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.stdout.isTTY === true) {
|
if (process.stdout.isTTY) {
|
||||||
process.stdout.moveCursor(0, - (ExecuteBatch.concurrency));
|
process.stdout.moveCursor(0, -ExecuteBatch.concurrency);
|
||||||
process.stdout.cursorTo(0);
|
process.stdout.cursorTo(0);
|
||||||
process.stdout.clearLine(0);
|
process.stdout.clearLine(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ExecuteBatch.workflowExecutionsProgress.map((concurrentThread, index) => {
|
ExecuteBatch.workflowExecutionsProgress.map((concurrentThread, index) => {
|
||||||
let message = `${index + 1}: `;
|
let message = `${index + 1}: `;
|
||||||
concurrentThread.map((executionItem, workflowIndex) => {
|
concurrentThread.map((executionItem, workflowIndex) => {
|
||||||
|
@ -537,16 +553,19 @@ export class ExecuteBatch extends Command {
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
message += (workflowIndex > 0 ? ', ' : '') + `${openColor}${executionItem.workflowId}${closeColor}`;
|
message += `${workflowIndex > 0 ? ', ' : ''}${openColor}${
|
||||||
|
executionItem.workflowId
|
||||||
|
}${closeColor}`;
|
||||||
});
|
});
|
||||||
if (process.stdout.isTTY === true) {
|
if (process.stdout.isTTY) {
|
||||||
process.stdout.cursorTo(0);
|
process.stdout.cursorTo(0);
|
||||||
process.stdout.clearLine(0);
|
process.stdout.clearLine(0);
|
||||||
}
|
}
|
||||||
process.stdout.write(message + '\n');
|
process.stdout.write(`${message}\n`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
initializeLogs() {
|
initializeLogs() {
|
||||||
process.stdout.write('**********************************************\n');
|
process.stdout.write('**********************************************\n');
|
||||||
process.stdout.write(' n8n test workflows\n');
|
process.stdout.write(' n8n test workflows\n');
|
||||||
|
@ -560,7 +579,7 @@ export class ExecuteBatch extends Command {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
startThread(workflowData: IWorkflowDb): Promise<IExecutionResult> {
|
async startThread(workflowData: IWorkflowDb): Promise<IExecutionResult> {
|
||||||
// This will be the object returned by the promise.
|
// This will be the object returned by the promise.
|
||||||
// It will be updated according to execution progress below.
|
// It will be updated according to execution progress below.
|
||||||
const executionResult: IExecutionResult = {
|
const executionResult: IExecutionResult = {
|
||||||
|
@ -572,10 +591,9 @@ export class ExecuteBatch extends Command {
|
||||||
coveredNodes: {},
|
coveredNodes: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const requiredNodeTypes = ['n8n-nodes-base.start'];
|
const requiredNodeTypes = ['n8n-nodes-base.start'];
|
||||||
let startNode: INode | undefined = undefined;
|
let startNode: INode | undefined;
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const node of workflowData.nodes) {
|
for (const node of workflowData.nodes) {
|
||||||
if (requiredNodeTypes.includes(node.type)) {
|
if (requiredNodeTypes.includes(node.type)) {
|
||||||
startNode = node;
|
startNode = node;
|
||||||
|
@ -593,10 +611,10 @@ export class ExecuteBatch extends Command {
|
||||||
// properties from the JSON object (useful for optional properties that can
|
// properties from the JSON object (useful for optional properties that can
|
||||||
// cause the comparison to detect changes when not true).
|
// cause the comparison to detect changes when not true).
|
||||||
const nodeEdgeCases = {} as INodeSpecialCases;
|
const nodeEdgeCases = {} as INodeSpecialCases;
|
||||||
workflowData.nodes.forEach(node => {
|
workflowData.nodes.forEach((node) => {
|
||||||
executionResult.coveredNodes[node.type] = (executionResult.coveredNodes[node.type] || 0) + 1;
|
executionResult.coveredNodes[node.type] = (executionResult.coveredNodes[node.type] || 0) + 1;
|
||||||
if (node.notes !== undefined && node.notes !== '') {
|
if (node.notes !== undefined && node.notes !== '') {
|
||||||
node.notes.split('\n').forEach(note => {
|
node.notes.split('\n').forEach((note) => {
|
||||||
const parts = note.split('=');
|
const parts = note.split('=');
|
||||||
if (parts.length === 2) {
|
if (parts.length === 2) {
|
||||||
if (nodeEdgeCases[node.name] === undefined) {
|
if (nodeEdgeCases[node.name] === undefined) {
|
||||||
|
@ -605,9 +623,13 @@ export class ExecuteBatch extends Command {
|
||||||
if (parts[0] === 'CAP_RESULTS_LENGTH') {
|
if (parts[0] === 'CAP_RESULTS_LENGTH') {
|
||||||
nodeEdgeCases[node.name].capResults = parseInt(parts[1], 10);
|
nodeEdgeCases[node.name].capResults = parseInt(parts[1], 10);
|
||||||
} else if (parts[0] === 'IGNORED_PROPERTIES') {
|
} else if (parts[0] === 'IGNORED_PROPERTIES') {
|
||||||
nodeEdgeCases[node.name].ignoredProperties = parts[1].split(',').map(property => property.trim());
|
nodeEdgeCases[node.name].ignoredProperties = parts[1]
|
||||||
|
.split(',')
|
||||||
|
.map((property) => property.trim());
|
||||||
} else if (parts[0] === 'KEEP_ONLY_PROPERTIES') {
|
} else if (parts[0] === 'KEEP_ONLY_PROPERTIES') {
|
||||||
nodeEdgeCases[node.name].keepOnlyProperties = parts[1].split(',').map(property => property.trim());
|
nodeEdgeCases[node.name].keepOnlyProperties = parts[1]
|
||||||
|
.split(',')
|
||||||
|
.map((property) => property.trim());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -633,15 +655,11 @@ export class ExecuteBatch extends Command {
|
||||||
resolve(executionResult);
|
resolve(executionResult);
|
||||||
}, ExecuteBatch.executionTimeout);
|
}, ExecuteBatch.executionTimeout);
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const credentials = await WorkflowCredentials(workflowData!.nodes);
|
|
||||||
|
|
||||||
const runData: IWorkflowExecutionDataProcess = {
|
const runData: IWorkflowExecutionDataProcess = {
|
||||||
credentials,
|
|
||||||
executionMode: 'cli',
|
executionMode: 'cli',
|
||||||
startNodes: [startNode!.name],
|
startNodes: [startNode!.name],
|
||||||
workflowData: workflowData!,
|
workflowData,
|
||||||
};
|
};
|
||||||
|
|
||||||
const workflowRunner = new WorkflowRunner();
|
const workflowRunner = new WorkflowRunner();
|
||||||
|
@ -649,7 +667,7 @@ export class ExecuteBatch extends Command {
|
||||||
|
|
||||||
const activeExecutions = ActiveExecutions.getInstance();
|
const activeExecutions = ActiveExecutions.getInstance();
|
||||||
const data = await activeExecutions.getPostExecutePromise(executionId);
|
const data = await activeExecutions.getPostExecutePromise(executionId);
|
||||||
if (gotCancel || ExecuteBatch.cancelled === true) {
|
if (gotCancel || ExecuteBatch.cancelled) {
|
||||||
clearTimeout(timeoutTimer);
|
clearTimeout(timeoutTimer);
|
||||||
// The promise was settled already so we simply ignore.
|
// The promise was settled already so we simply ignore.
|
||||||
return;
|
return;
|
||||||
|
@ -659,14 +677,18 @@ export class ExecuteBatch extends Command {
|
||||||
executionResult.error = 'Workflow did not return any data.';
|
executionResult.error = 'Workflow did not return any data.';
|
||||||
executionResult.executionStatus = 'error';
|
executionResult.executionStatus = 'error';
|
||||||
} else {
|
} else {
|
||||||
executionResult.executionTime = (Date.parse(data.stoppedAt as unknown as string) - Date.parse(data.startedAt as unknown as string)) / 1000;
|
executionResult.executionTime =
|
||||||
executionResult.finished = (data?.finished !== undefined) as boolean;
|
(Date.parse(data.stoppedAt as unknown as string) -
|
||||||
|
Date.parse(data.startedAt as unknown as string)) /
|
||||||
|
1000;
|
||||||
|
executionResult.finished = data?.finished !== undefined;
|
||||||
|
|
||||||
if (data.data.resultData.error) {
|
if (data.data.resultData.error) {
|
||||||
executionResult.error =
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, no-prototype-builtins
|
||||||
data.data.resultData.error.hasOwnProperty('description') ?
|
executionResult.error = data.data.resultData.error.hasOwnProperty('description')
|
||||||
// @ts-ignore
|
? // @ts-ignore
|
||||||
data.data.resultData.error.description : data.data.resultData.error.message;
|
data.data.resultData.error.description
|
||||||
|
: data.data.resultData.error.message;
|
||||||
if (data.data.resultData.lastNodeExecuted !== undefined) {
|
if (data.data.resultData.lastNodeExecuted !== undefined) {
|
||||||
executionResult.error += ` on node ${data.data.resultData.lastNodeExecuted}`;
|
executionResult.error += ` on node ${data.data.resultData.lastNodeExecuted}`;
|
||||||
}
|
}
|
||||||
|
@ -676,7 +698,7 @@ export class ExecuteBatch extends Command {
|
||||||
executionResult.executionStatus = 'warning';
|
executionResult.executionStatus = 'warning';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (ExecuteBatch.shallow === true) {
|
if (ExecuteBatch.shallow) {
|
||||||
// What this does is guarantee that top-level attributes
|
// What this does is guarantee that top-level attributes
|
||||||
// from the JSON are kept and the are the same type.
|
// from the JSON are kept and the are the same type.
|
||||||
|
|
||||||
|
@ -690,34 +712,48 @@ export class ExecuteBatch extends Command {
|
||||||
if (taskData.data === undefined) {
|
if (taskData.data === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Object.keys(taskData.data).map(connectionName => {
|
Object.keys(taskData.data).map((connectionName) => {
|
||||||
const connection = taskData.data![connectionName] as Array<INodeExecutionData[] | null>;
|
const connection = taskData.data![connectionName];
|
||||||
connection.map(executionDataArray => {
|
connection.map((executionDataArray) => {
|
||||||
if (executionDataArray === null) {
|
if (executionDataArray === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nodeEdgeCases[nodeName] !== undefined && nodeEdgeCases[nodeName].capResults !== undefined) {
|
if (
|
||||||
|
nodeEdgeCases[nodeName] !== undefined &&
|
||||||
|
nodeEdgeCases[nodeName].capResults !== undefined
|
||||||
|
) {
|
||||||
executionDataArray.splice(nodeEdgeCases[nodeName].capResults!);
|
executionDataArray.splice(nodeEdgeCases[nodeName].capResults!);
|
||||||
}
|
}
|
||||||
|
|
||||||
executionDataArray.map(executionData => {
|
executionDataArray.map((executionData) => {
|
||||||
if (executionData.json === undefined) {
|
if (executionData.json === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (nodeEdgeCases[nodeName] !== undefined && nodeEdgeCases[nodeName].ignoredProperties !== undefined) {
|
if (
|
||||||
nodeEdgeCases[nodeName].ignoredProperties!.forEach(ignoredProperty => delete executionData.json[ignoredProperty]);
|
nodeEdgeCases[nodeName] !== undefined &&
|
||||||
|
nodeEdgeCases[nodeName].ignoredProperties !== undefined
|
||||||
|
) {
|
||||||
|
nodeEdgeCases[nodeName].ignoredProperties!.forEach(
|
||||||
|
(ignoredProperty) => delete executionData.json[ignoredProperty],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let keepOnlyFields = [] as string[];
|
let keepOnlyFields = [] as string[];
|
||||||
if (nodeEdgeCases[nodeName] !== undefined && nodeEdgeCases[nodeName].keepOnlyProperties !== undefined) {
|
if (
|
||||||
|
nodeEdgeCases[nodeName] !== undefined &&
|
||||||
|
nodeEdgeCases[nodeName].keepOnlyProperties !== undefined
|
||||||
|
) {
|
||||||
keepOnlyFields = nodeEdgeCases[nodeName].keepOnlyProperties!;
|
keepOnlyFields = nodeEdgeCases[nodeName].keepOnlyProperties!;
|
||||||
}
|
}
|
||||||
executionData.json = keepOnlyFields.length > 0 ? pick(executionData.json, keepOnlyFields) : executionData.json;
|
executionData.json =
|
||||||
|
keepOnlyFields.length > 0
|
||||||
|
? pick(executionData.json, keepOnlyFields)
|
||||||
|
: executionData.json;
|
||||||
const jsonProperties = executionData.json;
|
const jsonProperties = executionData.json;
|
||||||
|
|
||||||
const nodeOutputAttributes = Object.keys(jsonProperties);
|
const nodeOutputAttributes = Object.keys(jsonProperties);
|
||||||
nodeOutputAttributes.map(attributeName => {
|
nodeOutputAttributes.map((attributeName) => {
|
||||||
if (Array.isArray(jsonProperties[attributeName])) {
|
if (Array.isArray(jsonProperties[attributeName])) {
|
||||||
jsonProperties[attributeName] = ['json array'];
|
jsonProperties[attributeName] = ['json array'];
|
||||||
} else if (typeof jsonProperties[attributeName] === 'object') {
|
} else if (typeof jsonProperties[attributeName] === 'object') {
|
||||||
|
@ -726,7 +762,6 @@ export class ExecuteBatch extends Command {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -734,14 +769,14 @@ export class ExecuteBatch extends Command {
|
||||||
// If not using shallow comparison then we only treat nodeEdgeCases.
|
// If not using shallow comparison then we only treat nodeEdgeCases.
|
||||||
const specialCases = Object.keys(nodeEdgeCases);
|
const specialCases = Object.keys(nodeEdgeCases);
|
||||||
|
|
||||||
specialCases.forEach(nodeName => {
|
specialCases.forEach((nodeName) => {
|
||||||
data.data.resultData.runData[nodeName].map((taskData: ITaskData) => {
|
data.data.resultData.runData[nodeName].map((taskData: ITaskData) => {
|
||||||
if (taskData.data === undefined) {
|
if (taskData.data === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Object.keys(taskData.data).map(connectionName => {
|
Object.keys(taskData.data).map((connectionName) => {
|
||||||
const connection = taskData.data![connectionName] as Array<INodeExecutionData[] | null>;
|
const connection = taskData.data![connectionName];
|
||||||
connection.map(executionDataArray => {
|
connection.map((executionDataArray) => {
|
||||||
if (executionDataArray === null) {
|
if (executionDataArray === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -751,15 +786,16 @@ export class ExecuteBatch extends Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nodeEdgeCases[nodeName].ignoredProperties !== undefined) {
|
if (nodeEdgeCases[nodeName].ignoredProperties !== undefined) {
|
||||||
executionDataArray.map(executionData => {
|
executionDataArray.map((executionData) => {
|
||||||
if (executionData.json === undefined) {
|
if (executionData.json === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
nodeEdgeCases[nodeName].ignoredProperties!.forEach(ignoredProperty => delete executionData.json[ignoredProperty]);
|
nodeEdgeCases[nodeName].ignoredProperties!.forEach(
|
||||||
|
(ignoredProperty) => delete executionData.json[ignoredProperty],
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -769,9 +805,12 @@ export class ExecuteBatch extends Command {
|
||||||
if (ExecuteBatch.compare === undefined) {
|
if (ExecuteBatch.compare === undefined) {
|
||||||
executionResult.executionStatus = 'success';
|
executionResult.executionStatus = 'success';
|
||||||
} else {
|
} else {
|
||||||
const fileName = (ExecuteBatch.compare.endsWith(sep) ? ExecuteBatch.compare : ExecuteBatch.compare + sep) + `${workflowData.id}-snapshot.json`;
|
const fileName = `${
|
||||||
if (fs.existsSync(fileName) === true) {
|
ExecuteBatch.compare.endsWith(sep)
|
||||||
|
? ExecuteBatch.compare
|
||||||
|
: ExecuteBatch.compare + sep
|
||||||
|
}${workflowData.id}-snapshot.json`;
|
||||||
|
if (fs.existsSync(fileName)) {
|
||||||
const contents = fs.readFileSync(fileName, { encoding: 'utf-8' });
|
const contents = fs.readFileSync(fileName, { encoding: 'utf-8' });
|
||||||
|
|
||||||
const changes = diff(JSON.parse(contents), data, { keysOnly: true });
|
const changes = diff(JSON.parse(contents), data, { keysOnly: true });
|
||||||
|
@ -792,7 +831,11 @@ export class ExecuteBatch extends Command {
|
||||||
// Save snapshots only after comparing - this is to make sure we're updating
|
// Save snapshots only after comparing - this is to make sure we're updating
|
||||||
// After comparing to existing verion.
|
// After comparing to existing verion.
|
||||||
if (ExecuteBatch.snapshot !== undefined) {
|
if (ExecuteBatch.snapshot !== undefined) {
|
||||||
const fileName = (ExecuteBatch.snapshot.endsWith(sep) ? ExecuteBatch.snapshot : ExecuteBatch.snapshot + sep) + `${workflowData.id}-snapshot.json`;
|
const fileName = `${
|
||||||
|
ExecuteBatch.snapshot.endsWith(sep)
|
||||||
|
? ExecuteBatch.snapshot
|
||||||
|
: ExecuteBatch.snapshot + sep
|
||||||
|
}${workflowData.id}-snapshot.json`;
|
||||||
fs.writeFileSync(fileName, serializedData);
|
fs.writeFileSync(fileName, serializedData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -805,5 +848,4 @@ export class ExecuteBatch extends Command {
|
||||||
resolve(executionResult);
|
resolve(executionResult);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +1,16 @@
|
||||||
import {
|
/* eslint-disable @typescript-eslint/restrict-plus-operands */
|
||||||
Command,
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
flags,
|
/* eslint-disable no-console */
|
||||||
} from '@oclif/command';
|
import { Command, flags } from '@oclif/command';
|
||||||
|
|
||||||
import {
|
import { Credentials, UserSettings } from 'n8n-core';
|
||||||
Credentials,
|
|
||||||
UserSettings,
|
|
||||||
} from 'n8n-core';
|
|
||||||
|
|
||||||
import {
|
import { IDataObject, LoggerProxy } from 'n8n-workflow';
|
||||||
IDataObject
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Db,
|
|
||||||
ICredentialsDecryptedDb,
|
|
||||||
} from '../../src';
|
|
||||||
|
|
||||||
import {
|
|
||||||
getLogger,
|
|
||||||
} from '../../src/Logger';
|
|
||||||
|
|
||||||
import {
|
|
||||||
LoggerProxy,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import { getLogger } from '../../src/Logger';
|
||||||
|
import { Db, ICredentialsDecryptedDb } from '../../src';
|
||||||
|
|
||||||
export class ExportCredentialsCommand extends Command {
|
export class ExportCredentialsCommand extends Command {
|
||||||
static description = 'Export credentials';
|
static description = 'Export credentials';
|
||||||
|
@ -45,7 +29,8 @@ export class ExportCredentialsCommand extends Command {
|
||||||
description: 'Export all credentials',
|
description: 'Export all credentials',
|
||||||
}),
|
}),
|
||||||
backup: flags.boolean({
|
backup: flags.boolean({
|
||||||
description: 'Sets --all --pretty --separate for simple backups. Only --output has to be set additionally.',
|
description:
|
||||||
|
'Sets --all --pretty --separate for simple backups. Only --output has to be set additionally.',
|
||||||
}),
|
}),
|
||||||
id: flags.string({
|
id: flags.string({
|
||||||
description: 'The ID of the credential to export',
|
description: 'The ID of the credential to export',
|
||||||
|
@ -58,19 +43,23 @@ export class ExportCredentialsCommand extends Command {
|
||||||
description: 'Format the output in an easier to read fashion',
|
description: 'Format the output in an easier to read fashion',
|
||||||
}),
|
}),
|
||||||
separate: flags.boolean({
|
separate: flags.boolean({
|
||||||
description: 'Exports one file per credential (useful for versioning). Must inform a directory via --output.',
|
description:
|
||||||
|
'Exports one file per credential (useful for versioning). Must inform a directory via --output.',
|
||||||
}),
|
}),
|
||||||
decrypted: flags.boolean({
|
decrypted: flags.boolean({
|
||||||
description: 'Exports data decrypted / in plain text. ALL SENSITIVE INFORMATION WILL BE VISIBLE IN THE FILES. Use to migrate from a installation to another that have a different secret key (in the config file).',
|
description:
|
||||||
|
'Exports data decrypted / in plain text. ALL SENSITIVE INFORMATION WILL BE VISIBLE IN THE FILES. Use to migrate from a installation to another that have a different secret key (in the config file).',
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
async run() {
|
async run() {
|
||||||
const logger = getLogger();
|
const logger = getLogger();
|
||||||
LoggerProxy.init(logger);
|
LoggerProxy.init(logger);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
const { flags } = this.parse(ExportCredentialsCommand);
|
const { flags } = this.parse(ExportCredentialsCommand);
|
||||||
|
|
||||||
if (flags.backup) {
|
if (flags.backup) {
|
||||||
flags.all = true;
|
flags.all = true;
|
||||||
flags.pretty = true;
|
flags.pretty = true;
|
||||||
|
@ -103,7 +92,9 @@ export class ExportCredentialsCommand extends Command {
|
||||||
fs.mkdirSync(flags.output, { recursive: true });
|
fs.mkdirSync(flags.output, { recursive: true });
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Aborting execution as a filesystem error has been encountered while creating the output directory. See log messages for details.');
|
console.error(
|
||||||
|
'Aborting execution as a filesystem error has been encountered while creating the output directory. See log messages for details.',
|
||||||
|
);
|
||||||
logger.error('\nFILESYSTEM ERROR');
|
logger.error('\nFILESYSTEM ERROR');
|
||||||
logger.info('====================================');
|
logger.info('====================================');
|
||||||
logger.error(e.message);
|
logger.error(e.message);
|
||||||
|
@ -127,6 +118,7 @@ export class ExportCredentialsCommand extends Command {
|
||||||
findQuery.id = flags.id;
|
findQuery.id = flags.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const credentials = await Db.collections.Credentials!.find(findQuery);
|
const credentials = await Db.collections.Credentials!.find(findQuery);
|
||||||
|
|
||||||
if (flags.decrypted) {
|
if (flags.decrypted) {
|
||||||
|
@ -148,17 +140,22 @@ export class ExportCredentialsCommand extends Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flags.separate) {
|
if (flags.separate) {
|
||||||
let fileContents: string, i: number;
|
let fileContents: string;
|
||||||
|
let i: number;
|
||||||
for (i = 0; i < credentials.length; i++) {
|
for (i = 0; i < credentials.length; i++) {
|
||||||
fileContents = JSON.stringify(credentials[i], null, flags.pretty ? 2 : undefined);
|
fileContents = JSON.stringify(credentials[i], null, flags.pretty ? 2 : undefined);
|
||||||
const filename = (flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) + credentials[i].id + '.json';
|
const filename = `${
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
(flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) +
|
||||||
|
credentials[i].id
|
||||||
|
}.json`;
|
||||||
fs.writeFileSync(filename, fileContents);
|
fs.writeFileSync(filename, fileContents);
|
||||||
}
|
}
|
||||||
console.info(`Successfully exported ${i} credentials.`);
|
console.info(`Successfully exported ${i} credentials.`);
|
||||||
} else {
|
} else {
|
||||||
const fileContents = JSON.stringify(credentials, null, flags.pretty ? 2 : undefined);
|
const fileContents = JSON.stringify(credentials, null, flags.pretty ? 2 : undefined);
|
||||||
if (flags.output) {
|
if (flags.output) {
|
||||||
fs.writeFileSync(flags.output!, fileContents);
|
fs.writeFileSync(flags.output, fileContents);
|
||||||
console.info(`Successfully exported ${credentials.length} credentials.`);
|
console.info(`Successfully exported ${credentials.length} credentials.`);
|
||||||
} else {
|
} else {
|
||||||
console.info(fileContents);
|
console.info(fileContents);
|
||||||
|
|
|
@ -1,26 +1,13 @@
|
||||||
import {
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
Command,
|
/* eslint-disable no-console */
|
||||||
flags,
|
import { Command, flags } from '@oclif/command';
|
||||||
} from '@oclif/command';
|
|
||||||
|
|
||||||
import {
|
import { IDataObject, LoggerProxy } from 'n8n-workflow';
|
||||||
IDataObject
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Db,
|
|
||||||
} from '../../src';
|
|
||||||
|
|
||||||
import {
|
|
||||||
getLogger,
|
|
||||||
} from '../../src/Logger';
|
|
||||||
|
|
||||||
import {
|
|
||||||
LoggerProxy,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import { getLogger } from '../../src/Logger';
|
||||||
|
import { Db } from '../../src';
|
||||||
|
|
||||||
export class ExportWorkflowsCommand extends Command {
|
export class ExportWorkflowsCommand extends Command {
|
||||||
static description = 'Export workflows';
|
static description = 'Export workflows';
|
||||||
|
@ -38,7 +25,8 @@ export class ExportWorkflowsCommand extends Command {
|
||||||
description: 'Export all workflows',
|
description: 'Export all workflows',
|
||||||
}),
|
}),
|
||||||
backup: flags.boolean({
|
backup: flags.boolean({
|
||||||
description: 'Sets --all --pretty --separate for simple backups. Only --output has to be set additionally.',
|
description:
|
||||||
|
'Sets --all --pretty --separate for simple backups. Only --output has to be set additionally.',
|
||||||
}),
|
}),
|
||||||
id: flags.string({
|
id: flags.string({
|
||||||
description: 'The ID of the workflow to export',
|
description: 'The ID of the workflow to export',
|
||||||
|
@ -51,14 +39,17 @@ export class ExportWorkflowsCommand extends Command {
|
||||||
description: 'Format the output in an easier to read fashion',
|
description: 'Format the output in an easier to read fashion',
|
||||||
}),
|
}),
|
||||||
separate: flags.boolean({
|
separate: flags.boolean({
|
||||||
description: 'Exports one file per workflow (useful for versioning). Must inform a directory via --output.',
|
description:
|
||||||
|
'Exports one file per workflow (useful for versioning). Must inform a directory via --output.',
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
async run() {
|
async run() {
|
||||||
const logger = getLogger();
|
const logger = getLogger();
|
||||||
LoggerProxy.init(logger);
|
LoggerProxy.init(logger);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
const { flags } = this.parse(ExportWorkflowsCommand);
|
const { flags } = this.parse(ExportWorkflowsCommand);
|
||||||
|
|
||||||
if (flags.backup) {
|
if (flags.backup) {
|
||||||
|
@ -93,7 +84,9 @@ export class ExportWorkflowsCommand extends Command {
|
||||||
fs.mkdirSync(flags.output, { recursive: true });
|
fs.mkdirSync(flags.output, { recursive: true });
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Aborting execution as a filesystem error has been encountered while creating the output directory. See log messages for details.');
|
console.error(
|
||||||
|
'Aborting execution as a filesystem error has been encountered while creating the output directory. See log messages for details.',
|
||||||
|
);
|
||||||
logger.error('\nFILESYSTEM ERROR');
|
logger.error('\nFILESYSTEM ERROR');
|
||||||
logger.info('====================================');
|
logger.info('====================================');
|
||||||
logger.error(e.message);
|
logger.error(e.message);
|
||||||
|
@ -117,6 +110,7 @@ export class ExportWorkflowsCommand extends Command {
|
||||||
findQuery.id = flags.id;
|
findQuery.id = flags.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const workflows = await Db.collections.Workflow!.find(findQuery);
|
const workflows = await Db.collections.Workflow!.find(findQuery);
|
||||||
|
|
||||||
if (workflows.length === 0) {
|
if (workflows.length === 0) {
|
||||||
|
@ -124,18 +118,27 @@ export class ExportWorkflowsCommand extends Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flags.separate) {
|
if (flags.separate) {
|
||||||
let fileContents: string, i: number;
|
let fileContents: string;
|
||||||
|
let i: number;
|
||||||
for (i = 0; i < workflows.length; i++) {
|
for (i = 0; i < workflows.length; i++) {
|
||||||
fileContents = JSON.stringify(workflows[i], null, flags.pretty ? 2 : undefined);
|
fileContents = JSON.stringify(workflows[i], null, flags.pretty ? 2 : undefined);
|
||||||
const filename = (flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) + workflows[i].id + '.json';
|
const filename = `${
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands, @typescript-eslint/no-non-null-assertion
|
||||||
|
(flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) +
|
||||||
|
workflows[i].id
|
||||||
|
}.json`;
|
||||||
fs.writeFileSync(filename, fileContents);
|
fs.writeFileSync(filename, fileContents);
|
||||||
}
|
}
|
||||||
console.info(`Successfully exported ${i} workflows.`);
|
console.info(`Successfully exported ${i} workflows.`);
|
||||||
} else {
|
} else {
|
||||||
const fileContents = JSON.stringify(workflows, null, flags.pretty ? 2 : undefined);
|
const fileContents = JSON.stringify(workflows, null, flags.pretty ? 2 : undefined);
|
||||||
if (flags.output) {
|
if (flags.output) {
|
||||||
fs.writeFileSync(flags.output!, fileContents);
|
fs.writeFileSync(flags.output, fileContents);
|
||||||
console.info(`Successfully exported ${workflows.length} ${workflows.length === 1 ? 'workflow.' : 'workflows.'}`);
|
console.info(
|
||||||
|
`Successfully exported ${workflows.length} ${
|
||||||
|
workflows.length === 1 ? 'workflow.' : 'workflows.'
|
||||||
|
}`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
console.info(fileContents);
|
console.info(fileContents);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,16 @@
|
||||||
import {
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
Command,
|
/* eslint-disable no-console */
|
||||||
flags,
|
import { Command, flags } from '@oclif/command';
|
||||||
} from '@oclif/command';
|
|
||||||
|
|
||||||
import {
|
import { Credentials, UserSettings } from 'n8n-core';
|
||||||
Credentials,
|
|
||||||
UserSettings,
|
|
||||||
} from 'n8n-core';
|
|
||||||
|
|
||||||
import {
|
import { LoggerProxy } from 'n8n-workflow';
|
||||||
Db,
|
|
||||||
} from '../../src';
|
|
||||||
|
|
||||||
import {
|
|
||||||
getLogger,
|
|
||||||
} from '../../src/Logger';
|
|
||||||
|
|
||||||
import {
|
|
||||||
LoggerProxy,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as glob from 'glob-promise';
|
import * as glob from 'fast-glob';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import { getLogger } from '../../src/Logger';
|
||||||
|
import { Db } from '../../src';
|
||||||
|
|
||||||
export class ImportCredentialsCommand extends Command {
|
export class ImportCredentialsCommand extends Command {
|
||||||
static description = 'Import credentials';
|
static description = 'Import credentials';
|
||||||
|
@ -43,10 +31,12 @@ export class ImportCredentialsCommand extends Command {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
async run() {
|
async run() {
|
||||||
const logger = getLogger();
|
const logger = getLogger();
|
||||||
LoggerProxy.init(logger);
|
LoggerProxy.init(logger);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
const { flags } = this.parse(ImportCredentialsCommand);
|
const { flags } = this.parse(ImportCredentialsCommand);
|
||||||
|
|
||||||
if (!flags.input) {
|
if (!flags.input) {
|
||||||
|
@ -76,18 +66,25 @@ export class ImportCredentialsCommand extends Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flags.separate) {
|
if (flags.separate) {
|
||||||
const files = await glob((flags.input.endsWith(path.sep) ? flags.input : flags.input + path.sep) + '*.json');
|
const files = await glob(
|
||||||
|
`${flags.input.endsWith(path.sep) ? flags.input : flags.input + path.sep}*.json`,
|
||||||
|
);
|
||||||
for (i = 0; i < files.length; i++) {
|
for (i = 0; i < files.length; i++) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
const credential = JSON.parse(fs.readFileSync(files[i], { encoding: 'utf8' }));
|
const credential = JSON.parse(fs.readFileSync(files[i], { encoding: 'utf8' }));
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
if (typeof credential.data === 'object') {
|
if (typeof credential.data === 'object') {
|
||||||
// plain data / decrypted input. Should be encrypted first.
|
// plain data / decrypted input. Should be encrypted first.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
Credentials.prototype.setData.call(credential, credential.data, encryptionKey);
|
Credentials.prototype.setData.call(credential, credential.data, encryptionKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-non-null-assertion
|
||||||
await Db.collections.Credentials!.save(credential);
|
await Db.collections.Credentials!.save(credential);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
const fileContents = JSON.parse(fs.readFileSync(flags.input, { encoding: 'utf8' }));
|
const fileContents = JSON.parse(fs.readFileSync(flags.input, { encoding: 'utf8' }));
|
||||||
|
|
||||||
if (!Array.isArray(fileContents)) {
|
if (!Array.isArray(fileContents)) {
|
||||||
|
@ -97,8 +94,13 @@ export class ImportCredentialsCommand extends Command {
|
||||||
for (i = 0; i < fileContents.length; i++) {
|
for (i = 0; i < fileContents.length; i++) {
|
||||||
if (typeof fileContents[i].data === 'object') {
|
if (typeof fileContents[i].data === 'object') {
|
||||||
// plain data / decrypted input. Should be encrypted first.
|
// plain data / decrypted input. Should be encrypted first.
|
||||||
Credentials.prototype.setData.call(fileContents[i], fileContents[i].data, encryptionKey);
|
Credentials.prototype.setData.call(
|
||||||
|
fileContents[i],
|
||||||
|
fileContents[i].data,
|
||||||
|
encryptionKey,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-non-null-assertion
|
||||||
await Db.collections.Credentials!.save(fileContents[i]);
|
await Db.collections.Credentials!.save(fileContents[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,15 @@
|
||||||
import {
|
/* eslint-disable no-console */
|
||||||
Command,
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
flags,
|
import { Command, flags } from '@oclif/command';
|
||||||
} from '@oclif/command';
|
|
||||||
|
|
||||||
import {
|
import { LoggerProxy } from 'n8n-workflow';
|
||||||
Db,
|
|
||||||
} from '../../src';
|
|
||||||
|
|
||||||
import {
|
|
||||||
getLogger,
|
|
||||||
} from '../../src/Logger';
|
|
||||||
|
|
||||||
import {
|
|
||||||
LoggerProxy,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as glob from 'glob-promise';
|
import * as glob from 'fast-glob';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import {
|
import { UserSettings } from 'n8n-core';
|
||||||
UserSettings,
|
import { getLogger } from '../../src/Logger';
|
||||||
} from 'n8n-core';
|
import { Db } from '../../src';
|
||||||
|
|
||||||
export class ImportWorkflowsCommand extends Command {
|
export class ImportWorkflowsCommand extends Command {
|
||||||
static description = 'Import workflows';
|
static description = 'Import workflows';
|
||||||
|
@ -41,10 +30,12 @@ export class ImportWorkflowsCommand extends Command {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
async run() {
|
async run() {
|
||||||
const logger = getLogger();
|
const logger = getLogger();
|
||||||
LoggerProxy.init(logger);
|
LoggerProxy.init(logger);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
const { flags } = this.parse(ImportWorkflowsCommand);
|
const { flags } = this.parse(ImportWorkflowsCommand);
|
||||||
|
|
||||||
if (!flags.input) {
|
if (!flags.input) {
|
||||||
|
@ -68,9 +59,12 @@ export class ImportWorkflowsCommand extends Command {
|
||||||
await UserSettings.prepareUserSettings();
|
await UserSettings.prepareUserSettings();
|
||||||
let i;
|
let i;
|
||||||
if (flags.separate) {
|
if (flags.separate) {
|
||||||
const files = await glob((flags.input.endsWith(path.sep) ? flags.input : flags.input + path.sep) + '*.json');
|
const files = await glob(
|
||||||
|
`${flags.input.endsWith(path.sep) ? flags.input : flags.input + path.sep}*.json`,
|
||||||
|
);
|
||||||
for (i = 0; i < files.length; i++) {
|
for (i = 0; i < files.length; i++) {
|
||||||
const workflow = JSON.parse(fs.readFileSync(files[i], { encoding: 'utf8' }));
|
const workflow = JSON.parse(fs.readFileSync(files[i], { encoding: 'utf8' }));
|
||||||
|
// eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-non-null-assertion
|
||||||
await Db.collections.Workflow!.save(workflow);
|
await Db.collections.Workflow!.save(workflow);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -81,6 +75,7 @@ export class ImportWorkflowsCommand extends Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (i = 0; i < fileContents.length; i++) {
|
for (i = 0; i < fileContents.length; i++) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-non-null-assertion
|
||||||
await Db.collections.Workflow!.save(fileContents[i]);
|
await Db.collections.Workflow!.save(fileContents[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,6 +84,7 @@ export class ImportWorkflowsCommand extends Command {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('An error occurred while exporting workflows. See log messages for details.');
|
console.error('An error occurred while exporting workflows. See log messages for details.');
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
logger.error(error.message);
|
logger.error(error.message);
|
||||||
this.exit(1);
|
this.exit(1);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,10 @@
|
||||||
import {
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
Command,
|
/* eslint-disable no-console */
|
||||||
flags,
|
import { Command, flags } from '@oclif/command';
|
||||||
} from '@oclif/command';
|
|
||||||
|
|
||||||
import {
|
import { IDataObject } from 'n8n-workflow';
|
||||||
IDataObject
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Db,
|
|
||||||
} from "../../src";
|
|
||||||
|
|
||||||
|
import { Db } from '../../src';
|
||||||
|
|
||||||
export class ListWorkflowCommand extends Command {
|
export class ListWorkflowCommand extends Command {
|
||||||
static description = '\nList workflows';
|
static description = '\nList workflows';
|
||||||
|
@ -31,7 +25,9 @@ export class ListWorkflowCommand extends Command {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
async run() {
|
async run() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
const { flags } = this.parse(ListWorkflowCommand);
|
const { flags } = this.parse(ListWorkflowCommand);
|
||||||
|
|
||||||
if (flags.active !== undefined && !['true', 'false'].includes(flags.active)) {
|
if (flags.active !== undefined && !['true', 'false'].includes(flags.active)) {
|
||||||
|
@ -46,14 +42,13 @@ export class ListWorkflowCommand extends Command {
|
||||||
findQuery.active = flags.active === 'true';
|
findQuery.active = flags.active === 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const workflows = await Db.collections.Workflow!.find(findQuery);
|
const workflows = await Db.collections.Workflow!.find(findQuery);
|
||||||
if (flags.onlyId) {
|
if (flags.onlyId) {
|
||||||
workflows.forEach(workflow => console.log(workflow.id));
|
workflows.forEach((workflow) => console.log(workflow.id));
|
||||||
} else {
|
} else {
|
||||||
workflows.forEach(workflow => console.log(workflow.id + "|" + workflow.name));
|
workflows.forEach((workflow) => console.log(`${workflow.id}|${workflow.name}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('\nGOT ERROR');
|
console.error('\nGOT ERROR');
|
||||||
console.log('====================================');
|
console.log('====================================');
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
|
/* eslint-disable @typescript-eslint/await-thenable */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
/* eslint-disable @typescript-eslint/unbound-method */
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
import * as localtunnel from 'localtunnel';
|
import * as localtunnel from 'localtunnel';
|
||||||
import {
|
import { TUNNEL_SUBDOMAIN_ENV, UserSettings } from 'n8n-core';
|
||||||
TUNNEL_SUBDOMAIN_ENV,
|
|
||||||
UserSettings,
|
|
||||||
} from 'n8n-core';
|
|
||||||
import { Command, flags } from '@oclif/command';
|
import { Command, flags } from '@oclif/command';
|
||||||
const open = require('open');
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
|
|
||||||
|
import { IDataObject, LoggerProxy } from 'n8n-workflow';
|
||||||
import * as config from '../config';
|
import * as config from '../config';
|
||||||
import {
|
import {
|
||||||
ActiveExecutions,
|
ActiveExecutions,
|
||||||
|
@ -17,21 +22,19 @@ import {
|
||||||
Db,
|
Db,
|
||||||
ExternalHooks,
|
ExternalHooks,
|
||||||
GenericHelpers,
|
GenericHelpers,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
IExecutionsCurrentSummary,
|
IExecutionsCurrentSummary,
|
||||||
LoadNodesAndCredentials,
|
LoadNodesAndCredentials,
|
||||||
NodeTypes,
|
NodeTypes,
|
||||||
Server,
|
Server,
|
||||||
TestWebhooks,
|
TestWebhooks,
|
||||||
|
WaitTracker,
|
||||||
} from '../src';
|
} from '../src';
|
||||||
import { IDataObject } from 'n8n-workflow';
|
|
||||||
|
|
||||||
import {
|
import { getLogger } from '../src/Logger';
|
||||||
getLogger,
|
|
||||||
} from '../src/Logger';
|
|
||||||
|
|
||||||
import {
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires
|
||||||
LoggerProxy,
|
const open = require('open');
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
let activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner | undefined;
|
let activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner | undefined;
|
||||||
let processExistCode = 0;
|
let processExistCode = 0;
|
||||||
|
@ -53,29 +56,32 @@ export class Start extends Command {
|
||||||
description: 'opens the UI automatically in browser',
|
description: 'opens the UI automatically in browser',
|
||||||
}),
|
}),
|
||||||
tunnel: flags.boolean({
|
tunnel: flags.boolean({
|
||||||
description: 'runs the webhooks via a hooks.n8n.cloud tunnel server. Use only for testing and development!',
|
description:
|
||||||
|
'runs the webhooks via a hooks.n8n.cloud tunnel server. Use only for testing and development!',
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the UI in browser
|
* Opens the UI in browser
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
static openBrowser() {
|
static openBrowser() {
|
||||||
const editorUrl = GenericHelpers.getBaseUrl();
|
const editorUrl = GenericHelpers.getBaseUrl();
|
||||||
|
|
||||||
open(editorUrl, { wait: true })
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
.catch((error: Error) => {
|
open(editorUrl, { wait: true }).catch((error: Error) => {
|
||||||
console.log(`\nWas not able to open URL in browser. Please open manually by visiting:\n${editorUrl}\n`);
|
console.log(
|
||||||
});
|
`\nWas not able to open URL in browser. Please open manually by visiting:\n${editorUrl}\n`,
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stoppes the n8n in a graceful way.
|
* Stoppes the n8n in a graceful way.
|
||||||
* Make for example sure that all the webhooks from third party services
|
* Make for example sure that all the webhooks from third party services
|
||||||
* get removed.
|
* get removed.
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
static async stopProcess() {
|
static async stopProcess() {
|
||||||
getLogger().info('\nStopping n8n...');
|
getLogger().info('\nStopping n8n...');
|
||||||
|
|
||||||
|
@ -89,10 +95,12 @@ export class Start extends Command {
|
||||||
process.exit(processExistCode);
|
process.exit(processExistCode);
|
||||||
}, 30000);
|
}, 30000);
|
||||||
|
|
||||||
const skipWebhookDeregistration = config.get('endpoints.skipWebhoooksDeregistrationOnShutdown') as boolean;
|
const skipWebhookDeregistration = config.get(
|
||||||
|
'endpoints.skipWebhoooksDeregistrationOnShutdown',
|
||||||
|
) as boolean;
|
||||||
|
|
||||||
const removePromises = [];
|
const removePromises = [];
|
||||||
if (activeWorkflowRunner !== undefined && skipWebhookDeregistration !== true) {
|
if (activeWorkflowRunner !== undefined && !skipWebhookDeregistration) {
|
||||||
removePromises.push(activeWorkflowRunner.removeAll());
|
removePromises.push(activeWorkflowRunner.removeAll());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,22 +112,23 @@ export class Start extends Command {
|
||||||
|
|
||||||
// Wait for active workflow executions to finish
|
// Wait for active workflow executions to finish
|
||||||
const activeExecutionsInstance = ActiveExecutions.getInstance();
|
const activeExecutionsInstance = ActiveExecutions.getInstance();
|
||||||
let executingWorkflows = activeExecutionsInstance.getActiveExecutions() as IExecutionsCurrentSummary[];
|
let executingWorkflows = activeExecutionsInstance.getActiveExecutions();
|
||||||
|
|
||||||
let count = 0;
|
let count = 0;
|
||||||
while (executingWorkflows.length !== 0) {
|
while (executingWorkflows.length !== 0) {
|
||||||
if (count++ % 4 === 0) {
|
if (count++ % 4 === 0) {
|
||||||
console.log(`Waiting for ${executingWorkflows.length} active executions to finish...`);
|
console.log(`Waiting for ${executingWorkflows.length} active executions to finish...`);
|
||||||
executingWorkflows.map(execution => {
|
// eslint-disable-next-line array-callback-return
|
||||||
|
executingWorkflows.map((execution) => {
|
||||||
console.log(` - Execution ID ${execution.id}, workflow ID: ${execution.workflowId}`);
|
console.log(` - Execution ID ${execution.id}, workflow ID: ${execution.workflowId}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
setTimeout(resolve, 500);
|
setTimeout(resolve, 500);
|
||||||
});
|
});
|
||||||
executingWorkflows = activeExecutionsInstance.getActiveExecutions();
|
executingWorkflows = activeExecutionsInstance.getActiveExecutions();
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('There was an error shutting down n8n.', error);
|
console.error('There was an error shutting down n8n.', error);
|
||||||
}
|
}
|
||||||
|
@ -127,12 +136,12 @@ export class Start extends Command {
|
||||||
process.exit(processExistCode);
|
process.exit(processExistCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
// Make sure that n8n shuts down gracefully if possible
|
// Make sure that n8n shuts down gracefully if possible
|
||||||
process.on('SIGTERM', Start.stopProcess);
|
process.on('SIGTERM', Start.stopProcess);
|
||||||
process.on('SIGINT', Start.stopProcess);
|
process.on('SIGINT', Start.stopProcess);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
const { flags } = this.parse(Start);
|
const { flags } = this.parse(Start);
|
||||||
|
|
||||||
// Wrap that the process does not close but we can still use async
|
// Wrap that the process does not close but we can still use async
|
||||||
|
@ -143,7 +152,9 @@ export class Start extends Command {
|
||||||
logger.info('Initializing n8n process');
|
logger.info('Initializing n8n process');
|
||||||
|
|
||||||
// todo remove a few versions after release
|
// todo remove a few versions after release
|
||||||
logger.info('\nn8n now checks for new versions and security updates. You can turn this off using the environment variable N8N_VERSION_NOTIFICATIONS_ENABLED to "false"\nFor more information, please refer to https://docs.n8n.io/getting-started/installation/advanced/configuration.html\n');
|
logger.info(
|
||||||
|
'\nn8n now checks for new versions and security updates. You can turn this off using the environment variable N8N_VERSION_NOTIFICATIONS_ENABLED to "false"\nFor more information, please refer to https://docs.n8n.io/getting-started/installation/advanced/configuration.html\n',
|
||||||
|
);
|
||||||
|
|
||||||
// Start directly with the init of the database to improve startup time
|
// Start directly with the init of the database to improve startup time
|
||||||
const startDbInitPromise = Db.init().catch((error: Error) => {
|
const startDbInitPromise = Db.init().catch((error: Error) => {
|
||||||
|
@ -185,9 +196,11 @@ export class Start extends Command {
|
||||||
const redisPort = config.get('queue.bull.redis.port');
|
const redisPort = config.get('queue.bull.redis.port');
|
||||||
const redisDB = config.get('queue.bull.redis.db');
|
const redisDB = config.get('queue.bull.redis.db');
|
||||||
const redisConnectionTimeoutLimit = config.get('queue.bull.redis.timeoutThreshold');
|
const redisConnectionTimeoutLimit = config.get('queue.bull.redis.timeoutThreshold');
|
||||||
let lastTimer = 0, cumulativeTimeout = 0;
|
let lastTimer = 0;
|
||||||
|
let cumulativeTimeout = 0;
|
||||||
|
|
||||||
const settings = {
|
const settings = {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
retryStrategy: (times: number): number | null => {
|
retryStrategy: (times: number): number | null => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - lastTimer > 30000) {
|
if (now - lastTimer > 30000) {
|
||||||
|
@ -198,7 +211,10 @@ export class Start extends Command {
|
||||||
cumulativeTimeout += now - lastTimer;
|
cumulativeTimeout += now - lastTimer;
|
||||||
lastTimer = now;
|
lastTimer = now;
|
||||||
if (cumulativeTimeout > redisConnectionTimeoutLimit) {
|
if (cumulativeTimeout > redisConnectionTimeoutLimit) {
|
||||||
logger.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + ". Exiting process.");
|
logger.error(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
|
`Unable to connect to Redis after ${redisConnectionTimeoutLimit}. Exiting process.`,
|
||||||
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -234,20 +250,24 @@ export class Start extends Command {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const dbType = await GenericHelpers.getConfigValue('database.type') as DatabaseType;
|
const dbType = (await GenericHelpers.getConfigValue('database.type')) as DatabaseType;
|
||||||
|
|
||||||
if (dbType === 'sqlite') {
|
if (dbType === 'sqlite') {
|
||||||
const shouldRunVacuum = config.get('database.sqlite.executeVacuumOnStartup') as number;
|
const shouldRunVacuum = config.get('database.sqlite.executeVacuumOnStartup') as number;
|
||||||
if (shouldRunVacuum) {
|
if (shouldRunVacuum) {
|
||||||
Db.collections.Execution!.query('VACUUM;');
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises, @typescript-eslint/no-non-null-assertion
|
||||||
|
await Db.collections.Execution!.query('VACUUM;');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flags.tunnel === true) {
|
if (flags.tunnel) {
|
||||||
this.log('\nWaiting for tunnel ...');
|
this.log('\nWaiting for tunnel ...');
|
||||||
|
|
||||||
let tunnelSubdomain;
|
let tunnelSubdomain;
|
||||||
if (process.env[TUNNEL_SUBDOMAIN_ENV] !== undefined && process.env[TUNNEL_SUBDOMAIN_ENV] !== '') {
|
if (
|
||||||
|
process.env[TUNNEL_SUBDOMAIN_ENV] !== undefined &&
|
||||||
|
process.env[TUNNEL_SUBDOMAIN_ENV] !== ''
|
||||||
|
) {
|
||||||
tunnelSubdomain = process.env[TUNNEL_SUBDOMAIN_ENV];
|
tunnelSubdomain = process.env[TUNNEL_SUBDOMAIN_ENV];
|
||||||
} else if (userSettings.tunnelSubdomain !== undefined) {
|
} else if (userSettings.tunnelSubdomain !== undefined) {
|
||||||
tunnelSubdomain = userSettings.tunnelSubdomain;
|
tunnelSubdomain = userSettings.tunnelSubdomain;
|
||||||
|
@ -256,9 +276,13 @@ export class Start extends Command {
|
||||||
if (tunnelSubdomain === undefined) {
|
if (tunnelSubdomain === undefined) {
|
||||||
// When no tunnel subdomain did exist yet create a new random one
|
// When no tunnel subdomain did exist yet create a new random one
|
||||||
const availableCharacters = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
const availableCharacters = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
userSettings.tunnelSubdomain = Array.from({ length: 24 }).map(() => {
|
userSettings.tunnelSubdomain = Array.from({ length: 24 })
|
||||||
return availableCharacters.charAt(Math.floor(Math.random() * availableCharacters.length));
|
.map(() => {
|
||||||
}).join('');
|
return availableCharacters.charAt(
|
||||||
|
Math.floor(Math.random() * availableCharacters.length),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
|
||||||
await UserSettings.writeUserSettings(userSettings);
|
await UserSettings.writeUserSettings(userSettings);
|
||||||
}
|
}
|
||||||
|
@ -268,14 +292,16 @@ export class Start extends Command {
|
||||||
subdomain: tunnelSubdomain,
|
subdomain: tunnelSubdomain,
|
||||||
};
|
};
|
||||||
|
|
||||||
const port = config.get('port') as number;
|
const port = config.get('port');
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const webhookTunnel = await localtunnel(port, tunnelSettings);
|
const webhookTunnel = await localtunnel(port, tunnelSettings);
|
||||||
|
|
||||||
process.env.WEBHOOK_URL = webhookTunnel.url + '/';
|
process.env.WEBHOOK_URL = `${webhookTunnel.url}/`;
|
||||||
this.log(`Tunnel URL: ${process.env.WEBHOOK_URL}\n`);
|
this.log(`Tunnel URL: ${process.env.WEBHOOK_URL}\n`);
|
||||||
this.log('IMPORTANT! Do not share with anybody as it would give people access to your n8n instance!');
|
this.log(
|
||||||
|
'IMPORTANT! Do not share with anybody as it would give people access to your n8n instance!',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Server.start();
|
await Server.start();
|
||||||
|
@ -284,6 +310,9 @@ export class Start extends Command {
|
||||||
activeWorkflowRunner = ActiveWorkflowRunner.getInstance();
|
activeWorkflowRunner = ActiveWorkflowRunner.getInstance();
|
||||||
await activeWorkflowRunner.init();
|
await activeWorkflowRunner.init();
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const waitTracker = WaitTracker();
|
||||||
|
|
||||||
const editorUrl = GenericHelpers.getBaseUrl();
|
const editorUrl = GenericHelpers.getBaseUrl();
|
||||||
this.log(`\nEditor is now accessible via:\n${editorUrl}`);
|
this.log(`\nEditor is now accessible via:\n${editorUrl}`);
|
||||||
|
|
||||||
|
@ -294,7 +323,7 @@ export class Start extends Command {
|
||||||
process.stdin.setEncoding('utf8');
|
process.stdin.setEncoding('utf8');
|
||||||
let inputText = '';
|
let inputText = '';
|
||||||
|
|
||||||
if (flags.open === true) {
|
if (flags.open) {
|
||||||
Start.openBrowser();
|
Start.openBrowser();
|
||||||
}
|
}
|
||||||
this.log(`\nPress "o" to open in Browser.`);
|
this.log(`\nPress "o" to open in Browser.`);
|
||||||
|
@ -304,15 +333,18 @@ export class Start extends Command {
|
||||||
inputText = '';
|
inputText = '';
|
||||||
} else if (key.charCodeAt(0) === 3) {
|
} else if (key.charCodeAt(0) === 3) {
|
||||||
// Ctrl + c got pressed
|
// Ctrl + c got pressed
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
Start.stopProcess();
|
Start.stopProcess();
|
||||||
} else {
|
} else {
|
||||||
// When anything else got pressed, record it and send it on enter into the child process
|
// When anything else got pressed, record it and send it on enter into the child process
|
||||||
|
// eslint-disable-next-line no-lonely-if
|
||||||
if (key.charCodeAt(0) === 13) {
|
if (key.charCodeAt(0) === 13) {
|
||||||
// send to child process and print in terminal
|
// send to child process and print in terminal
|
||||||
process.stdout.write('\n');
|
process.stdout.write('\n');
|
||||||
inputText = '';
|
inputText = '';
|
||||||
} else {
|
} else {
|
||||||
// record it and write into terminal
|
// record it and write into terminal
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
inputText += key;
|
inputText += key;
|
||||||
process.stdout.write(key);
|
process.stdout.write(key);
|
||||||
}
|
}
|
||||||
|
@ -320,6 +352,7 @@ export class Start extends Command {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
this.error(`There was an error: ${error.message}`);
|
this.error(`There was an error: ${error.message}`);
|
||||||
|
|
||||||
processExistCode = 1;
|
processExistCode = 1;
|
||||||
|
|
|
@ -1,26 +1,16 @@
|
||||||
import {
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
Command, flags,
|
/* eslint-disable no-console */
|
||||||
} from '@oclif/command';
|
import { Command, flags } from '@oclif/command';
|
||||||
|
|
||||||
import {
|
import { IDataObject, LoggerProxy } from 'n8n-workflow';
|
||||||
IDataObject
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
import {
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
Db,
|
import { Db, GenericHelpers } from '../../src';
|
||||||
GenericHelpers,
|
|
||||||
} from '../../src';
|
|
||||||
|
|
||||||
import {
|
import { getLogger } from '../../src/Logger';
|
||||||
getLogger,
|
|
||||||
} from '../../src/Logger';
|
|
||||||
|
|
||||||
import {
|
|
||||||
LoggerProxy,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
export class UpdateWorkflowCommand extends Command {
|
export class UpdateWorkflowCommand extends Command {
|
||||||
static description = '\Update workflows';
|
static description = 'Update workflows';
|
||||||
|
|
||||||
static examples = [
|
static examples = [
|
||||||
`$ n8n update:workflow --all --active=false`,
|
`$ n8n update:workflow --all --active=false`,
|
||||||
|
@ -40,10 +30,12 @@ export class UpdateWorkflowCommand extends Command {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
async run() {
|
async run() {
|
||||||
const logger = getLogger();
|
const logger = getLogger();
|
||||||
LoggerProxy.init(logger);
|
LoggerProxy.init(logger);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
const { flags } = this.parse(UpdateWorkflowCommand);
|
const { flags } = this.parse(UpdateWorkflowCommand);
|
||||||
|
|
||||||
if (!flags.all && !flags.id) {
|
if (!flags.all && !flags.id) {
|
||||||
|
@ -52,7 +44,9 @@ export class UpdateWorkflowCommand extends Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flags.all && flags.id) {
|
if (flags.all && flags.id) {
|
||||||
console.info(`Either something else on top should be "--all" or "--id" can be set never both!`);
|
console.info(
|
||||||
|
`Either something else on top should be "--all" or "--id" can be set never both!`,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,13 +54,12 @@ export class UpdateWorkflowCommand extends Command {
|
||||||
if (flags.active === undefined) {
|
if (flags.active === undefined) {
|
||||||
console.info(`No update flag like "--active=true" has been set!`);
|
console.info(`No update flag like "--active=true" has been set!`);
|
||||||
return;
|
return;
|
||||||
} else {
|
|
||||||
if (!['false', 'true'].includes(flags.active)) {
|
|
||||||
console.info(`Valid values for flag "--active" are only "false" or "true"!`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
updateQuery.active = flags.active === 'true';
|
|
||||||
}
|
}
|
||||||
|
if (!['false', 'true'].includes(flags.active)) {
|
||||||
|
console.info(`Valid values for flag "--active" are only "false" or "true"!`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateQuery.active = flags.active === 'true';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Db.init();
|
await Db.init();
|
||||||
|
@ -80,6 +73,7 @@ export class UpdateWorkflowCommand extends Command {
|
||||||
findQuery.active = true;
|
findQuery.active = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
await Db.collections.Workflow!.update(findQuery, updateQuery);
|
await Db.collections.Workflow!.update(findQuery, updateQuery);
|
||||||
console.info('Done');
|
console.info('Done');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
import {
|
/* eslint-disable no-console */
|
||||||
UserSettings,
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
} from 'n8n-core';
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
|
/* eslint-disable @typescript-eslint/unbound-method */
|
||||||
|
import { UserSettings } from 'n8n-core';
|
||||||
import { Command, flags } from '@oclif/command';
|
import { Command, flags } from '@oclif/command';
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
|
|
||||||
|
import { IDataObject, LoggerProxy } from 'n8n-workflow';
|
||||||
import * as config from '../config';
|
import * as config from '../config';
|
||||||
import {
|
import {
|
||||||
ActiveExecutions,
|
ActiveExecutions,
|
||||||
|
@ -15,29 +20,20 @@ import {
|
||||||
GenericHelpers,
|
GenericHelpers,
|
||||||
LoadNodesAndCredentials,
|
LoadNodesAndCredentials,
|
||||||
NodeTypes,
|
NodeTypes,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
TestWebhooks,
|
TestWebhooks,
|
||||||
WebhookServer,
|
WebhookServer,
|
||||||
} from '../src';
|
} from '../src';
|
||||||
import { IDataObject } from 'n8n-workflow';
|
|
||||||
|
|
||||||
import {
|
import { getLogger } from '../src/Logger';
|
||||||
getLogger,
|
|
||||||
} from '../src/Logger';
|
|
||||||
|
|
||||||
import {
|
|
||||||
LoggerProxy,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
let activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner | undefined;
|
let activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner | undefined;
|
||||||
let processExistCode = 0;
|
let processExistCode = 0;
|
||||||
|
|
||||||
|
|
||||||
export class Webhook extends Command {
|
export class Webhook extends Command {
|
||||||
static description = 'Starts n8n webhook process. Intercepts only production URLs.';
|
static description = 'Starts n8n webhook process. Intercepts only production URLs.';
|
||||||
|
|
||||||
static examples = [
|
static examples = [`$ n8n webhook`];
|
||||||
`$ n8n webhook`,
|
|
||||||
];
|
|
||||||
|
|
||||||
static flags = {
|
static flags = {
|
||||||
help: flags.help({ char: 'h' }),
|
help: flags.help({ char: 'h' }),
|
||||||
|
@ -48,6 +44,7 @@ export class Webhook extends Command {
|
||||||
* Make for example sure that all the webhooks from third party services
|
* Make for example sure that all the webhooks from third party services
|
||||||
* get removed.
|
* get removed.
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
static async stopProcess() {
|
static async stopProcess() {
|
||||||
LoggerProxy.info(`\nStopping n8n...`);
|
LoggerProxy.info(`\nStopping n8n...`);
|
||||||
|
|
||||||
|
@ -68,14 +65,16 @@ export class Webhook extends Command {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
while (executingWorkflows.length !== 0) {
|
while (executingWorkflows.length !== 0) {
|
||||||
if (count++ % 4 === 0) {
|
if (count++ % 4 === 0) {
|
||||||
LoggerProxy.info(`Waiting for ${executingWorkflows.length} active executions to finish...`);
|
LoggerProxy.info(
|
||||||
|
`Waiting for ${executingWorkflows.length} active executions to finish...`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
setTimeout(resolve, 500);
|
setTimeout(resolve, 500);
|
||||||
});
|
});
|
||||||
executingWorkflows = activeExecutionsInstance.getActiveExecutions();
|
executingWorkflows = activeExecutionsInstance.getActiveExecutions();
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
LoggerProxy.error('There was an error shutting down n8n.', error);
|
LoggerProxy.error('There was an error shutting down n8n.', error);
|
||||||
}
|
}
|
||||||
|
@ -83,7 +82,7 @@ export class Webhook extends Command {
|
||||||
process.exit(processExistCode);
|
process.exit(processExistCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
async run() {
|
async run() {
|
||||||
const logger = getLogger();
|
const logger = getLogger();
|
||||||
LoggerProxy.init(logger);
|
LoggerProxy.init(logger);
|
||||||
|
@ -92,6 +91,7 @@ export class Webhook extends Command {
|
||||||
process.on('SIGTERM', Webhook.stopProcess);
|
process.on('SIGTERM', Webhook.stopProcess);
|
||||||
process.on('SIGINT', Webhook.stopProcess);
|
process.on('SIGINT', Webhook.stopProcess);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-shadow
|
||||||
const { flags } = this.parse(Webhook);
|
const { flags } = this.parse(Webhook);
|
||||||
|
|
||||||
// Wrap that the process does not close but we can still use async
|
// Wrap that the process does not close but we can still use async
|
||||||
|
@ -114,7 +114,8 @@ export class Webhook extends Command {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Start directly with the init of the database to improve startup time
|
// Start directly with the init of the database to improve startup time
|
||||||
const startDbInitPromise = Db.init().catch(error => {
|
const startDbInitPromise = Db.init().catch((error) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
|
||||||
logger.error(`There was an error initializing DB: "${error.message}"`);
|
logger.error(`There was an error initializing DB: "${error.message}"`);
|
||||||
|
|
||||||
processExistCode = 1;
|
processExistCode = 1;
|
||||||
|
@ -124,6 +125,7 @@ export class Webhook extends Command {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Make sure the settings exist
|
// Make sure the settings exist
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const userSettings = await UserSettings.prepareUserSettings();
|
const userSettings = await UserSettings.prepareUserSettings();
|
||||||
|
|
||||||
// Load all node and credential types
|
// Load all node and credential types
|
||||||
|
@ -153,9 +155,11 @@ export class Webhook extends Command {
|
||||||
const redisPort = config.get('queue.bull.redis.port');
|
const redisPort = config.get('queue.bull.redis.port');
|
||||||
const redisDB = config.get('queue.bull.redis.db');
|
const redisDB = config.get('queue.bull.redis.db');
|
||||||
const redisConnectionTimeoutLimit = config.get('queue.bull.redis.timeoutThreshold');
|
const redisConnectionTimeoutLimit = config.get('queue.bull.redis.timeoutThreshold');
|
||||||
let lastTimer = 0, cumulativeTimeout = 0;
|
let lastTimer = 0;
|
||||||
|
let cumulativeTimeout = 0;
|
||||||
|
|
||||||
const settings = {
|
const settings = {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
retryStrategy: (times: number): number | null => {
|
retryStrategy: (times: number): number | null => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - lastTimer > 30000) {
|
if (now - lastTimer > 30000) {
|
||||||
|
@ -166,7 +170,10 @@ export class Webhook extends Command {
|
||||||
cumulativeTimeout += now - lastTimer;
|
cumulativeTimeout += now - lastTimer;
|
||||||
lastTimer = now;
|
lastTimer = now;
|
||||||
if (cumulativeTimeout > redisConnectionTimeoutLimit) {
|
if (cumulativeTimeout > redisConnectionTimeoutLimit) {
|
||||||
logger.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + ". Exiting process.");
|
logger.error(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
|
`Unable to connect to Redis after ${redisConnectionTimeoutLimit}. Exiting process.`,
|
||||||
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -208,11 +215,12 @@ export class Webhook extends Command {
|
||||||
activeWorkflowRunner = ActiveWorkflowRunner.getInstance();
|
activeWorkflowRunner = ActiveWorkflowRunner.getInstance();
|
||||||
await activeWorkflowRunner.initWebhooks();
|
await activeWorkflowRunner.initWebhooks();
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const editorUrl = GenericHelpers.getBaseUrl();
|
const editorUrl = GenericHelpers.getBaseUrl();
|
||||||
console.info('Webhook listener waiting for requests.');
|
console.info('Webhook listener waiting for requests.');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Exiting due to error. See log message for details.');
|
console.error('Exiting due to error. See log message for details.');
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
logger.error(`Webhook process cannot continue. "${error.message}"`);
|
logger.error(`Webhook process cannot continue. "${error.message}"`);
|
||||||
|
|
||||||
processExistCode = 1;
|
processExistCode = 1;
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
/* eslint-disable @typescript-eslint/no-shadow */
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
/* eslint-disable @typescript-eslint/unbound-method */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
|
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
import * as PCancelable from 'p-cancelable';
|
import * as PCancelable from 'p-cancelable';
|
||||||
|
|
||||||
import { Command, flags } from '@oclif/command';
|
import { Command, flags } from '@oclif/command';
|
||||||
import {
|
import { UserSettings, WorkflowExecute } from 'n8n-core';
|
||||||
UserSettings,
|
|
||||||
WorkflowExecute,
|
|
||||||
} from 'n8n-core';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IDataObject,
|
IDataObject,
|
||||||
|
@ -13,12 +19,12 @@ import {
|
||||||
IWorkflowExecuteHooks,
|
IWorkflowExecuteHooks,
|
||||||
Workflow,
|
Workflow,
|
||||||
WorkflowHooks,
|
WorkflowHooks,
|
||||||
|
LoggerProxy,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import {
|
import { FindOneOptions } from 'typeorm';
|
||||||
FindOneOptions,
|
|
||||||
} from 'typeorm';
|
|
||||||
|
|
||||||
|
import * as Bull from 'bull';
|
||||||
import {
|
import {
|
||||||
ActiveExecutions,
|
ActiveExecutions,
|
||||||
CredentialsOverwrites,
|
CredentialsOverwrites,
|
||||||
|
@ -37,24 +43,15 @@ import {
|
||||||
WorkflowExecuteAdditionalData,
|
WorkflowExecuteAdditionalData,
|
||||||
} from '../src';
|
} from '../src';
|
||||||
|
|
||||||
import {
|
import { getLogger } from '../src/Logger';
|
||||||
getLogger,
|
|
||||||
} from '../src/Logger';
|
|
||||||
|
|
||||||
import {
|
|
||||||
LoggerProxy,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
import * as config from '../config';
|
import * as config from '../config';
|
||||||
import * as Bull from 'bull';
|
|
||||||
import * as Queue from '../src/Queue';
|
import * as Queue from '../src/Queue';
|
||||||
|
|
||||||
export class Worker extends Command {
|
export class Worker extends Command {
|
||||||
static description = '\nStarts a n8n worker';
|
static description = '\nStarts a n8n worker';
|
||||||
|
|
||||||
static examples = [
|
static examples = [`$ n8n worker --concurrency=5`];
|
||||||
`$ n8n worker --concurrency=5`,
|
|
||||||
];
|
|
||||||
|
|
||||||
static flags = {
|
static flags = {
|
||||||
help: flags.help({ char: 'h' }),
|
help: flags.help({ char: 'h' }),
|
||||||
|
@ -82,6 +79,7 @@ export class Worker extends Command {
|
||||||
LoggerProxy.info(`Stopping n8n...`);
|
LoggerProxy.info(`Stopping n8n...`);
|
||||||
|
|
||||||
// Stop accepting new jobs
|
// Stop accepting new jobs
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
Worker.jobQueue.pause(true);
|
Worker.jobQueue.pause(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -103,13 +101,17 @@ export class Worker extends Command {
|
||||||
while (Object.keys(Worker.runningJobs).length !== 0) {
|
while (Object.keys(Worker.runningJobs).length !== 0) {
|
||||||
if (count++ % 4 === 0) {
|
if (count++ % 4 === 0) {
|
||||||
const waitLeft = Math.ceil((stopTime - new Date().getTime()) / 1000);
|
const waitLeft = Math.ceil((stopTime - new Date().getTime()) / 1000);
|
||||||
LoggerProxy.info(`Waiting for ${Object.keys(Worker.runningJobs).length} active executions to finish... (wait ${waitLeft} more seconds)`);
|
LoggerProxy.info(
|
||||||
|
`Waiting for ${
|
||||||
|
Object.keys(Worker.runningJobs).length
|
||||||
|
} active executions to finish... (wait ${waitLeft} more seconds)`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
setTimeout(resolve, 500);
|
setTimeout(resolve, 500);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
LoggerProxy.error('There was an error shutting down n8n.', error);
|
LoggerProxy.error('There was an error shutting down n8n.', error);
|
||||||
}
|
}
|
||||||
|
@ -119,25 +121,38 @@ export class Worker extends Command {
|
||||||
|
|
||||||
async runJob(job: Bull.Job, nodeTypes: INodeTypes): Promise<IBullJobResponse> {
|
async runJob(job: Bull.Job, nodeTypes: INodeTypes): Promise<IBullJobResponse> {
|
||||||
const jobData = job.data as IBullJobData;
|
const jobData = job.data as IBullJobData;
|
||||||
const executionDb = await Db.collections.Execution!.findOne(jobData.executionId) as IExecutionFlattedDb;
|
const executionDb = (await Db.collections.Execution!.findOne(
|
||||||
const currentExecutionDb = ResponseHelper.unflattenExecutionData(executionDb) as IExecutionResponse;
|
jobData.executionId,
|
||||||
LoggerProxy.info(`Start job: ${job.id} (Workflow ID: ${currentExecutionDb.workflowData.id} | Execution: ${jobData.executionId})`);
|
)) as IExecutionFlattedDb;
|
||||||
|
const currentExecutionDb = ResponseHelper.unflattenExecutionData(executionDb);
|
||||||
|
LoggerProxy.info(
|
||||||
|
`Start job: ${job.id} (Workflow ID: ${currentExecutionDb.workflowData.id} | Execution: ${jobData.executionId})`,
|
||||||
|
);
|
||||||
|
|
||||||
let staticData = currentExecutionDb.workflowData!.staticData;
|
let { staticData } = currentExecutionDb.workflowData;
|
||||||
if (jobData.loadStaticData === true) {
|
if (jobData.loadStaticData) {
|
||||||
const findOptions = {
|
const findOptions = {
|
||||||
select: ['id', 'staticData'],
|
select: ['id', 'staticData'],
|
||||||
} as FindOneOptions;
|
} as FindOneOptions;
|
||||||
const workflowData = await Db.collections!.Workflow!.findOne(currentExecutionDb.workflowData.id, findOptions);
|
const workflowData = await Db.collections.Workflow!.findOne(
|
||||||
|
currentExecutionDb.workflowData.id,
|
||||||
|
findOptions,
|
||||||
|
);
|
||||||
if (workflowData === undefined) {
|
if (workflowData === undefined) {
|
||||||
throw new Error(`The workflow with the ID "${currentExecutionDb.workflowData.id}" could not be found`);
|
throw new Error(
|
||||||
|
`The workflow with the ID "${currentExecutionDb.workflowData.id}" could not be found`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
staticData = workflowData.staticData;
|
staticData = workflowData.staticData;
|
||||||
}
|
}
|
||||||
|
|
||||||
let workflowTimeout = config.get('executions.timeout') as number; // initialize with default
|
let workflowTimeout = config.get('executions.timeout') as number; // initialize with default
|
||||||
if (currentExecutionDb.workflowData.settings && currentExecutionDb.workflowData.settings.executionTimeout) {
|
if (
|
||||||
workflowTimeout = currentExecutionDb.workflowData.settings!.executionTimeout as number; // preference on workflow setting
|
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
|
||||||
|
currentExecutionDb.workflowData.settings &&
|
||||||
|
currentExecutionDb.workflowData.settings.executionTimeout
|
||||||
|
) {
|
||||||
|
workflowTimeout = currentExecutionDb.workflowData.settings.executionTimeout as number; // preference on workflow setting
|
||||||
}
|
}
|
||||||
|
|
||||||
let executionTimeoutTimestamp: number | undefined;
|
let executionTimeoutTimestamp: number | undefined;
|
||||||
|
@ -146,17 +161,37 @@ export class Worker extends Command {
|
||||||
executionTimeoutTimestamp = Date.now() + workflowTimeout * 1000;
|
executionTimeoutTimestamp = Date.now() + workflowTimeout * 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
const workflow = new Workflow({ id: currentExecutionDb.workflowData.id as string, name: currentExecutionDb.workflowData.name, nodes: currentExecutionDb.workflowData!.nodes, connections: currentExecutionDb.workflowData!.connections, active: currentExecutionDb.workflowData!.active, nodeTypes, staticData, settings: currentExecutionDb.workflowData!.settings });
|
const workflow = new Workflow({
|
||||||
|
id: currentExecutionDb.workflowData.id as string,
|
||||||
|
name: currentExecutionDb.workflowData.name,
|
||||||
|
nodes: currentExecutionDb.workflowData.nodes,
|
||||||
|
connections: currentExecutionDb.workflowData.connections,
|
||||||
|
active: currentExecutionDb.workflowData.active,
|
||||||
|
nodeTypes,
|
||||||
|
staticData,
|
||||||
|
settings: currentExecutionDb.workflowData.settings,
|
||||||
|
});
|
||||||
|
|
||||||
const credentials = await WorkflowCredentials(currentExecutionDb.workflowData.nodes);
|
const additionalData = await WorkflowExecuteAdditionalData.getBase(
|
||||||
|
undefined,
|
||||||
const additionalData = await WorkflowExecuteAdditionalData.getBase(credentials, undefined, executionTimeoutTimestamp);
|
executionTimeoutTimestamp,
|
||||||
additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(currentExecutionDb.mode, job.data.executionId, currentExecutionDb.workflowData, { retryOf: currentExecutionDb.retryOf as string });
|
);
|
||||||
|
additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(
|
||||||
|
currentExecutionDb.mode,
|
||||||
|
job.data.executionId,
|
||||||
|
currentExecutionDb.workflowData,
|
||||||
|
{ retryOf: currentExecutionDb.retryOf as string },
|
||||||
|
);
|
||||||
|
additionalData.executionId = jobData.executionId;
|
||||||
|
|
||||||
let workflowExecute: WorkflowExecute;
|
let workflowExecute: WorkflowExecute;
|
||||||
let workflowRun: PCancelable<IRun>;
|
let workflowRun: PCancelable<IRun>;
|
||||||
if (currentExecutionDb.data !== undefined) {
|
if (currentExecutionDb.data !== undefined) {
|
||||||
workflowExecute = new WorkflowExecute(additionalData, currentExecutionDb.mode, currentExecutionDb.data);
|
workflowExecute = new WorkflowExecute(
|
||||||
|
additionalData,
|
||||||
|
currentExecutionDb.mode,
|
||||||
|
currentExecutionDb.data,
|
||||||
|
);
|
||||||
workflowRun = workflowExecute.processRunExecutionData(workflow);
|
workflowRun = workflowExecute.processRunExecutionData(workflow);
|
||||||
} else {
|
} else {
|
||||||
// Execute all nodes
|
// Execute all nodes
|
||||||
|
@ -181,6 +216,7 @@ export class Worker extends Command {
|
||||||
const logger = getLogger();
|
const logger = getLogger();
|
||||||
LoggerProxy.init(logger);
|
LoggerProxy.init(logger);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.info('Starting n8n worker...');
|
console.info('Starting n8n worker...');
|
||||||
|
|
||||||
// Make sure that n8n shuts down gracefully if possible
|
// Make sure that n8n shuts down gracefully if possible
|
||||||
|
@ -193,7 +229,7 @@ export class Worker extends Command {
|
||||||
const { flags } = this.parse(Worker);
|
const { flags } = this.parse(Worker);
|
||||||
|
|
||||||
// Start directly with the init of the database to improve startup time
|
// Start directly with the init of the database to improve startup time
|
||||||
const startDbInitPromise = Db.init().catch(error => {
|
const startDbInitPromise = Db.init().catch((error) => {
|
||||||
logger.error(`There was an error initializing DB: "${error.message}"`);
|
logger.error(`There was an error initializing DB: "${error.message}"`);
|
||||||
|
|
||||||
Worker.processExistCode = 1;
|
Worker.processExistCode = 1;
|
||||||
|
@ -226,10 +262,12 @@ export class Worker extends Command {
|
||||||
// Wait till the database is ready
|
// Wait till the database is ready
|
||||||
await startDbInitPromise;
|
await startDbInitPromise;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
const redisConnectionTimeoutLimit = config.get('queue.bull.redis.timeoutThreshold');
|
const redisConnectionTimeoutLimit = config.get('queue.bull.redis.timeoutThreshold');
|
||||||
|
|
||||||
Worker.jobQueue = Queue.getInstance().getBullObjectInstance();
|
Worker.jobQueue = Queue.getInstance().getBullObjectInstance();
|
||||||
Worker.jobQueue.process(flags.concurrency, (job) => this.runJob(job, nodeTypes));
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
Worker.jobQueue.process(flags.concurrency, async (job) => this.runJob(job, nodeTypes));
|
||||||
|
|
||||||
const versions = await GenericHelpers.getVersions();
|
const versions = await GenericHelpers.getVersions();
|
||||||
|
|
||||||
|
@ -252,9 +290,10 @@ export class Worker extends Command {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let lastTimer = 0, cumulativeTimeout = 0;
|
let lastTimer = 0;
|
||||||
|
let cumulativeTimeout = 0;
|
||||||
Worker.jobQueue.on('error', (error: Error) => {
|
Worker.jobQueue.on('error', (error: Error) => {
|
||||||
if (error.toString().includes('ECONNREFUSED') === true) {
|
if (error.toString().includes('ECONNREFUSED')) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - lastTimer > 30000) {
|
if (now - lastTimer > 30000) {
|
||||||
// Means we had no timeout at all or last timeout was temporary and we recovered
|
// Means we had no timeout at all or last timeout was temporary and we recovered
|
||||||
|
@ -264,12 +303,14 @@ export class Worker extends Command {
|
||||||
cumulativeTimeout += now - lastTimer;
|
cumulativeTimeout += now - lastTimer;
|
||||||
lastTimer = now;
|
lastTimer = now;
|
||||||
if (cumulativeTimeout > redisConnectionTimeoutLimit) {
|
if (cumulativeTimeout > redisConnectionTimeoutLimit) {
|
||||||
logger.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + ". Exiting process.");
|
logger.error(
|
||||||
|
`Unable to connect to Redis after ${redisConnectionTimeoutLimit}. Exiting process.`,
|
||||||
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.warn('Redis unavailable - trying to reconnect...');
|
logger.warn('Redis unavailable - trying to reconnect...');
|
||||||
} else if (error.toString().includes('Error initializing Lua scripts') === true) {
|
} else if (error.toString().includes('Error initializing Lua scripts')) {
|
||||||
// This is a non-recoverable error
|
// This is a non-recoverable error
|
||||||
// Happens when worker starts and Redis is unavailable
|
// Happens when worker starts and Redis is unavailable
|
||||||
// Even if Redis comes back online, worker will be zombie
|
// Even if Redis comes back online, worker will be zombie
|
||||||
|
@ -288,6 +329,5 @@ export class Worker extends Command {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
/* eslint-disable no-restricted-syntax */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
import * as convict from 'convict';
|
import * as convict from 'convict';
|
||||||
import * as dotenv from 'dotenv';
|
import * as dotenv from 'dotenv';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
@ -6,7 +9,6 @@ import * as core from 'n8n-core';
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
const config = convict({
|
const config = convict({
|
||||||
|
|
||||||
database: {
|
database: {
|
||||||
type: {
|
type: {
|
||||||
doc: 'Type of database to use',
|
doc: 'Type of database to use',
|
||||||
|
@ -84,7 +86,6 @@ const config = convict({
|
||||||
env: 'DB_POSTGRESDB_SSL_REJECT_UNAUTHORIZED',
|
env: 'DB_POSTGRESDB_SSL_REJECT_UNAUTHORIZED',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
mysqldb: {
|
mysqldb: {
|
||||||
database: {
|
database: {
|
||||||
|
@ -159,7 +160,6 @@ const config = convict({
|
||||||
},
|
},
|
||||||
|
|
||||||
executions: {
|
executions: {
|
||||||
|
|
||||||
// By default workflows get always executed in their own process.
|
// By default workflows get always executed in their own process.
|
||||||
// If this option gets set to "main" it will run them in the
|
// If this option gets set to "main" it will run them in the
|
||||||
// main-process instead.
|
// main-process instead.
|
||||||
|
@ -489,6 +489,12 @@ const config = convict({
|
||||||
env: 'N8N_ENDPOINT_WEBHOOK',
|
env: 'N8N_ENDPOINT_WEBHOOK',
|
||||||
doc: 'Path for webhook endpoint',
|
doc: 'Path for webhook endpoint',
|
||||||
},
|
},
|
||||||
|
webhookWaiting: {
|
||||||
|
format: String,
|
||||||
|
default: 'webhook-waiting',
|
||||||
|
env: 'N8N_ENDPOINT_WEBHOOK_WAIT',
|
||||||
|
doc: 'Path for waiting-webhook endpoint',
|
||||||
|
},
|
||||||
webhookTest: {
|
webhookTest: {
|
||||||
format: String,
|
format: String,
|
||||||
default: 'webhook-test',
|
default: 'webhook-test',
|
||||||
|
@ -567,7 +573,6 @@ const config = convict({
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TypeError(`The Nodes to exclude is not a valid Array of strings.`);
|
throw new TypeError(`The Nodes to exclude is not a valid Array of strings.`);
|
||||||
}
|
}
|
||||||
|
@ -638,7 +643,6 @@ const config = convict({
|
||||||
env: 'N8N_VERSION_NOTIFICATIONS_INFO_URL',
|
env: 'N8N_VERSION_NOTIFICATIONS_INFO_URL',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Overwrite default configuration with settings which got defined in
|
// Overwrite default configuration with settings which got defined in
|
||||||
|
|
|
@ -3,89 +3,73 @@ import { UserSettings } from 'n8n-core';
|
||||||
import { entities } from '../src/databases/entities';
|
import { entities } from '../src/databases/entities';
|
||||||
|
|
||||||
module.exports = [
|
module.exports = [
|
||||||
{
|
{
|
||||||
"name": "sqlite",
|
name: 'sqlite',
|
||||||
"type": "sqlite",
|
type: 'sqlite',
|
||||||
"logging": true,
|
logging: true,
|
||||||
"entities": Object.values(entities),
|
entities: Object.values(entities),
|
||||||
"database": path.join(UserSettings.getUserN8nFolderPath(), 'database.sqlite'),
|
database: path.join(UserSettings.getUserN8nFolderPath(), 'database.sqlite'),
|
||||||
"migrations": [
|
migrations: ['./src/databases/sqlite/migrations/*.ts'],
|
||||||
"./src/databases/sqlite/migrations/*.ts"
|
subscribers: ['./src/databases/sqlite/subscribers/*.ts'],
|
||||||
],
|
cli: {
|
||||||
"subscribers": [
|
entitiesDir: './src/databases/entities',
|
||||||
"./src/databases/sqlite/subscribers/*.ts"
|
migrationsDir: './src/databases/sqlite/migrations',
|
||||||
],
|
subscribersDir: './src/databases/sqlite/subscribers',
|
||||||
"cli": {
|
},
|
||||||
"entitiesDir": "./src/databases/entities",
|
},
|
||||||
"migrationsDir": "./src/databases/sqlite/migrations",
|
{
|
||||||
"subscribersDir": "./src/databases/sqlite/subscribers"
|
name: 'postgres',
|
||||||
}
|
type: 'postgres',
|
||||||
},
|
logging: false,
|
||||||
{
|
host: 'localhost',
|
||||||
"name": "postgres",
|
username: 'postgres',
|
||||||
"type": "postgres",
|
password: '',
|
||||||
"logging": false,
|
port: 5432,
|
||||||
"host": "localhost",
|
database: 'n8n',
|
||||||
"username": "postgres",
|
schema: 'public',
|
||||||
"password": "",
|
entities: Object.values(entities),
|
||||||
"port": 5432,
|
migrations: ['./src/databases/postgresdb/migrations/*.ts'],
|
||||||
"database": "n8n",
|
subscribers: ['src/subscriber/**/*.ts'],
|
||||||
"schema": "public",
|
cli: {
|
||||||
"entities": Object.values(entities),
|
entitiesDir: './src/databases/entities',
|
||||||
"migrations": [
|
migrationsDir: './src/databases/postgresdb/migrations',
|
||||||
"./src/databases/postgresdb/migrations/*.ts"
|
subscribersDir: './src/databases/postgresdb/subscribers',
|
||||||
],
|
},
|
||||||
"subscribers": [
|
},
|
||||||
"src/subscriber/**/*.ts"
|
{
|
||||||
],
|
name: 'mysql',
|
||||||
"cli": {
|
type: 'mysql',
|
||||||
"entitiesDir": "./src/databases/entities",
|
database: 'n8n',
|
||||||
"migrationsDir": "./src/databases/postgresdb/migrations",
|
username: 'root',
|
||||||
"subscribersDir": "./src/databases/postgresdb/subscribers"
|
password: 'password',
|
||||||
}
|
host: 'localhost',
|
||||||
},
|
port: '3306',
|
||||||
{
|
logging: false,
|
||||||
"name": "mysql",
|
entities: Object.values(entities),
|
||||||
"type": "mysql",
|
migrations: ['./src/databases/mysqldb/migrations/*.ts'],
|
||||||
"database": "n8n",
|
subscribers: ['src/subscriber/**/*.ts'],
|
||||||
"username": "root",
|
cli: {
|
||||||
"password": "password",
|
entitiesDir: './src/databases/entities',
|
||||||
"host": "localhost",
|
migrationsDir: './src/databases/mysqldb/migrations',
|
||||||
"port": "3306",
|
subscribersDir: './src/databases/mysqldb/Subscribers',
|
||||||
"logging": false,
|
},
|
||||||
"entities": Object.values(entities),
|
},
|
||||||
"migrations": [
|
{
|
||||||
"./src/databases/mysqldb/migrations/*.ts"
|
name: 'mariadb',
|
||||||
],
|
type: 'mariadb',
|
||||||
"subscribers": [
|
database: 'n8n',
|
||||||
"src/subscriber/**/*.ts"
|
username: 'root',
|
||||||
],
|
password: 'password',
|
||||||
"cli": {
|
host: 'localhost',
|
||||||
"entitiesDir": "./src/databases/entities",
|
port: '3306',
|
||||||
"migrationsDir": "./src/databases/mysqldb/migrations",
|
logging: false,
|
||||||
"subscribersDir": "./src/databases/mysqldb/Subscribers"
|
entities: Object.values(entities),
|
||||||
}
|
migrations: ['./src/databases/mysqldb/migrations/*.ts'],
|
||||||
},
|
subscribers: ['src/subscriber/**/*.ts'],
|
||||||
{
|
cli: {
|
||||||
"name": "mariadb",
|
entitiesDir: './src/databases/entities',
|
||||||
"type": "mariadb",
|
migrationsDir: './src/databases/mysqldb/migrations',
|
||||||
"database": "n8n",
|
subscribersDir: './src/databases/mysqldb/Subscribers',
|
||||||
"username": "root",
|
},
|
||||||
"password": "password",
|
},
|
||||||
"host": "localhost",
|
|
||||||
"port": "3306",
|
|
||||||
"logging": false,
|
|
||||||
"entities": Object.values(entities),
|
|
||||||
"migrations": [
|
|
||||||
"./src/databases/mysqldb/migrations/*.ts"
|
|
||||||
],
|
|
||||||
"subscribers": [
|
|
||||||
"src/subscriber/**/*.ts"
|
|
||||||
],
|
|
||||||
"cli": {
|
|
||||||
"entitiesDir": "./src/databases/entities",
|
|
||||||
"migrationsDir": "./src/databases/mysqldb/migrations",
|
|
||||||
"subscribersDir": "./src/databases/mysqldb/Subscribers"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n",
|
"name": "n8n",
|
||||||
"version": "0.132.2",
|
"version": "0.136.0",
|
||||||
"description": "n8n Workflow Automation Tool",
|
"description": "n8n Workflow Automation Tool",
|
||||||
"license": "SEE LICENSE IN LICENSE.md",
|
"license": "SEE LICENSE IN LICENSE.md",
|
||||||
"homepage": "https://n8n.io",
|
"homepage": "https://n8n.io",
|
||||||
|
@ -21,14 +21,15 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"dev": "concurrently -k -n \"TypeScript,Node\" -c \"yellow.bold,cyan.bold\" \"npm run watch\" \"nodemon\"",
|
"dev": "concurrently -k -n \"TypeScript,Node\" -c \"yellow.bold,cyan.bold\" \"npm run watch\" \"nodemon\"",
|
||||||
|
"format": "cd ../.. && node_modules/prettier/bin-prettier.js packages/cli/**/**.ts --write",
|
||||||
|
"lint": "cd ../.. && node_modules/eslint/bin/eslint.js packages/cli",
|
||||||
|
"lintfix": "cd ../.. && node_modules/eslint/bin/eslint.js packages/cli --fix",
|
||||||
"postpack": "rm -f oclif.manifest.json",
|
"postpack": "rm -f oclif.manifest.json",
|
||||||
"prepack": "echo \"Building project...\" && rm -rf dist && tsc -b && oclif-dev manifest",
|
"prepack": "echo \"Building project...\" && rm -rf dist && tsc -b && oclif-dev manifest",
|
||||||
"start": "run-script-os",
|
"start": "run-script-os",
|
||||||
"start:default": "cd bin && ./n8n",
|
"start:default": "cd bin && ./n8n",
|
||||||
"start:windows": "cd bin && n8n",
|
"start:windows": "cd bin && n8n",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"tslint": "tslint -p tsconfig.json -c tslint.json",
|
|
||||||
"tslintfix": "tslint --fix -p tsconfig.json -c tslint.json",
|
|
||||||
"watch": "tsc --watch",
|
"watch": "tsc --watch",
|
||||||
"typeorm": "ts-node ./node_modules/typeorm/cli.js"
|
"typeorm": "ts-node ./node_modules/typeorm/cli.js"
|
||||||
},
|
},
|
||||||
|
@ -77,7 +78,7 @@
|
||||||
"ts-jest": "^26.3.0",
|
"ts-jest": "^26.3.0",
|
||||||
"ts-node": "^8.9.1",
|
"ts-node": "^8.9.1",
|
||||||
"tslint": "^6.1.2",
|
"tslint": "^6.1.2",
|
||||||
"typescript": "~3.9.7"
|
"typescript": "~4.3.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oclif/command": "^1.5.18",
|
"@oclif/command": "^1.5.18",
|
||||||
|
@ -98,8 +99,8 @@
|
||||||
"csrf": "^3.1.0",
|
"csrf": "^3.1.0",
|
||||||
"dotenv": "^8.0.0",
|
"dotenv": "^8.0.0",
|
||||||
"express": "^4.16.4",
|
"express": "^4.16.4",
|
||||||
|
"fast-glob": "^3.2.5",
|
||||||
"flatted": "^2.0.0",
|
"flatted": "^2.0.0",
|
||||||
"glob-promise": "^3.4.0",
|
|
||||||
"google-timezones-json": "^1.0.2",
|
"google-timezones-json": "^1.0.2",
|
||||||
"inquirer": "^7.0.1",
|
"inquirer": "^7.0.1",
|
||||||
"json-diff": "^0.5.4",
|
"json-diff": "^0.5.4",
|
||||||
|
@ -107,11 +108,11 @@
|
||||||
"jwks-rsa": "~1.12.1",
|
"jwks-rsa": "~1.12.1",
|
||||||
"localtunnel": "^2.0.0",
|
"localtunnel": "^2.0.0",
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
"mysql2": "~2.2.0",
|
"mysql2": "~2.3.0",
|
||||||
"n8n-core": "~0.78.0",
|
"n8n-core": "~0.81.0",
|
||||||
"n8n-editor-ui": "~0.100.0",
|
"n8n-editor-ui": "~0.104.0",
|
||||||
"n8n-nodes-base": "~0.129.1",
|
"n8n-nodes-base": "~0.133.0",
|
||||||
"n8n-workflow": "~0.64.0",
|
"n8n-workflow": "~0.66.0",
|
||||||
"oauth-1.0a": "^2.2.6",
|
"oauth-1.0a": "^2.2.6",
|
||||||
"open": "^7.0.0",
|
"open": "^7.0.0",
|
||||||
"pg": "^8.3.0",
|
"pg": "^8.3.0",
|
||||||
|
@ -120,7 +121,7 @@
|
||||||
"sqlite3": "^5.0.1",
|
"sqlite3": "^5.0.1",
|
||||||
"sse-channel": "^3.1.1",
|
"sse-channel": "^3.1.1",
|
||||||
"tslib": "1.14.1",
|
"tslib": "1.14.1",
|
||||||
"typeorm": "0.2.34",
|
"typeorm": "^0.2.30",
|
||||||
"winston": "^3.3.3"
|
"winston": "^3.3.3"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
|
|
|
@ -1,11 +1,18 @@
|
||||||
import {
|
/* eslint-disable prefer-template */
|
||||||
IRun,
|
/* eslint-disable @typescript-eslint/restrict-plus-operands */
|
||||||
} from 'n8n-workflow';
|
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
|
||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
|
import { IRun } from 'n8n-workflow';
|
||||||
|
|
||||||
import {
|
import { createDeferredPromise } from 'n8n-core';
|
||||||
createDeferredPromise,
|
|
||||||
} from 'n8n-core';
|
|
||||||
|
|
||||||
|
import { ChildProcess } from 'child_process';
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
import * as PCancelable from 'p-cancelable';
|
||||||
|
// eslint-disable-next-line import/no-cycle
|
||||||
import {
|
import {
|
||||||
Db,
|
Db,
|
||||||
IExecutingWorkflowData,
|
IExecutingWorkflowData,
|
||||||
|
@ -17,16 +24,11 @@ import {
|
||||||
WorkflowHelpers,
|
WorkflowHelpers,
|
||||||
} from '.';
|
} from '.';
|
||||||
|
|
||||||
import { ChildProcess } from 'child_process';
|
|
||||||
import * as PCancelable from 'p-cancelable';
|
|
||||||
|
|
||||||
|
|
||||||
export class ActiveExecutions {
|
export class ActiveExecutions {
|
||||||
private activeExecutions: {
|
private activeExecutions: {
|
||||||
[index: string]: IExecutingWorkflowData;
|
[index: string]: IExecutingWorkflowData;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new active execution
|
* Add a new active execution
|
||||||
*
|
*
|
||||||
|
@ -35,31 +37,56 @@ export class ActiveExecutions {
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
* @memberof ActiveExecutions
|
* @memberof ActiveExecutions
|
||||||
*/
|
*/
|
||||||
async add(executionData: IWorkflowExecutionDataProcess, process?: ChildProcess): Promise<string> {
|
async add(
|
||||||
|
executionData: IWorkflowExecutionDataProcess,
|
||||||
|
process?: ChildProcess,
|
||||||
|
executionId?: string,
|
||||||
|
): Promise<string> {
|
||||||
|
if (executionId === undefined) {
|
||||||
|
// Is a new execution so save in DB
|
||||||
|
|
||||||
const fullExecutionData: IExecutionDb = {
|
const fullExecutionData: IExecutionDb = {
|
||||||
data: executionData.executionData!,
|
data: executionData.executionData!,
|
||||||
mode: executionData.executionMode,
|
mode: executionData.executionMode,
|
||||||
finished: false,
|
finished: false,
|
||||||
startedAt: new Date(),
|
startedAt: new Date(),
|
||||||
workflowData: executionData.workflowData,
|
workflowData: executionData.workflowData,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (executionData.retryOf !== undefined) {
|
if (executionData.retryOf !== undefined) {
|
||||||
fullExecutionData.retryOf = executionData.retryOf.toString();
|
fullExecutionData.retryOf = executionData.retryOf.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
executionData.workflowData.id !== undefined &&
|
||||||
|
WorkflowHelpers.isWorkflowIdValid(executionData.workflowData.id.toString())
|
||||||
|
) {
|
||||||
|
fullExecutionData.workflowId = executionData.workflowData.id.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const execution = ResponseHelper.flattenExecutionData(fullExecutionData);
|
||||||
|
|
||||||
|
const executionResult = await Db.collections.Execution!.save(
|
||||||
|
execution as IExecutionFlattedDb,
|
||||||
|
);
|
||||||
|
executionId =
|
||||||
|
typeof executionResult.id === 'object'
|
||||||
|
? // @ts-ignore
|
||||||
|
executionResult.id!.toString()
|
||||||
|
: executionResult.id + '';
|
||||||
|
} else {
|
||||||
|
// Is an existing execution we want to finish so update in DB
|
||||||
|
|
||||||
|
const execution = {
|
||||||
|
id: executionId,
|
||||||
|
waitTill: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
await Db.collections.Execution!.update(executionId, execution);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (executionData.workflowData.id !== undefined && WorkflowHelpers.isWorkflowIdValid(executionData.workflowData.id.toString()) === true) {
|
// @ts-ignore
|
||||||
fullExecutionData.workflowId = executionData.workflowData.id.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
const execution = ResponseHelper.flattenExecutionData(fullExecutionData);
|
|
||||||
|
|
||||||
// Save the Execution in DB
|
|
||||||
const executionResult = await Db.collections.Execution!.save(execution as IExecutionFlattedDb);
|
|
||||||
|
|
||||||
const executionId = typeof executionResult.id === "object" ? executionResult.id!.toString() : executionResult.id + "";
|
|
||||||
|
|
||||||
this.activeExecutions[executionId] = {
|
this.activeExecutions[executionId] = {
|
||||||
executionData,
|
executionData,
|
||||||
process,
|
process,
|
||||||
|
@ -67,10 +94,10 @@ export class ActiveExecutions {
|
||||||
postExecutePromises: [],
|
postExecutePromises: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
return executionId;
|
return executionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attaches an execution
|
* Attaches an execution
|
||||||
*
|
*
|
||||||
|
@ -78,15 +105,17 @@ export class ActiveExecutions {
|
||||||
* @param {PCancelable<IRun>} workflowExecution
|
* @param {PCancelable<IRun>} workflowExecution
|
||||||
* @memberof ActiveExecutions
|
* @memberof ActiveExecutions
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
attachWorkflowExecution(executionId: string, workflowExecution: PCancelable<IRun>) {
|
attachWorkflowExecution(executionId: string, workflowExecution: PCancelable<IRun>) {
|
||||||
if (this.activeExecutions[executionId] === undefined) {
|
if (this.activeExecutions[executionId] === undefined) {
|
||||||
throw new Error(`No active execution with id "${executionId}" got found to attach to workflowExecution to!`);
|
throw new Error(
|
||||||
|
`No active execution with id "${executionId}" got found to attach to workflowExecution to!`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.activeExecutions[executionId].workflowExecution = workflowExecution;
|
this.activeExecutions[executionId].workflowExecution = workflowExecution;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove an active execution
|
* Remove an active execution
|
||||||
*
|
*
|
||||||
|
@ -101,6 +130,7 @@ export class ActiveExecutions {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve all the waiting promises
|
// Resolve all the waiting promises
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const promise of this.activeExecutions[executionId].postExecutePromises) {
|
for (const promise of this.activeExecutions[executionId].postExecutePromises) {
|
||||||
promise.resolve(fullRunData);
|
promise.resolve(fullRunData);
|
||||||
}
|
}
|
||||||
|
@ -109,7 +139,6 @@ export class ActiveExecutions {
|
||||||
delete this.activeExecutions[executionId];
|
delete this.activeExecutions[executionId];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Forces an execution to stop
|
* Forces an execution to stop
|
||||||
*
|
*
|
||||||
|
@ -130,9 +159,10 @@ export class ActiveExecutions {
|
||||||
// Workflow is running in subprocess
|
// Workflow is running in subprocess
|
||||||
if (this.activeExecutions[executionId].process!.connected) {
|
if (this.activeExecutions[executionId].process!.connected) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// execute on next event loop tick;
|
// execute on next event loop tick;
|
||||||
this.activeExecutions[executionId].process!.send({
|
this.activeExecutions[executionId].process!.send({
|
||||||
type: timeout ? timeout : 'stopExecution',
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
|
type: timeout || 'stopExecution',
|
||||||
});
|
});
|
||||||
}, 1);
|
}, 1);
|
||||||
}
|
}
|
||||||
|
@ -141,10 +171,10 @@ export class ActiveExecutions {
|
||||||
this.activeExecutions[executionId].workflowExecution!.cancel();
|
this.activeExecutions[executionId].workflowExecution!.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
return this.getPostExecutePromise(executionId);
|
return this.getPostExecutePromise(executionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a promise which will resolve with the data of the execution
|
* Returns a promise which will resolve with the data of the execution
|
||||||
* with the given id
|
* with the given id
|
||||||
|
@ -166,7 +196,6 @@ export class ActiveExecutions {
|
||||||
return waitPromise.promise();
|
return waitPromise.promise();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all the currently active executions
|
* Returns all the currently active executions
|
||||||
*
|
*
|
||||||
|
@ -177,25 +206,22 @@ export class ActiveExecutions {
|
||||||
const returnData: IExecutionsCurrentSummary[] = [];
|
const returnData: IExecutionsCurrentSummary[] = [];
|
||||||
|
|
||||||
let data;
|
let data;
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const id of Object.keys(this.activeExecutions)) {
|
for (const id of Object.keys(this.activeExecutions)) {
|
||||||
data = this.activeExecutions[id];
|
data = this.activeExecutions[id];
|
||||||
returnData.push(
|
returnData.push({
|
||||||
{
|
id,
|
||||||
id,
|
retryOf: data.executionData.retryOf as string | undefined,
|
||||||
retryOf: data.executionData.retryOf as string | undefined,
|
startedAt: data.startedAt,
|
||||||
startedAt: data.startedAt,
|
mode: data.executionData.executionMode,
|
||||||
mode: data.executionData.executionMode,
|
workflowId: data.executionData.workflowData.id! as string,
|
||||||
workflowId: data.executionData.workflowData.id! as string,
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return returnData;
|
return returnData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let activeExecutionsInstance: ActiveExecutions | undefined;
|
let activeExecutionsInstance: ActiveExecutions | undefined;
|
||||||
|
|
||||||
export function getInstance(): ActiveExecutions {
|
export function getInstance(): ActiveExecutions {
|
||||||
|
|
|
@ -1,23 +1,15 @@
|
||||||
import {
|
/* eslint-disable prefer-spread */
|
||||||
Db,
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
IActivationError,
|
/* eslint-disable no-param-reassign */
|
||||||
IResponseCallbackData,
|
/* eslint-disable no-console */
|
||||||
IWebhookDb,
|
/* eslint-disable no-await-in-loop */
|
||||||
IWorkflowDb,
|
/* eslint-disable no-restricted-syntax */
|
||||||
IWorkflowExecutionDataProcess,
|
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||||
NodeTypes,
|
/* eslint-disable @typescript-eslint/no-shadow */
|
||||||
ResponseHelper,
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
WebhookHelpers,
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
WorkflowCredentials,
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
WorkflowExecuteAdditionalData,
|
import { ActiveWorkflows, NodeExecuteFunctions } from 'n8n-core';
|
||||||
WorkflowHelpers,
|
|
||||||
WorkflowRunner,
|
|
||||||
} from './';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ActiveWorkflows,
|
|
||||||
NodeExecuteFunctions,
|
|
||||||
} from 'n8n-core';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IExecuteData,
|
IExecuteData,
|
||||||
|
@ -32,12 +24,28 @@ import {
|
||||||
Workflow,
|
Workflow,
|
||||||
WorkflowActivateMode,
|
WorkflowActivateMode,
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
|
LoggerProxy as Logger,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-cycle
|
||||||
import {
|
import {
|
||||||
LoggerProxy as Logger,
|
Db,
|
||||||
} from 'n8n-workflow';
|
IActivationError,
|
||||||
|
IResponseCallbackData,
|
||||||
|
IWebhookDb,
|
||||||
|
IWorkflowDb,
|
||||||
|
IWorkflowExecutionDataProcess,
|
||||||
|
NodeTypes,
|
||||||
|
ResponseHelper,
|
||||||
|
WebhookHelpers,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
WorkflowCredentials,
|
||||||
|
WorkflowExecuteAdditionalData,
|
||||||
|
WorkflowHelpers,
|
||||||
|
WorkflowRunner,
|
||||||
|
} from '.';
|
||||||
|
|
||||||
const WEBHOOK_PROD_UNREGISTERED_HINT = `The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)`;
|
const WEBHOOK_PROD_UNREGISTERED_HINT = `The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)`;
|
||||||
|
|
||||||
|
@ -48,14 +56,16 @@ export class ActiveWorkflowRunner {
|
||||||
[key: string]: IActivationError;
|
[key: string]: IActivationError;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
async init() {
|
async init() {
|
||||||
|
|
||||||
// Get the active workflows from database
|
// Get the active workflows from database
|
||||||
|
|
||||||
// NOTE
|
// NOTE
|
||||||
// Here I guess we can have a flag on the workflow table like hasTrigger
|
// Here I guess we can have a flag on the workflow table like hasTrigger
|
||||||
// so intead of pulling all the active wehhooks just pull the actives that have a trigger
|
// so intead of pulling all the active wehhooks just pull the actives that have a trigger
|
||||||
const workflowsData: IWorkflowDb[] = await Db.collections.Workflow!.find({ active: true }) as IWorkflowDb[];
|
const workflowsData: IWorkflowDb[] = (await Db.collections.Workflow!.find({
|
||||||
|
active: true,
|
||||||
|
})) as IWorkflowDb[];
|
||||||
|
|
||||||
// Clear up active workflow table
|
// Clear up active workflow table
|
||||||
await Db.collections.Webhook?.clear();
|
await Db.collections.Webhook?.clear();
|
||||||
|
@ -69,21 +79,32 @@ export class ActiveWorkflowRunner {
|
||||||
|
|
||||||
for (const workflowData of workflowsData) {
|
for (const workflowData of workflowsData) {
|
||||||
console.log(` - ${workflowData.name}`);
|
console.log(` - ${workflowData.name}`);
|
||||||
Logger.debug(`Initializing active workflow "${workflowData.name}" (startup)`, { workflowName: workflowData.name, workflowId: workflowData.id });
|
Logger.debug(`Initializing active workflow "${workflowData.name}" (startup)`, {
|
||||||
|
workflowName: workflowData.name,
|
||||||
|
workflowId: workflowData.id,
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
await this.add(workflowData.id.toString(), 'init', workflowData);
|
await this.add(workflowData.id.toString(), 'init', workflowData);
|
||||||
Logger.verbose(`Successfully started workflow "${workflowData.name}"`, { workflowName: workflowData.name, workflowId: workflowData.id });
|
Logger.verbose(`Successfully started workflow "${workflowData.name}"`, {
|
||||||
|
workflowName: workflowData.name,
|
||||||
|
workflowId: workflowData.id,
|
||||||
|
});
|
||||||
console.log(` => Started`);
|
console.log(` => Started`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(` => ERROR: Workflow could not be activated:`);
|
console.log(` => ERROR: Workflow could not be activated:`);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
console.log(` ${error.message}`);
|
console.log(` ${error.message}`);
|
||||||
Logger.error(`Unable to initialize workflow "${workflowData.name}" (startup)`, { workflowName: workflowData.name, workflowId: workflowData.id });
|
Logger.error(`Unable to initialize workflow "${workflowData.name}" (startup)`, {
|
||||||
|
workflowName: workflowData.name,
|
||||||
|
workflowId: workflowData.id,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Logger.verbose('Finished initializing active workflows (startup)');
|
Logger.verbose('Finished initializing active workflows (startup)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
async initWebhooks() {
|
async initWebhooks() {
|
||||||
this.activeWorkflows = new ActiveWorkflows();
|
this.activeWorkflows = new ActiveWorkflows();
|
||||||
}
|
}
|
||||||
|
@ -104,7 +125,10 @@ export class ActiveWorkflowRunner {
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeWorkflows = await this.getActiveWorkflows();
|
const activeWorkflows = await this.getActiveWorkflows();
|
||||||
activeWorkflowId.push.apply(activeWorkflowId, activeWorkflows.map(workflow => workflow.id));
|
activeWorkflowId.push.apply(
|
||||||
|
activeWorkflowId,
|
||||||
|
activeWorkflows.map((workflow) => workflow.id),
|
||||||
|
);
|
||||||
|
|
||||||
const removePromises = [];
|
const removePromises = [];
|
||||||
for (const workflowId of activeWorkflowId) {
|
for (const workflowId of activeWorkflowId) {
|
||||||
|
@ -112,7 +136,6 @@ export class ActiveWorkflowRunner {
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(removePromises);
|
await Promise.all(removePromises);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -125,10 +148,19 @@ export class ActiveWorkflowRunner {
|
||||||
* @returns {Promise<object>}
|
* @returns {Promise<object>}
|
||||||
* @memberof ActiveWorkflowRunner
|
* @memberof ActiveWorkflowRunner
|
||||||
*/
|
*/
|
||||||
async executeWebhook(httpMethod: WebhookHttpMethod, path: string, req: express.Request, res: express.Response): Promise<IResponseCallbackData> {
|
async executeWebhook(
|
||||||
|
httpMethod: WebhookHttpMethod,
|
||||||
|
path: string,
|
||||||
|
req: express.Request,
|
||||||
|
res: express.Response,
|
||||||
|
): Promise<IResponseCallbackData> {
|
||||||
Logger.debug(`Received webhoook "${httpMethod}" for path "${path}"`);
|
Logger.debug(`Received webhoook "${httpMethod}" for path "${path}"`);
|
||||||
if (this.activeWorkflows === null) {
|
if (this.activeWorkflows === null) {
|
||||||
throw new ResponseHelper.ResponseError('The "activeWorkflows" instance did not get initialized yet.', 404, 404);
|
throw new ResponseHelper.ResponseError(
|
||||||
|
'The "activeWorkflows" instance did not get initialized yet.',
|
||||||
|
404,
|
||||||
|
404,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset request parameters
|
// Reset request parameters
|
||||||
|
@ -139,7 +171,10 @@ export class ActiveWorkflowRunner {
|
||||||
path = path.slice(0, -1);
|
path = path.slice(0, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
let webhook = await Db.collections.Webhook?.findOne({ webhookPath: path, method: httpMethod }) as IWebhookDb;
|
let webhook = (await Db.collections.Webhook?.findOne({
|
||||||
|
webhookPath: path,
|
||||||
|
method: httpMethod,
|
||||||
|
})) as IWebhookDb;
|
||||||
let webhookId: string | undefined;
|
let webhookId: string | undefined;
|
||||||
|
|
||||||
// check if path is dynamic
|
// check if path is dynamic
|
||||||
|
@ -147,19 +182,30 @@ export class ActiveWorkflowRunner {
|
||||||
// check if a dynamic webhook path exists
|
// check if a dynamic webhook path exists
|
||||||
const pathElements = path.split('/');
|
const pathElements = path.split('/');
|
||||||
webhookId = pathElements.shift();
|
webhookId = pathElements.shift();
|
||||||
const dynamicWebhooks = await Db.collections.Webhook?.find({ webhookId, method: httpMethod, pathLength: pathElements.length });
|
const dynamicWebhooks = await Db.collections.Webhook?.find({
|
||||||
|
webhookId,
|
||||||
|
method: httpMethod,
|
||||||
|
pathLength: pathElements.length,
|
||||||
|
});
|
||||||
if (dynamicWebhooks === undefined || dynamicWebhooks.length === 0) {
|
if (dynamicWebhooks === undefined || dynamicWebhooks.length === 0) {
|
||||||
// The requested webhook is not registered
|
// The requested webhook is not registered
|
||||||
throw new ResponseHelper.ResponseError(`The requested webhook "${httpMethod} ${path}" is not registered.`, 404, 404, WEBHOOK_PROD_UNREGISTERED_HINT);
|
throw new ResponseHelper.ResponseError(
|
||||||
|
`The requested webhook "${httpMethod} ${path}" is not registered.`,
|
||||||
|
404,
|
||||||
|
404,
|
||||||
|
WEBHOOK_PROD_UNREGISTERED_HINT,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let maxMatches = 0;
|
let maxMatches = 0;
|
||||||
const pathElementsSet = new Set(pathElements);
|
const pathElementsSet = new Set(pathElements);
|
||||||
// check if static elements match in path
|
// check if static elements match in path
|
||||||
// if more results have been returned choose the one with the most static-route matches
|
// if more results have been returned choose the one with the most static-route matches
|
||||||
dynamicWebhooks.forEach(dynamicWebhook => {
|
dynamicWebhooks.forEach((dynamicWebhook) => {
|
||||||
const staticElements = dynamicWebhook.webhookPath.split('/').filter(ele => !ele.startsWith(':'));
|
const staticElements = dynamicWebhook.webhookPath
|
||||||
const allStaticExist = staticElements.every(staticEle => pathElementsSet.has(staticEle));
|
.split('/')
|
||||||
|
.filter((ele) => !ele.startsWith(':'));
|
||||||
|
const allStaticExist = staticElements.every((staticEle) => pathElementsSet.has(staticEle));
|
||||||
|
|
||||||
if (allStaticExist && staticElements.length > maxMatches) {
|
if (allStaticExist && staticElements.length > maxMatches) {
|
||||||
maxMatches = staticElements.length;
|
maxMatches = staticElements.length;
|
||||||
|
@ -171,12 +217,20 @@ export class ActiveWorkflowRunner {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (webhook === undefined) {
|
if (webhook === undefined) {
|
||||||
throw new ResponseHelper.ResponseError(`The requested webhook "${httpMethod} ${path}" is not registered.`, 404, 404, WEBHOOK_PROD_UNREGISTERED_HINT);
|
throw new ResponseHelper.ResponseError(
|
||||||
|
`The requested webhook "${httpMethod} ${path}" is not registered.`,
|
||||||
|
404,
|
||||||
|
404,
|
||||||
|
WEBHOOK_PROD_UNREGISTERED_HINT,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
path = webhook!.webhookPath;
|
// @ts-ignore
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
path = webhook.webhookPath;
|
||||||
// extracting params from path
|
// extracting params from path
|
||||||
webhook!.webhookPath.split('/').forEach((ele, index) => {
|
// @ts-ignore
|
||||||
|
webhook.webhookPath.split('/').forEach((ele, index) => {
|
||||||
if (ele.startsWith(':')) {
|
if (ele.startsWith(':')) {
|
||||||
// write params to req.params
|
// write params to req.params
|
||||||
req.params[ele.slice(1)] = pathElements[index];
|
req.params[ele.slice(1)] = pathElements[index];
|
||||||
|
@ -186,18 +240,33 @@ export class ActiveWorkflowRunner {
|
||||||
|
|
||||||
const workflowData = await Db.collections.Workflow!.findOne(webhook.workflowId);
|
const workflowData = await Db.collections.Workflow!.findOne(webhook.workflowId);
|
||||||
if (workflowData === undefined) {
|
if (workflowData === undefined) {
|
||||||
throw new ResponseHelper.ResponseError(`Could not find workflow with id "${webhook.workflowId}"`, 404, 404);
|
throw new ResponseHelper.ResponseError(
|
||||||
|
`Could not find workflow with id "${webhook.workflowId}"`,
|
||||||
|
404,
|
||||||
|
404,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodeTypes = NodeTypes();
|
const nodeTypes = NodeTypes();
|
||||||
const workflow = new Workflow({ id: webhook.workflowId.toString(), name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, nodeTypes, staticData: workflowData.staticData, settings: workflowData.settings });
|
const workflow = new Workflow({
|
||||||
|
id: webhook.workflowId.toString(),
|
||||||
|
name: workflowData.name,
|
||||||
|
nodes: workflowData.nodes,
|
||||||
|
connections: workflowData.connections,
|
||||||
|
active: workflowData.active,
|
||||||
|
nodeTypes,
|
||||||
|
staticData: workflowData.staticData,
|
||||||
|
settings: workflowData.settings,
|
||||||
|
});
|
||||||
|
|
||||||
const credentials = await WorkflowCredentials([workflow.getNode(webhook.node as string) as INode]);
|
const additionalData = await WorkflowExecuteAdditionalData.getBase();
|
||||||
|
|
||||||
const additionalData = await WorkflowExecuteAdditionalData.getBase(credentials);
|
const webhookData = NodeHelpers.getNodeWebhooks(
|
||||||
|
workflow,
|
||||||
const webhookData = NodeHelpers.getNodeWebhooks(workflow, workflow.getNode(webhook.node as string) as INode, additionalData).filter((webhook) => {
|
workflow.getNode(webhook.node) as INode,
|
||||||
return (webhook.httpMethod === httpMethod && webhook.path === path);
|
additionalData,
|
||||||
|
).filter((webhook) => {
|
||||||
|
return webhook.httpMethod === httpMethod && webhook.path === path;
|
||||||
})[0];
|
})[0];
|
||||||
|
|
||||||
// Get the node which has the webhook defined to know where to start from and to
|
// Get the node which has the webhook defined to know where to start from and to
|
||||||
|
@ -210,13 +279,26 @@ export class ActiveWorkflowRunner {
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const executionMode = 'webhook';
|
const executionMode = 'webhook';
|
||||||
//@ts-ignore
|
// @ts-ignore
|
||||||
WebhookHelpers.executeWebhook(workflow, webhookData, workflowData, workflowStartNode, executionMode, undefined, req, res, (error: Error | null, data: object) => {
|
WebhookHelpers.executeWebhook(
|
||||||
if (error !== null) {
|
workflow,
|
||||||
return reject(error);
|
webhookData,
|
||||||
}
|
workflowData,
|
||||||
resolve(data);
|
workflowStartNode,
|
||||||
});
|
executionMode,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
|
(error: Error | null, data: object) => {
|
||||||
|
if (error !== null) {
|
||||||
|
return reject(error);
|
||||||
|
}
|
||||||
|
resolve(data);
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,10 +310,10 @@ export class ActiveWorkflowRunner {
|
||||||
* @memberof ActiveWorkflowRunner
|
* @memberof ActiveWorkflowRunner
|
||||||
*/
|
*/
|
||||||
async getWebhookMethods(path: string): Promise<string[]> {
|
async getWebhookMethods(path: string): Promise<string[]> {
|
||||||
const webhooks = await Db.collections.Webhook?.find({ webhookPath: path }) as IWebhookDb[];
|
const webhooks = (await Db.collections.Webhook?.find({ webhookPath: path })) as IWebhookDb[];
|
||||||
|
|
||||||
// Gather all request methods in string array
|
// Gather all request methods in string array
|
||||||
const webhookMethods: string[] = webhooks.map(webhook => webhook.method);
|
const webhookMethods: string[] = webhooks.map((webhook) => webhook.method);
|
||||||
return webhookMethods;
|
return webhookMethods;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,11 +324,15 @@ export class ActiveWorkflowRunner {
|
||||||
* @memberof ActiveWorkflowRunner
|
* @memberof ActiveWorkflowRunner
|
||||||
*/
|
*/
|
||||||
async getActiveWorkflows(): Promise<IWorkflowDb[]> {
|
async getActiveWorkflows(): Promise<IWorkflowDb[]> {
|
||||||
const activeWorkflows = await Db.collections.Workflow?.find({ where: { active: true }, select: ['id'] }) as IWorkflowDb[];
|
const activeWorkflows = (await Db.collections.Workflow?.find({
|
||||||
return activeWorkflows.filter(workflow => this.activationErrors[workflow.id.toString()] === undefined);
|
where: { active: true },
|
||||||
|
select: ['id'],
|
||||||
|
})) as IWorkflowDb[];
|
||||||
|
return activeWorkflows.filter(
|
||||||
|
(workflow) => this.activationErrors[workflow.id.toString()] === undefined,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns if the workflow is active
|
* Returns if the workflow is active
|
||||||
*
|
*
|
||||||
|
@ -255,8 +341,8 @@ export class ActiveWorkflowRunner {
|
||||||
* @memberof ActiveWorkflowRunner
|
* @memberof ActiveWorkflowRunner
|
||||||
*/
|
*/
|
||||||
async isActive(id: string): Promise<boolean> {
|
async isActive(id: string): Promise<boolean> {
|
||||||
const workflow = await Db.collections.Workflow?.findOne({ id: Number(id) }) as IWorkflowDb;
|
const workflow = (await Db.collections.Workflow?.findOne({ id: Number(id) })) as IWorkflowDb;
|
||||||
return workflow?.active as boolean;
|
return workflow?.active;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -283,12 +369,16 @@ export class ActiveWorkflowRunner {
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
* @memberof ActiveWorkflowRunner
|
* @memberof ActiveWorkflowRunner
|
||||||
*/
|
*/
|
||||||
async addWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): Promise<void> {
|
async addWorkflowWebhooks(
|
||||||
const webhooks = WebhookHelpers.getWorkflowWebhooks(workflow, additionalData);
|
workflow: Workflow,
|
||||||
|
additionalData: IWorkflowExecuteAdditionalDataWorkflow,
|
||||||
|
mode: WorkflowExecuteMode,
|
||||||
|
activation: WorkflowActivateMode,
|
||||||
|
): Promise<void> {
|
||||||
|
const webhooks = WebhookHelpers.getWorkflowWebhooks(workflow, additionalData, undefined, true);
|
||||||
let path = '' as string | undefined;
|
let path = '' as string | undefined;
|
||||||
|
|
||||||
for (const webhookData of webhooks) {
|
for (const webhookData of webhooks) {
|
||||||
|
|
||||||
const node = workflow.getNode(webhookData.node) as INode;
|
const node = workflow.getNode(webhookData.node) as INode;
|
||||||
node.name = webhookData.node;
|
node.name = webhookData.node;
|
||||||
|
|
||||||
|
@ -314,18 +404,35 @@ export class ActiveWorkflowRunner {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await Db.collections.Webhook?.insert(webhook);
|
await Db.collections.Webhook?.insert(webhook);
|
||||||
const webhookExists = await workflow.runWebhookMethod('checkExists', webhookData, NodeExecuteFunctions, mode, activation, false);
|
const webhookExists = await workflow.runWebhookMethod(
|
||||||
|
'checkExists',
|
||||||
|
webhookData,
|
||||||
|
NodeExecuteFunctions,
|
||||||
|
mode,
|
||||||
|
activation,
|
||||||
|
false,
|
||||||
|
);
|
||||||
if (webhookExists !== true) {
|
if (webhookExists !== true) {
|
||||||
// If webhook does not exist yet create it
|
// If webhook does not exist yet create it
|
||||||
await workflow.runWebhookMethod('create', webhookData, NodeExecuteFunctions, mode, activation, false);
|
await workflow.runWebhookMethod(
|
||||||
|
'create',
|
||||||
|
webhookData,
|
||||||
|
NodeExecuteFunctions,
|
||||||
|
mode,
|
||||||
|
activation,
|
||||||
|
false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
try {
|
try {
|
||||||
await this.removeWorkflowWebhooks(workflow.id as string);
|
await this.removeWorkflowWebhooks(workflow.id as string);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Could not remove webhooks of workflow "${workflow.id}" because of error: "${error.message}"`);
|
console.error(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
|
`Could not remove webhooks of workflow "${workflow.id}" because of error: "${error.message}"`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let errorMessage = '';
|
let errorMessage = '';
|
||||||
|
@ -339,6 +446,7 @@ export class ActiveWorkflowRunner {
|
||||||
// it's a error runnig the webhook methods (checkExists, create)
|
// it's a error runnig the webhook methods (checkExists, create)
|
||||||
errorMessage = error.detail;
|
errorMessage = error.detail;
|
||||||
} else {
|
} else {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
errorMessage = error.message;
|
errorMessage = error.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -349,7 +457,6 @@ export class ActiveWorkflowRunner {
|
||||||
await WorkflowHelpers.saveStaticData(workflow);
|
await WorkflowHelpers.saveStaticData(workflow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove all the webhooks of the workflow
|
* Remove all the webhooks of the workflow
|
||||||
*
|
*
|
||||||
|
@ -364,17 +471,32 @@ export class ActiveWorkflowRunner {
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodeTypes = NodeTypes();
|
const nodeTypes = NodeTypes();
|
||||||
const workflow = new Workflow({ id: workflowId, name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, nodeTypes, staticData: workflowData.staticData, settings: workflowData.settings });
|
const workflow = new Workflow({
|
||||||
|
id: workflowId,
|
||||||
|
name: workflowData.name,
|
||||||
|
nodes: workflowData.nodes,
|
||||||
|
connections: workflowData.connections,
|
||||||
|
active: workflowData.active,
|
||||||
|
nodeTypes,
|
||||||
|
staticData: workflowData.staticData,
|
||||||
|
settings: workflowData.settings,
|
||||||
|
});
|
||||||
|
|
||||||
const mode = 'internal';
|
const mode = 'internal';
|
||||||
|
|
||||||
const credentials = await WorkflowCredentials(workflowData.nodes);
|
const additionalData = await WorkflowExecuteAdditionalData.getBase();
|
||||||
const additionalData = await WorkflowExecuteAdditionalData.getBase(credentials);
|
|
||||||
|
|
||||||
const webhooks = WebhookHelpers.getWorkflowWebhooks(workflow, additionalData);
|
const webhooks = WebhookHelpers.getWorkflowWebhooks(workflow, additionalData, undefined, true);
|
||||||
|
|
||||||
for (const webhookData of webhooks) {
|
for (const webhookData of webhooks) {
|
||||||
await workflow.runWebhookMethod('delete', webhookData, NodeExecuteFunctions, mode, 'update', false);
|
await workflow.runWebhookMethod(
|
||||||
|
'delete',
|
||||||
|
webhookData,
|
||||||
|
NodeExecuteFunctions,
|
||||||
|
mode,
|
||||||
|
'update',
|
||||||
|
false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await WorkflowHelpers.saveStaticData(workflow);
|
await WorkflowHelpers.saveStaticData(workflow);
|
||||||
|
@ -397,7 +519,14 @@ export class ActiveWorkflowRunner {
|
||||||
* @returns
|
* @returns
|
||||||
* @memberof ActiveWorkflowRunner
|
* @memberof ActiveWorkflowRunner
|
||||||
*/
|
*/
|
||||||
runWorkflow(workflowData: IWorkflowDb, node: INode, data: INodeExecutionData[][], additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode) {
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
|
async runWorkflow(
|
||||||
|
workflowData: IWorkflowDb,
|
||||||
|
node: INode,
|
||||||
|
data: INodeExecutionData[][],
|
||||||
|
additionalData: IWorkflowExecuteAdditionalDataWorkflow,
|
||||||
|
mode: WorkflowExecuteMode,
|
||||||
|
) {
|
||||||
const nodeExecutionStack: IExecuteData[] = [
|
const nodeExecutionStack: IExecuteData[] = [
|
||||||
{
|
{
|
||||||
node,
|
node,
|
||||||
|
@ -421,7 +550,6 @@ export class ActiveWorkflowRunner {
|
||||||
|
|
||||||
// Start the workflow
|
// Start the workflow
|
||||||
const runData: IWorkflowExecutionDataProcess = {
|
const runData: IWorkflowExecutionDataProcess = {
|
||||||
credentials: additionalData.credentials,
|
|
||||||
executionMode: mode,
|
executionMode: mode,
|
||||||
executionData,
|
executionData,
|
||||||
workflowData,
|
workflowData,
|
||||||
|
@ -431,7 +559,6 @@ export class ActiveWorkflowRunner {
|
||||||
return workflowRunner.run(runData, true);
|
return workflowRunner.run(runData, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return poll function which gets the global functions from n8n-core
|
* Return poll function which gets the global functions from n8n-core
|
||||||
* and overwrites the __emit to be able to start it in subprocess
|
* and overwrites the __emit to be able to start it in subprocess
|
||||||
|
@ -442,18 +569,30 @@ export class ActiveWorkflowRunner {
|
||||||
* @returns {IGetExecutePollFunctions}
|
* @returns {IGetExecutePollFunctions}
|
||||||
* @memberof ActiveWorkflowRunner
|
* @memberof ActiveWorkflowRunner
|
||||||
*/
|
*/
|
||||||
getExecutePollFunctions(workflowData: IWorkflowDb, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): IGetExecutePollFunctions {
|
getExecutePollFunctions(
|
||||||
return ((workflow: Workflow, node: INode) => {
|
workflowData: IWorkflowDb,
|
||||||
const returnFunctions = NodeExecuteFunctions.getExecutePollFunctions(workflow, node, additionalData, mode, activation);
|
additionalData: IWorkflowExecuteAdditionalDataWorkflow,
|
||||||
|
mode: WorkflowExecuteMode,
|
||||||
|
activation: WorkflowActivateMode,
|
||||||
|
): IGetExecutePollFunctions {
|
||||||
|
return (workflow: Workflow, node: INode) => {
|
||||||
|
const returnFunctions = NodeExecuteFunctions.getExecutePollFunctions(
|
||||||
|
workflow,
|
||||||
|
node,
|
||||||
|
additionalData,
|
||||||
|
mode,
|
||||||
|
activation,
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
returnFunctions.__emit = (data: INodeExecutionData[][]): void => {
|
returnFunctions.__emit = (data: INodeExecutionData[][]): void => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
Logger.debug(`Received event to trigger execution for workflow "${workflow.name}"`);
|
Logger.debug(`Received event to trigger execution for workflow "${workflow.name}"`);
|
||||||
this.runWorkflow(workflowData, node, data, additionalData, mode);
|
this.runWorkflow(workflowData, node, data, additionalData, mode);
|
||||||
};
|
};
|
||||||
return returnFunctions;
|
return returnFunctions;
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return trigger function which gets the global functions from n8n-core
|
* Return trigger function which gets the global functions from n8n-core
|
||||||
* and overwrites the emit to be able to start it in subprocess
|
* and overwrites the emit to be able to start it in subprocess
|
||||||
|
@ -464,16 +603,31 @@ export class ActiveWorkflowRunner {
|
||||||
* @returns {IGetExecuteTriggerFunctions}
|
* @returns {IGetExecuteTriggerFunctions}
|
||||||
* @memberof ActiveWorkflowRunner
|
* @memberof ActiveWorkflowRunner
|
||||||
*/
|
*/
|
||||||
getExecuteTriggerFunctions(workflowData: IWorkflowDb, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): IGetExecuteTriggerFunctions {
|
getExecuteTriggerFunctions(
|
||||||
return ((workflow: Workflow, node: INode) => {
|
workflowData: IWorkflowDb,
|
||||||
const returnFunctions = NodeExecuteFunctions.getExecuteTriggerFunctions(workflow, node, additionalData, mode, activation);
|
additionalData: IWorkflowExecuteAdditionalDataWorkflow,
|
||||||
|
mode: WorkflowExecuteMode,
|
||||||
|
activation: WorkflowActivateMode,
|
||||||
|
): IGetExecuteTriggerFunctions {
|
||||||
|
return (workflow: Workflow, node: INode) => {
|
||||||
|
const returnFunctions = NodeExecuteFunctions.getExecuteTriggerFunctions(
|
||||||
|
workflow,
|
||||||
|
node,
|
||||||
|
additionalData,
|
||||||
|
mode,
|
||||||
|
activation,
|
||||||
|
);
|
||||||
returnFunctions.emit = (data: INodeExecutionData[][]): void => {
|
returnFunctions.emit = (data: INodeExecutionData[][]): void => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
Logger.debug(`Received trigger for workflow "${workflow.name}"`);
|
Logger.debug(`Received trigger for workflow "${workflow.name}"`);
|
||||||
WorkflowHelpers.saveStaticData(workflow);
|
WorkflowHelpers.saveStaticData(workflow);
|
||||||
this.runWorkflow(workflowData, node, data, additionalData, mode).catch((err) => console.error(err));
|
// eslint-disable-next-line id-denylist
|
||||||
|
this.runWorkflow(workflowData, node, data, additionalData, mode).catch((err) =>
|
||||||
|
console.error(err),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
return returnFunctions;
|
return returnFunctions;
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -484,7 +638,11 @@ export class ActiveWorkflowRunner {
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
* @memberof ActiveWorkflowRunner
|
* @memberof ActiveWorkflowRunner
|
||||||
*/
|
*/
|
||||||
async add(workflowId: string, activation: WorkflowActivateMode, workflowData?: IWorkflowDb): Promise<void> {
|
async add(
|
||||||
|
workflowId: string,
|
||||||
|
activation: WorkflowActivateMode,
|
||||||
|
workflowData?: IWorkflowDb,
|
||||||
|
): Promise<void> {
|
||||||
if (this.activeWorkflows === null) {
|
if (this.activeWorkflows === null) {
|
||||||
throw new Error(`The "activeWorkflows" instance did not get initialized yet.`);
|
throw new Error(`The "activeWorkflows" instance did not get initialized yet.`);
|
||||||
}
|
}
|
||||||
|
@ -492,34 +650,69 @@ export class ActiveWorkflowRunner {
|
||||||
let workflowInstance: Workflow;
|
let workflowInstance: Workflow;
|
||||||
try {
|
try {
|
||||||
if (workflowData === undefined) {
|
if (workflowData === undefined) {
|
||||||
workflowData = await Db.collections.Workflow!.findOne(workflowId) as IWorkflowDb;
|
workflowData = (await Db.collections.Workflow!.findOne(workflowId)) as IWorkflowDb;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!workflowData) {
|
if (!workflowData) {
|
||||||
throw new Error(`Could not find workflow with id "${workflowId}".`);
|
throw new Error(`Could not find workflow with id "${workflowId}".`);
|
||||||
}
|
}
|
||||||
const nodeTypes = NodeTypes();
|
const nodeTypes = NodeTypes();
|
||||||
workflowInstance = new Workflow({ id: workflowId, name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, nodeTypes, staticData: workflowData.staticData, settings: workflowData.settings });
|
workflowInstance = new Workflow({
|
||||||
|
id: workflowId,
|
||||||
|
name: workflowData.name,
|
||||||
|
nodes: workflowData.nodes,
|
||||||
|
connections: workflowData.connections,
|
||||||
|
active: workflowData.active,
|
||||||
|
nodeTypes,
|
||||||
|
staticData: workflowData.staticData,
|
||||||
|
settings: workflowData.settings,
|
||||||
|
});
|
||||||
|
|
||||||
const canBeActivated = workflowInstance.checkIfWorkflowCanBeActivated(['n8n-nodes-base.start']);
|
const canBeActivated = workflowInstance.checkIfWorkflowCanBeActivated([
|
||||||
if (canBeActivated === false) {
|
'n8n-nodes-base.start',
|
||||||
|
]);
|
||||||
|
if (!canBeActivated) {
|
||||||
Logger.error(`Unable to activate workflow "${workflowData.name}"`);
|
Logger.error(`Unable to activate workflow "${workflowData.name}"`);
|
||||||
throw new Error(`The workflow can not be activated because it does not contain any nodes which could start the workflow. Only workflows which have trigger or webhook nodes can be activated.`);
|
throw new Error(
|
||||||
|
`The workflow can not be activated because it does not contain any nodes which could start the workflow. Only workflows which have trigger or webhook nodes can be activated.`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mode = 'trigger';
|
const mode = 'trigger';
|
||||||
const credentials = await WorkflowCredentials(workflowData.nodes);
|
const additionalData = await WorkflowExecuteAdditionalData.getBase();
|
||||||
const additionalData = await WorkflowExecuteAdditionalData.getBase(credentials);
|
const getTriggerFunctions = this.getExecuteTriggerFunctions(
|
||||||
const getTriggerFunctions = this.getExecuteTriggerFunctions(workflowData, additionalData, mode, activation);
|
workflowData,
|
||||||
const getPollFunctions = this.getExecutePollFunctions(workflowData, additionalData, mode, activation);
|
additionalData,
|
||||||
|
mode,
|
||||||
|
activation,
|
||||||
|
);
|
||||||
|
const getPollFunctions = this.getExecutePollFunctions(
|
||||||
|
workflowData,
|
||||||
|
additionalData,
|
||||||
|
mode,
|
||||||
|
activation,
|
||||||
|
);
|
||||||
|
|
||||||
// Add the workflows which have webhooks defined
|
// Add the workflows which have webhooks defined
|
||||||
await this.addWorkflowWebhooks(workflowInstance, additionalData, mode, activation);
|
await this.addWorkflowWebhooks(workflowInstance, additionalData, mode, activation);
|
||||||
|
|
||||||
if (workflowInstance.getTriggerNodes().length !== 0
|
if (
|
||||||
|| workflowInstance.getPollNodes().length !== 0) {
|
workflowInstance.getTriggerNodes().length !== 0 ||
|
||||||
await this.activeWorkflows.add(workflowId, workflowInstance, additionalData, mode, activation, getTriggerFunctions, getPollFunctions);
|
workflowInstance.getPollNodes().length !== 0
|
||||||
Logger.verbose(`Successfully activated workflow "${workflowData.name}"`, { workflowId, workflowName: workflowData.name });
|
) {
|
||||||
|
await this.activeWorkflows.add(
|
||||||
|
workflowId,
|
||||||
|
workflowInstance,
|
||||||
|
additionalData,
|
||||||
|
mode,
|
||||||
|
activation,
|
||||||
|
getTriggerFunctions,
|
||||||
|
getPollFunctions,
|
||||||
|
);
|
||||||
|
Logger.verbose(`Successfully activated workflow "${workflowData.name}"`, {
|
||||||
|
workflowId,
|
||||||
|
workflowName: workflowData.name,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.activationErrors[workflowId] !== undefined) {
|
if (this.activationErrors[workflowId] !== undefined) {
|
||||||
|
@ -553,13 +746,15 @@ export class ActiveWorkflowRunner {
|
||||||
* @memberof ActiveWorkflowRunner
|
* @memberof ActiveWorkflowRunner
|
||||||
*/
|
*/
|
||||||
async remove(workflowId: string): Promise<void> {
|
async remove(workflowId: string): Promise<void> {
|
||||||
|
|
||||||
if (this.activeWorkflows !== null) {
|
if (this.activeWorkflows !== null) {
|
||||||
// Remove all the webhooks of the workflow
|
// Remove all the webhooks of the workflow
|
||||||
try {
|
try {
|
||||||
await this.removeWorkflowWebhooks(workflowId);
|
await this.removeWorkflowWebhooks(workflowId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Could not remove webhooks of workflow "${workflowId}" because of error: "${error.message}"`);
|
console.error(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
|
`Could not remove webhooks of workflow "${workflowId}" because of error: "${error.message}"`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.activationErrors[workflowId] !== undefined) {
|
if (this.activationErrors[workflowId] !== undefined) {
|
||||||
|
@ -581,8 +776,6 @@ export class ActiveWorkflowRunner {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let workflowRunnerInstance: ActiveWorkflowRunner | undefined;
|
let workflowRunnerInstance: ActiveWorkflowRunner | undefined;
|
||||||
|
|
||||||
export function getInstance(): ActiveWorkflowRunner {
|
export function getInstance(): ActiveWorkflowRunner {
|
||||||
|
|
|
@ -1,32 +1,30 @@
|
||||||
import {
|
import { ICredentialType, ICredentialTypes as ICredentialTypesInterface } from 'n8n-workflow';
|
||||||
ICredentialType,
|
|
||||||
ICredentialTypes as ICredentialTypesInterface,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
import {
|
// eslint-disable-next-line import/no-cycle
|
||||||
CredentialsOverwrites,
|
import { CredentialsOverwrites, ICredentialsTypeData } from '.';
|
||||||
ICredentialsTypeData,
|
|
||||||
} from './';
|
|
||||||
|
|
||||||
class CredentialTypesClass implements ICredentialTypesInterface {
|
class CredentialTypesClass implements ICredentialTypesInterface {
|
||||||
|
|
||||||
credentialTypes: ICredentialsTypeData = {};
|
credentialTypes: ICredentialsTypeData = {};
|
||||||
|
|
||||||
|
|
||||||
async init(credentialTypes: ICredentialsTypeData): Promise<void> {
|
async init(credentialTypes: ICredentialsTypeData): Promise<void> {
|
||||||
this.credentialTypes = credentialTypes;
|
this.credentialTypes = credentialTypes;
|
||||||
|
|
||||||
// Load the credentials overwrites if any exist
|
// Load the credentials overwrites if any exist
|
||||||
const credentialsOverwrites = CredentialsOverwrites().getAll();
|
const credentialsOverwrites = CredentialsOverwrites().getAll();
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const credentialType of Object.keys(credentialsOverwrites)) {
|
for (const credentialType of Object.keys(credentialsOverwrites)) {
|
||||||
if (credentialTypes[credentialType] === undefined) {
|
if (credentialTypes[credentialType] === undefined) {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add which properties got overwritten that the Editor-UI knows
|
// Add which properties got overwritten that the Editor-UI knows
|
||||||
// which properties it should hide
|
// which properties it should hide
|
||||||
credentialTypes[credentialType].__overwrittenProperties = Object.keys(credentialsOverwrites[credentialType]);
|
// eslint-disable-next-line no-underscore-dangle, no-param-reassign
|
||||||
|
credentialTypes[credentialType].__overwrittenProperties = Object.keys(
|
||||||
|
credentialsOverwrites[credentialType],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,10 +37,9 @@ class CredentialTypesClass implements ICredentialTypesInterface {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let credentialTypesInstance: CredentialTypesClass | undefined;
|
let credentialTypesInstance: CredentialTypesClass | undefined;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
export function CredentialTypes(): CredentialTypesClass {
|
export function CredentialTypes(): CredentialTypesClass {
|
||||||
if (credentialTypesInstance === undefined) {
|
if (credentialTypesInstance === undefined) {
|
||||||
credentialTypesInstance = new CredentialTypesClass();
|
credentialTypesInstance = new CredentialTypesClass();
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
import {
|
import { Credentials } from 'n8n-core';
|
||||||
Credentials,
|
|
||||||
} from 'n8n-core';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ICredentialDataDecryptedObject,
|
ICredentialDataDecryptedObject,
|
||||||
|
@ -17,29 +15,24 @@ import {
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import {
|
// eslint-disable-next-line import/no-cycle
|
||||||
CredentialsOverwrites,
|
import { CredentialsOverwrites, CredentialTypes, Db, ICredentialsDb } from '.';
|
||||||
CredentialTypes,
|
|
||||||
Db,
|
|
||||||
ICredentialsDb,
|
|
||||||
} from './';
|
|
||||||
|
|
||||||
|
|
||||||
const mockNodeTypes: INodeTypes = {
|
const mockNodeTypes: INodeTypes = {
|
||||||
nodeTypes: {},
|
nodeTypes: {},
|
||||||
init: async (nodeTypes?: INodeTypeData): Promise<void> => { },
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
init: async (nodeTypes?: INodeTypeData): Promise<void> => {},
|
||||||
getAll: (): INodeType[] => {
|
getAll: (): INodeType[] => {
|
||||||
// Does not get used in Workflow so no need to return it
|
// Does not get used in Workflow so no need to return it
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
getByName: (nodeType: string): INodeType | undefined => {
|
getByName: (nodeType: string): INodeType | undefined => {
|
||||||
return undefined;
|
return undefined;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export class CredentialsHelper extends ICredentialsHelper {
|
export class CredentialsHelper extends ICredentialsHelper {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the credentials instance
|
* Returns the credentials instance
|
||||||
*
|
*
|
||||||
|
@ -48,19 +41,28 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||||
* @returns {Credentials}
|
* @returns {Credentials}
|
||||||
* @memberof CredentialsHelper
|
* @memberof CredentialsHelper
|
||||||
*/
|
*/
|
||||||
getCredentials(name: string, type: string): Credentials {
|
async getCredentials(name: string, type: string): Promise<Credentials> {
|
||||||
if (!this.workflowCredentials[type]) {
|
const credentialsDb = await Db.collections.Credentials?.find({ type });
|
||||||
|
|
||||||
|
if (credentialsDb === undefined || credentialsDb.length === 0) {
|
||||||
throw new Error(`No credentials of type "${type}" exist.`);
|
throw new Error(`No credentials of type "${type}" exist.`);
|
||||||
}
|
}
|
||||||
if (!this.workflowCredentials[type][name]) {
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
|
const credential = credentialsDb.find((credential) => credential.name === name);
|
||||||
|
|
||||||
|
if (credential === undefined) {
|
||||||
throw new Error(`No credentials with name "${name}" exist for type "${type}".`);
|
throw new Error(`No credentials with name "${name}" exist for type "${type}".`);
|
||||||
}
|
}
|
||||||
const credentialData = this.workflowCredentials[type][name];
|
|
||||||
|
|
||||||
return new Credentials(credentialData.name, credentialData.type, credentialData.nodesAccess, credentialData.data);
|
return new Credentials(
|
||||||
|
credential.name,
|
||||||
|
credential.type,
|
||||||
|
credential.nodesAccess,
|
||||||
|
credential.data,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all the properties of the credentials with the given name
|
* Returns all the properties of the credentials with the given name
|
||||||
*
|
*
|
||||||
|
@ -81,6 +83,7 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
const combineProperties = [] as INodeProperties[];
|
const combineProperties = [] as INodeProperties[];
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const credentialsTypeName of credentialTypeData.extends) {
|
for (const credentialsTypeName of credentialTypeData.extends) {
|
||||||
const mergeCredentialProperties = this.getCredentialsProperties(credentialsTypeName);
|
const mergeCredentialProperties = this.getCredentialsProperties(credentialsTypeName);
|
||||||
NodeHelpers.mergeNodeProperties(combineProperties, mergeCredentialProperties);
|
NodeHelpers.mergeNodeProperties(combineProperties, mergeCredentialProperties);
|
||||||
|
@ -92,7 +95,6 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||||
return combineProperties;
|
return combineProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the decrypted credential data with applied overwrites
|
* Returns the decrypted credential data with applied overwrites
|
||||||
*
|
*
|
||||||
|
@ -102,8 +104,14 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||||
* @returns {ICredentialDataDecryptedObject}
|
* @returns {ICredentialDataDecryptedObject}
|
||||||
* @memberof CredentialsHelper
|
* @memberof CredentialsHelper
|
||||||
*/
|
*/
|
||||||
getDecrypted(name: string, type: string, mode: WorkflowExecuteMode, raw?: boolean, expressionResolveValues?: ICredentialsExpressionResolveValues): ICredentialDataDecryptedObject {
|
async getDecrypted(
|
||||||
const credentials = this.getCredentials(name, type);
|
name: string,
|
||||||
|
type: string,
|
||||||
|
mode: WorkflowExecuteMode,
|
||||||
|
raw?: boolean,
|
||||||
|
expressionResolveValues?: ICredentialsExpressionResolveValues,
|
||||||
|
): Promise<ICredentialDataDecryptedObject> {
|
||||||
|
const credentials = await this.getCredentials(name, type);
|
||||||
|
|
||||||
const decryptedDataOriginal = credentials.getData(this.encryptionKey);
|
const decryptedDataOriginal = credentials.getData(this.encryptionKey);
|
||||||
|
|
||||||
|
@ -111,10 +119,14 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||||
return decryptedDataOriginal;
|
return decryptedDataOriginal;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.applyDefaultsAndOverwrites(decryptedDataOriginal, type, mode, expressionResolveValues);
|
return this.applyDefaultsAndOverwrites(
|
||||||
|
decryptedDataOriginal,
|
||||||
|
type,
|
||||||
|
mode,
|
||||||
|
expressionResolveValues,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies credential default data and overwrites
|
* Applies credential default data and overwrites
|
||||||
*
|
*
|
||||||
|
@ -123,11 +135,21 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||||
* @returns {ICredentialDataDecryptedObject}
|
* @returns {ICredentialDataDecryptedObject}
|
||||||
* @memberof CredentialsHelper
|
* @memberof CredentialsHelper
|
||||||
*/
|
*/
|
||||||
applyDefaultsAndOverwrites(decryptedDataOriginal: ICredentialDataDecryptedObject, type: string, mode: WorkflowExecuteMode, expressionResolveValues?: ICredentialsExpressionResolveValues): ICredentialDataDecryptedObject {
|
applyDefaultsAndOverwrites(
|
||||||
|
decryptedDataOriginal: ICredentialDataDecryptedObject,
|
||||||
|
type: string,
|
||||||
|
mode: WorkflowExecuteMode,
|
||||||
|
expressionResolveValues?: ICredentialsExpressionResolveValues,
|
||||||
|
): ICredentialDataDecryptedObject {
|
||||||
const credentialsProperties = this.getCredentialsProperties(type);
|
const credentialsProperties = this.getCredentialsProperties(type);
|
||||||
|
|
||||||
// Add the default credential values
|
// Add the default credential values
|
||||||
let decryptedData = NodeHelpers.getNodeParameters(credentialsProperties, decryptedDataOriginal as INodeParameters, true, false) as ICredentialDataDecryptedObject;
|
let decryptedData = NodeHelpers.getNodeParameters(
|
||||||
|
credentialsProperties,
|
||||||
|
decryptedDataOriginal as INodeParameters,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
) as ICredentialDataDecryptedObject;
|
||||||
|
|
||||||
if (decryptedDataOriginal.oauthTokenData !== undefined) {
|
if (decryptedDataOriginal.oauthTokenData !== undefined) {
|
||||||
// The OAuth data gets removed as it is not defined specifically as a parameter
|
// The OAuth data gets removed as it is not defined specifically as a parameter
|
||||||
|
@ -137,9 +159,26 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||||
|
|
||||||
if (expressionResolveValues) {
|
if (expressionResolveValues) {
|
||||||
try {
|
try {
|
||||||
const workflow = new Workflow({ nodes: Object.values(expressionResolveValues.workflow.nodes), connections: expressionResolveValues.workflow.connectionsBySourceNode, active: false, nodeTypes: expressionResolveValues.workflow.nodeTypes });
|
const workflow = new Workflow({
|
||||||
decryptedData = workflow.expression.getParameterValue(decryptedData as INodeParameters, expressionResolveValues.runExecutionData, expressionResolveValues.runIndex, expressionResolveValues.itemIndex, expressionResolveValues.node.name, expressionResolveValues.connectionInputData, mode, false, decryptedData) as ICredentialDataDecryptedObject;
|
nodes: Object.values(expressionResolveValues.workflow.nodes),
|
||||||
|
connections: expressionResolveValues.workflow.connectionsBySourceNode,
|
||||||
|
active: false,
|
||||||
|
nodeTypes: expressionResolveValues.workflow.nodeTypes,
|
||||||
|
});
|
||||||
|
decryptedData = workflow.expression.getParameterValue(
|
||||||
|
decryptedData as INodeParameters,
|
||||||
|
expressionResolveValues.runExecutionData,
|
||||||
|
expressionResolveValues.runIndex,
|
||||||
|
expressionResolveValues.itemIndex,
|
||||||
|
expressionResolveValues.node.name,
|
||||||
|
expressionResolveValues.connectionInputData,
|
||||||
|
mode,
|
||||||
|
{},
|
||||||
|
false,
|
||||||
|
decryptedData,
|
||||||
|
) as ICredentialDataDecryptedObject;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
e.message += ' [Error resolving credentials]';
|
e.message += ' [Error resolving credentials]';
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
@ -152,18 +191,30 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||||
parameters: {} as INodeParameters,
|
parameters: {} as INodeParameters,
|
||||||
} as INode;
|
} as INode;
|
||||||
|
|
||||||
const workflow = new Workflow({ nodes: [node!], connections: {}, active: false, nodeTypes: mockNodeTypes });
|
const workflow = new Workflow({
|
||||||
|
nodes: [node],
|
||||||
|
connections: {},
|
||||||
|
active: false,
|
||||||
|
nodeTypes: mockNodeTypes,
|
||||||
|
});
|
||||||
|
|
||||||
// Resolve expressions if any are set
|
// Resolve expressions if any are set
|
||||||
decryptedData = workflow.expression.getComplexParameterValue(node!, decryptedData as INodeParameters, mode, undefined, decryptedData) as ICredentialDataDecryptedObject;
|
decryptedData = workflow.expression.getComplexParameterValue(
|
||||||
|
node,
|
||||||
|
decryptedData as INodeParameters,
|
||||||
|
mode,
|
||||||
|
{},
|
||||||
|
undefined,
|
||||||
|
decryptedData,
|
||||||
|
) as ICredentialDataDecryptedObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load and apply the credentials overwrites if any exist
|
// Load and apply the credentials overwrites if any exist
|
||||||
const credentialsOverwrites = CredentialsOverwrites();
|
const credentialsOverwrites = CredentialsOverwrites();
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
return credentialsOverwrites.applyOverwrite(type, decryptedData);
|
return credentialsOverwrites.applyOverwrite(type, decryptedData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates credentials in the database
|
* Updates credentials in the database
|
||||||
*
|
*
|
||||||
|
@ -173,10 +224,15 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
* @memberof CredentialsHelper
|
* @memberof CredentialsHelper
|
||||||
*/
|
*/
|
||||||
async updateCredentials(name: string, type: string, data: ICredentialDataDecryptedObject): Promise<void> {
|
async updateCredentials(
|
||||||
|
name: string,
|
||||||
|
type: string,
|
||||||
|
data: ICredentialDataDecryptedObject,
|
||||||
|
): Promise<void> {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/await-thenable
|
||||||
const credentials = await this.getCredentials(name, type);
|
const credentials = await this.getCredentials(name, type);
|
||||||
|
|
||||||
if (Db.collections!.Credentials === null) {
|
if (Db.collections.Credentials === null) {
|
||||||
// The first time executeWorkflow gets called the Database has
|
// The first time executeWorkflow gets called the Database has
|
||||||
// to get initialized first
|
// to get initialized first
|
||||||
await Db.init();
|
await Db.init();
|
||||||
|
@ -196,7 +252,7 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||||
type,
|
type,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
await Db.collections.Credentials!.update(findQuery, newCredentialsData);
|
await Db.collections.Credentials!.update(findQuery, newCredentialsData);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,15 @@
|
||||||
import {
|
/* eslint-disable no-underscore-dangle */
|
||||||
ICredentialDataDecryptedObject,
|
import { ICredentialDataDecryptedObject } from 'n8n-workflow';
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
import {
|
|
||||||
CredentialTypes,
|
|
||||||
GenericHelpers,
|
|
||||||
ICredentialsOverwrite,
|
|
||||||
} from './';
|
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-cycle
|
||||||
|
import { CredentialTypes, GenericHelpers, ICredentialsOverwrite } from '.';
|
||||||
|
|
||||||
class CredentialsOverwritesClass {
|
class CredentialsOverwritesClass {
|
||||||
|
|
||||||
private credentialTypes = CredentialTypes();
|
private credentialTypes = CredentialTypes();
|
||||||
private overwriteData: ICredentialsOverwrite = {};
|
|
||||||
private resolvedTypes: string[] = [];
|
|
||||||
|
|
||||||
|
private overwriteData: ICredentialsOverwrite = {};
|
||||||
|
|
||||||
|
private resolvedTypes: string[] = [];
|
||||||
|
|
||||||
async init(overwriteData?: ICredentialsOverwrite) {
|
async init(overwriteData?: ICredentialsOverwrite) {
|
||||||
if (overwriteData !== undefined) {
|
if (overwriteData !== undefined) {
|
||||||
|
@ -24,9 +19,10 @@ class CredentialsOverwritesClass {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await GenericHelpers.getConfigValue('credentials.overwrite.data') as string;
|
const data = (await GenericHelpers.getConfigValue('credentials.overwrite.data')) as string;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-shadow
|
||||||
const overwriteData = JSON.parse(data);
|
const overwriteData = JSON.parse(data);
|
||||||
this.__setData(overwriteData);
|
this.__setData(overwriteData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -34,10 +30,10 @@ class CredentialsOverwritesClass {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
__setData(overwriteData: ICredentialsOverwrite) {
|
__setData(overwriteData: ICredentialsOverwrite) {
|
||||||
this.overwriteData = overwriteData;
|
this.overwriteData = overwriteData;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const credentialTypeData of this.credentialTypes.getAll()) {
|
for (const credentialTypeData of this.credentialTypes.getAll()) {
|
||||||
const type = credentialTypeData.name;
|
const type = credentialTypeData.name;
|
||||||
|
|
||||||
|
@ -49,29 +45,30 @@ class CredentialsOverwritesClass {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
applyOverwrite(type: string, data: ICredentialDataDecryptedObject) {
|
applyOverwrite(type: string, data: ICredentialDataDecryptedObject) {
|
||||||
|
|
||||||
const overwrites = this.get(type);
|
const overwrites = this.get(type);
|
||||||
|
|
||||||
if (overwrites === undefined) {
|
if (overwrites === undefined) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
const returnData = JSON.parse(JSON.stringify(data));
|
const returnData = JSON.parse(JSON.stringify(data));
|
||||||
// Overwrite only if there is currently no data set
|
// Overwrite only if there is currently no data set
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const key of Object.keys(overwrites)) {
|
for (const key of Object.keys(overwrites)) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
if ([null, undefined, ''].includes(returnData[key])) {
|
if ([null, undefined, ''].includes(returnData[key])) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
returnData[key] = overwrites[key];
|
returnData[key] = overwrites[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
return returnData;
|
return returnData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
__getExtended(type: string): ICredentialDataDecryptedObject | undefined {
|
__getExtended(type: string): ICredentialDataDecryptedObject | undefined {
|
||||||
|
|
||||||
if (this.resolvedTypes.includes(type)) {
|
if (this.resolvedTypes.includes(type)) {
|
||||||
// Type got already resolved and can so returned directly
|
// Type got already resolved and can so returned directly
|
||||||
return this.overwriteData[type];
|
return this.overwriteData[type];
|
||||||
|
@ -89,6 +86,7 @@ class CredentialsOverwritesClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
const overwrites: ICredentialDataDecryptedObject = {};
|
const overwrites: ICredentialDataDecryptedObject = {};
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const credentialsTypeName of credentialTypeData.extends) {
|
for (const credentialsTypeName of credentialTypeData.extends) {
|
||||||
Object.assign(overwrites, this.__getExtended(credentialsTypeName));
|
Object.assign(overwrites, this.__getExtended(credentialsTypeName));
|
||||||
}
|
}
|
||||||
|
@ -102,20 +100,18 @@ class CredentialsOverwritesClass {
|
||||||
return overwrites;
|
return overwrites;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
get(type: string): ICredentialDataDecryptedObject | undefined {
|
get(type: string): ICredentialDataDecryptedObject | undefined {
|
||||||
return this.overwriteData[type];
|
return this.overwriteData[type];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
getAll(): ICredentialsOverwrite {
|
getAll(): ICredentialsOverwrite {
|
||||||
return this.overwriteData;
|
return this.overwriteData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let credentialsOverwritesInstance: CredentialsOverwritesClass | undefined;
|
let credentialsOverwritesInstance: CredentialsOverwritesClass | undefined;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
export function CredentialsOverwrites(): CredentialsOverwritesClass {
|
export function CredentialsOverwrites(): CredentialsOverwritesClass {
|
||||||
if (credentialsOverwritesInstance === undefined) {
|
if (credentialsOverwritesInstance === undefined) {
|
||||||
credentialsOverwritesInstance = new CredentialsOverwritesClass();
|
credentialsOverwritesInstance = new CredentialsOverwritesClass();
|
||||||
|
|
|
@ -1,26 +1,24 @@
|
||||||
import {
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
DatabaseType,
|
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||||
GenericHelpers,
|
/* eslint-disable no-case-declarations */
|
||||||
IDatabaseCollections,
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
} from './';
|
import { UserSettings } from 'n8n-core';
|
||||||
|
import { ConnectionOptions, createConnection, getRepository } from 'typeorm';
|
||||||
import {
|
|
||||||
UserSettings,
|
|
||||||
} from 'n8n-core';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ConnectionOptions,
|
|
||||||
createConnection,
|
|
||||||
getRepository,
|
|
||||||
} from 'typeorm';
|
|
||||||
|
|
||||||
import { TlsOptions } from 'tls';
|
import { TlsOptions } from 'tls';
|
||||||
|
import * as path from 'path';
|
||||||
|
// eslint-disable-next-line import/no-cycle
|
||||||
|
import { DatabaseType, GenericHelpers, IDatabaseCollections } from '.';
|
||||||
|
|
||||||
import * as config from '../config';
|
import * as config from '../config';
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-cycle
|
||||||
import { entities } from './databases/entities';
|
import { entities } from './databases/entities';
|
||||||
|
|
||||||
export let collections: IDatabaseCollections = {
|
import { postgresMigrations } from './databases/postgresdb/migrations';
|
||||||
|
import { mysqlMigrations } from './databases/mysqldb/migrations';
|
||||||
|
import { sqliteMigrations } from './databases/sqlite/migrations';
|
||||||
|
|
||||||
|
export const collections: IDatabaseCollections = {
|
||||||
Credentials: null,
|
Credentials: null,
|
||||||
Execution: null,
|
Execution: null,
|
||||||
Workflow: null,
|
Workflow: null,
|
||||||
|
@ -28,14 +26,8 @@ export let collections: IDatabaseCollections = {
|
||||||
Tag: null,
|
Tag: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
import { postgresMigrations } from './databases/postgresdb/migrations';
|
|
||||||
import { mysqlMigrations } from './databases/mysqldb/migrations';
|
|
||||||
import { sqliteMigrations } from './databases/sqlite/migrations';
|
|
||||||
|
|
||||||
import * as path from 'path';
|
|
||||||
|
|
||||||
export async function init(): Promise<IDatabaseCollections> {
|
export async function init(): Promise<IDatabaseCollections> {
|
||||||
const dbType = await GenericHelpers.getConfigValue('database.type') as DatabaseType;
|
const dbType = (await GenericHelpers.getConfigValue('database.type')) as DatabaseType;
|
||||||
const n8nFolder = UserSettings.getUserN8nFolderPath();
|
const n8nFolder = UserSettings.getUserN8nFolderPath();
|
||||||
|
|
||||||
let connectionOptions: ConnectionOptions;
|
let connectionOptions: ConnectionOptions;
|
||||||
|
@ -44,13 +36,17 @@ export async function init(): Promise<IDatabaseCollections> {
|
||||||
|
|
||||||
switch (dbType) {
|
switch (dbType) {
|
||||||
case 'postgresdb':
|
case 'postgresdb':
|
||||||
const sslCa = await GenericHelpers.getConfigValue('database.postgresdb.ssl.ca') as string;
|
const sslCa = (await GenericHelpers.getConfigValue('database.postgresdb.ssl.ca')) as string;
|
||||||
const sslCert = await GenericHelpers.getConfigValue('database.postgresdb.ssl.cert') as string;
|
const sslCert = (await GenericHelpers.getConfigValue(
|
||||||
const sslKey = await GenericHelpers.getConfigValue('database.postgresdb.ssl.key') as string;
|
'database.postgresdb.ssl.cert',
|
||||||
const sslRejectUnauthorized = await GenericHelpers.getConfigValue('database.postgresdb.ssl.rejectUnauthorized') as boolean;
|
)) as string;
|
||||||
|
const sslKey = (await GenericHelpers.getConfigValue('database.postgresdb.ssl.key')) as string;
|
||||||
|
const sslRejectUnauthorized = (await GenericHelpers.getConfigValue(
|
||||||
|
'database.postgresdb.ssl.rejectUnauthorized',
|
||||||
|
)) as boolean;
|
||||||
|
|
||||||
let ssl: TlsOptions | undefined = undefined;
|
let ssl: TlsOptions | undefined;
|
||||||
if (sslCa !== '' || sslCert !== '' || sslKey !== '' || sslRejectUnauthorized !== true) {
|
if (sslCa !== '' || sslCert !== '' || sslKey !== '' || !sslRejectUnauthorized) {
|
||||||
ssl = {
|
ssl = {
|
||||||
ca: sslCa || undefined,
|
ca: sslCa || undefined,
|
||||||
cert: sslCert || undefined,
|
cert: sslCert || undefined,
|
||||||
|
@ -62,11 +58,11 @@ export async function init(): Promise<IDatabaseCollections> {
|
||||||
connectionOptions = {
|
connectionOptions = {
|
||||||
type: 'postgres',
|
type: 'postgres',
|
||||||
entityPrefix,
|
entityPrefix,
|
||||||
database: await GenericHelpers.getConfigValue('database.postgresdb.database') as string,
|
database: (await GenericHelpers.getConfigValue('database.postgresdb.database')) as string,
|
||||||
host: await GenericHelpers.getConfigValue('database.postgresdb.host') as string,
|
host: (await GenericHelpers.getConfigValue('database.postgresdb.host')) as string,
|
||||||
password: await GenericHelpers.getConfigValue('database.postgresdb.password') as string,
|
password: (await GenericHelpers.getConfigValue('database.postgresdb.password')) as string,
|
||||||
port: await GenericHelpers.getConfigValue('database.postgresdb.port') as number,
|
port: (await GenericHelpers.getConfigValue('database.postgresdb.port')) as number,
|
||||||
username: await GenericHelpers.getConfigValue('database.postgresdb.user') as string,
|
username: (await GenericHelpers.getConfigValue('database.postgresdb.user')) as string,
|
||||||
schema: config.get('database.postgresdb.schema'),
|
schema: config.get('database.postgresdb.schema'),
|
||||||
migrations: postgresMigrations,
|
migrations: postgresMigrations,
|
||||||
migrationsRun: true,
|
migrationsRun: true,
|
||||||
|
@ -80,12 +76,12 @@ export async function init(): Promise<IDatabaseCollections> {
|
||||||
case 'mysqldb':
|
case 'mysqldb':
|
||||||
connectionOptions = {
|
connectionOptions = {
|
||||||
type: dbType === 'mysqldb' ? 'mysql' : 'mariadb',
|
type: dbType === 'mysqldb' ? 'mysql' : 'mariadb',
|
||||||
database: await GenericHelpers.getConfigValue('database.mysqldb.database') as string,
|
database: (await GenericHelpers.getConfigValue('database.mysqldb.database')) as string,
|
||||||
entityPrefix,
|
entityPrefix,
|
||||||
host: await GenericHelpers.getConfigValue('database.mysqldb.host') as string,
|
host: (await GenericHelpers.getConfigValue('database.mysqldb.host')) as string,
|
||||||
password: await GenericHelpers.getConfigValue('database.mysqldb.password') as string,
|
password: (await GenericHelpers.getConfigValue('database.mysqldb.password')) as string,
|
||||||
port: await GenericHelpers.getConfigValue('database.mysqldb.port') as number,
|
port: (await GenericHelpers.getConfigValue('database.mysqldb.port')) as number,
|
||||||
username: await GenericHelpers.getConfigValue('database.mysqldb.user') as string,
|
username: (await GenericHelpers.getConfigValue('database.mysqldb.user')) as string,
|
||||||
migrations: mysqlMigrations,
|
migrations: mysqlMigrations,
|
||||||
migrationsRun: true,
|
migrationsRun: true,
|
||||||
migrationsTableName: `${entityPrefix}migrations`,
|
migrationsTableName: `${entityPrefix}migrations`,
|
||||||
|
@ -106,7 +102,7 @@ export async function init(): Promise<IDatabaseCollections> {
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`The database "${dbType}" is currently not supported!`);
|
throw new Error(`The database "${dbType}" is currently not supported!`);
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.assign(connectionOptions, {
|
Object.assign(connectionOptions, {
|
||||||
entities: Object.values(entities),
|
entities: Object.values(entities),
|
||||||
|
@ -122,8 +118,10 @@ export async function init(): Promise<IDatabaseCollections> {
|
||||||
// n8n knows it has changed. Happens only on sqlite.
|
// n8n knows it has changed. Happens only on sqlite.
|
||||||
let migrations = [];
|
let migrations = [];
|
||||||
try {
|
try {
|
||||||
migrations = await connection.query(`SELECT id FROM ${entityPrefix}migrations where name = "MakeStoppedAtNullable1607431743769"`);
|
migrations = await connection.query(
|
||||||
} catch(error) {
|
`SELECT id FROM ${entityPrefix}migrations where name = "MakeStoppedAtNullable1607431743769"`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
// Migration table does not exist yet - it will be created after migrations run for the first time.
|
// Migration table does not exist yet - it will be created after migrations run for the first time.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,6 +131,7 @@ export async function init(): Promise<IDatabaseCollections> {
|
||||||
transaction: 'none',
|
transaction: 'none',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
if (migrations.length === 0) {
|
if (migrations.length === 0) {
|
||||||
await connection.close();
|
await connection.close();
|
||||||
connection = await createConnection(connectionOptions);
|
connection = await createConnection(connectionOptions);
|
||||||
|
|
|
@ -1,23 +1,20 @@
|
||||||
import {
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
Db,
|
/* eslint-disable import/no-dynamic-require */
|
||||||
IExternalHooksClass,
|
/* eslint-disable no-restricted-syntax */
|
||||||
IExternalHooksFileData,
|
// eslint-disable-next-line import/no-cycle
|
||||||
IExternalHooksFunctions,
|
import { Db, IExternalHooksClass, IExternalHooksFileData, IExternalHooksFunctions } from '.';
|
||||||
} from './';
|
|
||||||
|
|
||||||
import * as config from '../config';
|
import * as config from '../config';
|
||||||
|
|
||||||
|
|
||||||
class ExternalHooksClass implements IExternalHooksClass {
|
class ExternalHooksClass implements IExternalHooksClass {
|
||||||
|
|
||||||
externalHooks: {
|
externalHooks: {
|
||||||
[key: string]: Array<() => {}>
|
[key: string]: Array<() => {}>;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
initDidRun = false;
|
initDidRun = false;
|
||||||
|
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
if (this.initDidRun === true) {
|
if (this.initDidRun) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +23,6 @@ class ExternalHooksClass implements IExternalHooksClass {
|
||||||
this.initDidRun = true;
|
this.initDidRun = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async reload(externalHooks?: IExternalHooksFileData) {
|
async reload(externalHooks?: IExternalHooksFileData) {
|
||||||
this.externalHooks = {};
|
this.externalHooks = {};
|
||||||
|
|
||||||
|
@ -37,7 +33,6 @@ class ExternalHooksClass implements IExternalHooksClass {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async loadHooksFiles(reload = false) {
|
async loadHooksFiles(reload = false) {
|
||||||
const externalHookFiles = config.get('externalHookFiles').split(':');
|
const externalHookFiles = config.get('externalHookFiles').split(':');
|
||||||
|
|
||||||
|
@ -46,21 +41,22 @@ class ExternalHooksClass implements IExternalHooksClass {
|
||||||
hookFilePath = hookFilePath.trim();
|
hookFilePath = hookFilePath.trim();
|
||||||
if (hookFilePath !== '') {
|
if (hookFilePath !== '') {
|
||||||
try {
|
try {
|
||||||
|
if (reload) {
|
||||||
if (reload === true) {
|
|
||||||
delete require.cache[require.resolve(hookFilePath)];
|
delete require.cache[require.resolve(hookFilePath)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-dynamic-require
|
||||||
|
// eslint-disable-next-line global-require
|
||||||
const hookFile = require(hookFilePath) as IExternalHooksFileData;
|
const hookFile = require(hookFilePath) as IExternalHooksFileData;
|
||||||
this.loadHooks(hookFile);
|
this.loadHooks(hookFile);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
|
||||||
throw new Error(`Problem loading external hook file "${hookFilePath}": ${error.message}`);
|
throw new Error(`Problem loading external hook file "${hookFilePath}": ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
loadHooks(hookFileData: IExternalHooksFileData) {
|
loadHooks(hookFileData: IExternalHooksFileData) {
|
||||||
for (const resource of Object.keys(hookFileData)) {
|
for (const resource of Object.keys(hookFileData)) {
|
||||||
for (const operation of Object.keys(hookFileData[resource])) {
|
for (const operation of Object.keys(hookFileData[resource])) {
|
||||||
|
@ -71,13 +67,17 @@ class ExternalHooksClass implements IExternalHooksClass {
|
||||||
this.externalHooks[hookString] = [];
|
this.externalHooks[hookString] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
this.externalHooks[hookString].push.apply(this.externalHooks[hookString], hookFileData[resource][operation]);
|
// eslint-disable-next-line prefer-spread
|
||||||
|
this.externalHooks[hookString].push.apply(
|
||||||
|
this.externalHooks[hookString],
|
||||||
|
hookFileData[resource][operation],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
async run(hookName: string, hookParameters?: any[]): Promise<void> { // tslint:disable-line:no-any
|
async run(hookName: string, hookParameters?: any[]): Promise<void> {
|
||||||
const externalHookFunctions: IExternalHooksFunctions = {
|
const externalHookFunctions: IExternalHooksFunctions = {
|
||||||
dbCollections: Db.collections,
|
dbCollections: Db.collections,
|
||||||
};
|
};
|
||||||
|
@ -86,22 +86,20 @@ class ExternalHooksClass implements IExternalHooksClass {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for(const externalHookFunction of this.externalHooks[hookName]) {
|
for (const externalHookFunction of this.externalHooks[hookName]) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await externalHookFunction.apply(externalHookFunctions, hookParameters);
|
await externalHookFunction.apply(externalHookFunctions, hookParameters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
exists(hookName: string): boolean {
|
exists(hookName: string): boolean {
|
||||||
return !!this.externalHooks[hookName];
|
return !!this.externalHooks[hookName];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let externalHooksInstance: ExternalHooksClass | undefined;
|
let externalHooksInstance: ExternalHooksClass | undefined;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
export function ExternalHooks(): ExternalHooksClass {
|
export function ExternalHooks(): ExternalHooksClass {
|
||||||
if (externalHooksInstance === undefined) {
|
if (externalHooksInstance === undefined) {
|
||||||
externalHooksInstance = new ExternalHooksClass();
|
externalHooksInstance = new ExternalHooksClass();
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
import * as config from '../config';
|
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
/* eslint-disable no-underscore-dangle */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
import { join as pathJoin } from 'path';
|
import { join as pathJoin } from 'path';
|
||||||
import { readFile as fsReadFile } from 'fs/promises';
|
import { readFile as fsReadFile } from 'fs/promises';
|
||||||
import { readFileSync as fsReadFileSync } from 'fs';
|
import { readFileSync as fsReadFileSync } from 'fs';
|
||||||
import { IDataObject } from 'n8n-workflow';
|
import { IDataObject } from 'n8n-workflow';
|
||||||
|
import * as config from '../config';
|
||||||
|
|
||||||
import { IPackageVersions } from './';
|
// eslint-disable-next-line import/no-cycle
|
||||||
|
import { IPackageVersions } from '.';
|
||||||
|
|
||||||
let versionCache: IPackageVersions | undefined;
|
let versionCache: IPackageVersions | undefined;
|
||||||
|
|
||||||
|
@ -16,18 +22,17 @@ let versionCache: IPackageVersions | undefined;
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export function getBaseUrl(): string {
|
export function getBaseUrl(): string {
|
||||||
const protocol = config.get('protocol') as string;
|
const protocol = config.get('protocol');
|
||||||
const host = config.get('host') as string;
|
const host = config.get('host');
|
||||||
const port = config.get('port') as number;
|
const port = config.get('port');
|
||||||
const path = config.get('path') as string;
|
const path = config.get('path');
|
||||||
|
|
||||||
if (protocol === 'http' && port === 80 || protocol === 'https' && port === 443) {
|
if ((protocol === 'http' && port === 80) || (protocol === 'https' && port === 443)) {
|
||||||
return `${protocol}://${host}${path}`;
|
return `${protocol}://${host}${path}`;
|
||||||
}
|
}
|
||||||
return `${protocol}://${host}:${port}${path}`;
|
return `${protocol}://${host}:${port}${path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the session id if one is set
|
* Returns the session id if one is set
|
||||||
*
|
*
|
||||||
|
@ -39,7 +44,6 @@ export function getSessionId(req: express.Request): string | undefined {
|
||||||
return req.headers.sessionid as string | undefined;
|
return req.headers.sessionid as string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns information which version of the packages are installed
|
* Returns information which version of the packages are installed
|
||||||
*
|
*
|
||||||
|
@ -51,10 +55,12 @@ export async function getVersions(): Promise<IPackageVersions> {
|
||||||
return versionCache;
|
return versionCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
const packageFile = await fsReadFile(pathJoin(__dirname, '../../package.json'), 'utf8') as string;
|
const packageFile = await fsReadFile(pathJoin(__dirname, '../../package.json'), 'utf8');
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
const packageData = JSON.parse(packageFile);
|
const packageData = JSON.parse(packageFile);
|
||||||
|
|
||||||
versionCache = {
|
versionCache = {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
cli: packageData.version,
|
cli: packageData.version,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -71,9 +77,11 @@ export async function getVersions(): Promise<IPackageVersions> {
|
||||||
function extractSchemaForKey(configKey: string, configSchema: IDataObject): IDataObject {
|
function extractSchemaForKey(configKey: string, configSchema: IDataObject): IDataObject {
|
||||||
const configKeyParts = configKey.split('.');
|
const configKeyParts = configKey.split('.');
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const key of configKeyParts) {
|
for (const key of configKeyParts) {
|
||||||
if (configSchema[key] === undefined) {
|
if (configSchema[key] === undefined) {
|
||||||
throw new Error(`Key "${key}" of ConfigKey "${configKey}" does not exist`);
|
throw new Error(`Key "${key}" of ConfigKey "${configKey}" does not exist`);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
} else if ((configSchema[key]! as IDataObject)._cvtProperties === undefined) {
|
} else if ((configSchema[key]! as IDataObject)._cvtProperties === undefined) {
|
||||||
configSchema = configSchema[key] as IDataObject;
|
configSchema = configSchema[key] as IDataObject;
|
||||||
} else {
|
} else {
|
||||||
|
@ -90,7 +98,9 @@ function extractSchemaForKey(configKey: string, configSchema: IDataObject): IDat
|
||||||
* @param {string} configKey The key of the config data to get
|
* @param {string} configKey The key of the config data to get
|
||||||
* @returns {(Promise<string | boolean | number | undefined>)}
|
* @returns {(Promise<string | boolean | number | undefined>)}
|
||||||
*/
|
*/
|
||||||
export async function getConfigValue(configKey: string): Promise<string | boolean | number | undefined> {
|
export async function getConfigValue(
|
||||||
|
configKey: string,
|
||||||
|
): Promise<string | boolean | number | undefined> {
|
||||||
// Get the environment variable
|
// Get the environment variable
|
||||||
const configSchema = config.getSchema();
|
const configSchema = config.getSchema();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -102,7 +112,7 @@ export async function getConfigValue(configKey: string): Promise<string | boolea
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if special file enviroment variable exists
|
// Check if special file enviroment variable exists
|
||||||
const fileEnvironmentVariable = process.env[currentSchema.env + '_FILE'];
|
const fileEnvironmentVariable = process.env[`${currentSchema.env}_FILE`];
|
||||||
if (fileEnvironmentVariable === undefined) {
|
if (fileEnvironmentVariable === undefined) {
|
||||||
// Does not exist, so return value from config
|
// Does not exist, so return value from config
|
||||||
return config.get(configKey);
|
return config.get(configKey);
|
||||||
|
@ -110,7 +120,7 @@ export async function getConfigValue(configKey: string): Promise<string | boolea
|
||||||
|
|
||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
data = await fsReadFile(fileEnvironmentVariable, 'utf8') as string;
|
data = await fsReadFile(fileEnvironmentVariable, 'utf8');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === 'ENOENT') {
|
if (error.code === 'ENOENT') {
|
||||||
throw new Error(`The file "${fileEnvironmentVariable}" could not be found.`);
|
throw new Error(`The file "${fileEnvironmentVariable}" could not be found.`);
|
||||||
|
@ -141,7 +151,7 @@ export function getConfigValueSync(configKey: string): string | boolean | number
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if special file enviroment variable exists
|
// Check if special file enviroment variable exists
|
||||||
const fileEnvironmentVariable = process.env[currentSchema.env + '_FILE'];
|
const fileEnvironmentVariable = process.env[`${currentSchema.env}_FILE`];
|
||||||
if (fileEnvironmentVariable === undefined) {
|
if (fileEnvironmentVariable === undefined) {
|
||||||
// Does not exist, so return value from config
|
// Does not exist, so return value from config
|
||||||
return config.get(configKey);
|
return config.get(configKey);
|
||||||
|
@ -149,7 +159,7 @@ export function getConfigValueSync(configKey: string): string | boolean | number
|
||||||
|
|
||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
data = fsReadFileSync(fileEnvironmentVariable, 'utf8') as string;
|
data = fsReadFileSync(fileEnvironmentVariable, 'utf8');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === 'ENOENT') {
|
if (error.code === 'ENOENT') {
|
||||||
throw new Error(`The file "${fileEnvironmentVariable}" could not be found.`);
|
throw new Error(`The file "${fileEnvironmentVariable}" could not be found.`);
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
/* eslint-disable import/no-cycle */
|
||||||
import {
|
import {
|
||||||
ExecutionError,
|
ExecutionError,
|
||||||
ICredentialDataDecryptedObject,
|
ICredentialDataDecryptedObject,
|
||||||
|
@ -10,15 +12,15 @@ import {
|
||||||
IRunExecutionData,
|
IRunExecutionData,
|
||||||
ITaskData,
|
ITaskData,
|
||||||
IWorkflowBase as IWorkflowBaseWorkflow,
|
IWorkflowBase as IWorkflowBaseWorkflow,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
IWorkflowCredentials,
|
IWorkflowCredentials,
|
||||||
Workflow,
|
Workflow,
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import {
|
import { IDeferredPromise, WorkflowExecute } from 'n8n-core';
|
||||||
IDeferredPromise, WorkflowExecute,
|
|
||||||
} from 'n8n-core';
|
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
import * as PCancelable from 'p-cancelable';
|
import * as PCancelable from 'p-cancelable';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
@ -85,7 +87,7 @@ export interface ITagDb {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UsageCount = {
|
export type UsageCount = {
|
||||||
usageCount: number
|
usageCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ITagWithCountDb = ITagDb & UsageCount;
|
export type ITagWithCountDb = ITagDb & UsageCount;
|
||||||
|
@ -150,6 +152,7 @@ export interface IExecutionBase {
|
||||||
// Data in regular format with references
|
// Data in regular format with references
|
||||||
export interface IExecutionDb extends IExecutionBase {
|
export interface IExecutionDb extends IExecutionBase {
|
||||||
data: IRunExecutionData;
|
data: IRunExecutionData;
|
||||||
|
waitTill?: Date;
|
||||||
workflowData?: IWorkflowBase;
|
workflowData?: IWorkflowBase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,6 +166,7 @@ export interface IExecutionResponse extends IExecutionBase {
|
||||||
data: IRunExecutionData;
|
data: IRunExecutionData;
|
||||||
retryOf?: string;
|
retryOf?: string;
|
||||||
retrySuccessId?: string;
|
retrySuccessId?: string;
|
||||||
|
waitTill?: Date;
|
||||||
workflowData: IWorkflowBase;
|
workflowData: IWorkflowBase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,6 +180,7 @@ export interface IExecutionFlatted extends IExecutionBase {
|
||||||
export interface IExecutionFlattedDb extends IExecutionBase {
|
export interface IExecutionFlattedDb extends IExecutionBase {
|
||||||
id: number | string;
|
id: number | string;
|
||||||
data: string;
|
data: string;
|
||||||
|
waitTill?: Date | null;
|
||||||
workflowData: IWorkflowBase;
|
workflowData: IWorkflowBase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,13 +209,13 @@ export interface IExecutionsSummary {
|
||||||
mode: WorkflowExecuteMode;
|
mode: WorkflowExecuteMode;
|
||||||
retryOf?: string;
|
retryOf?: string;
|
||||||
retrySuccessId?: string;
|
retrySuccessId?: string;
|
||||||
|
waitTill?: Date;
|
||||||
startedAt: Date;
|
startedAt: Date;
|
||||||
stoppedAt?: Date;
|
stoppedAt?: Date;
|
||||||
workflowId: string;
|
workflowId: string;
|
||||||
workflowName?: string;
|
workflowName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface IExecutionsCurrentSummary {
|
export interface IExecutionsCurrentSummary {
|
||||||
id: string;
|
id: string;
|
||||||
retryOf?: string;
|
retryOf?: string;
|
||||||
|
@ -219,7 +224,6 @@ export interface IExecutionsCurrentSummary {
|
||||||
workflowId: string;
|
workflowId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface IExecutionDeleteFilter {
|
export interface IExecutionDeleteFilter {
|
||||||
deleteBefore?: Date;
|
deleteBefore?: Date;
|
||||||
filters?: IDataObject;
|
filters?: IDataObject;
|
||||||
|
@ -236,22 +240,33 @@ export interface IExecutingWorkflowData {
|
||||||
|
|
||||||
export interface IExternalHooks {
|
export interface IExternalHooks {
|
||||||
credentials?: {
|
credentials?: {
|
||||||
create?: Array<{ (this: IExternalHooksFunctions, credentialsData: ICredentialsEncrypted): Promise<void>; }>
|
create?: Array<{
|
||||||
delete?: Array<{ (this: IExternalHooksFunctions, credentialId: string): Promise<void>; }>
|
(this: IExternalHooksFunctions, credentialsData: ICredentialsEncrypted): Promise<void>;
|
||||||
update?: Array<{ (this: IExternalHooksFunctions, credentialsData: ICredentialsDb): Promise<void>; }>
|
}>;
|
||||||
|
delete?: Array<{ (this: IExternalHooksFunctions, credentialId: string): Promise<void> }>;
|
||||||
|
update?: Array<{
|
||||||
|
(this: IExternalHooksFunctions, credentialsData: ICredentialsDb): Promise<void>;
|
||||||
|
}>;
|
||||||
};
|
};
|
||||||
workflow?: {
|
workflow?: {
|
||||||
activate?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb): Promise<void>; }>
|
activate?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb): Promise<void> }>;
|
||||||
create?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowBase): Promise<void>; }>
|
create?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowBase): Promise<void> }>;
|
||||||
delete?: Array<{ (this: IExternalHooksFunctions, workflowId: string): Promise<void>; }>
|
delete?: Array<{ (this: IExternalHooksFunctions, workflowId: string): Promise<void> }>;
|
||||||
execute?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb, mode: WorkflowExecuteMode): Promise<void>; }>
|
execute?: Array<{
|
||||||
update?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb): Promise<void>; }>
|
(
|
||||||
|
this: IExternalHooksFunctions,
|
||||||
|
workflowData: IWorkflowDb,
|
||||||
|
mode: WorkflowExecuteMode,
|
||||||
|
): Promise<void>;
|
||||||
|
}>;
|
||||||
|
update?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb): Promise<void> }>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IExternalHooksFileData {
|
export interface IExternalHooksFileData {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
[key: string]: Array<(...args: any[]) => Promise<void>>; //tslint:disable-line:no-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
[key: string]: Array<(...args: any[]) => Promise<void>>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -261,7 +276,8 @@ export interface IExternalHooksFunctions {
|
||||||
|
|
||||||
export interface IExternalHooksClass {
|
export interface IExternalHooksClass {
|
||||||
init(): Promise<void>;
|
init(): Promise<void>;
|
||||||
run(hookName: string, hookParameters?: any[]): Promise<void>; // tslint:disable-line:no-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
run(hookName: string, hookParameters?: any[]): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IN8nConfig {
|
export interface IN8nConfig {
|
||||||
|
@ -291,12 +307,14 @@ export interface IN8nConfigEndpoints {
|
||||||
webhookTest: string;
|
webhookTest: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/export
|
||||||
export interface IN8nConfigExecutions {
|
export interface IN8nConfigExecutions {
|
||||||
saveDataOnError: SaveExecutionDataType;
|
saveDataOnError: SaveExecutionDataType;
|
||||||
saveDataOnSuccess: SaveExecutionDataType;
|
saveDataOnSuccess: SaveExecutionDataType;
|
||||||
saveDataManualExecutions: boolean;
|
saveDataManualExecutions: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/export
|
||||||
export interface IN8nConfigExecutions {
|
export interface IN8nConfigExecutions {
|
||||||
saveDataOnError: SaveExecutionDataType;
|
saveDataOnError: SaveExecutionDataType;
|
||||||
saveDataOnSuccess: SaveExecutionDataType;
|
saveDataOnSuccess: SaveExecutionDataType;
|
||||||
|
@ -405,13 +423,11 @@ export interface IPushDataNodeExecuteAfter {
|
||||||
nodeName: string;
|
nodeName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface IPushDataNodeExecuteBefore {
|
export interface IPushDataNodeExecuteBefore {
|
||||||
executionId: string;
|
executionId: string;
|
||||||
nodeName: string;
|
nodeName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface IPushDataTestWebhook {
|
export interface IPushDataTestWebhook {
|
||||||
executionId: string;
|
executionId: string;
|
||||||
workflowId: string;
|
workflowId: string;
|
||||||
|
@ -428,7 +444,6 @@ export interface IResponseCallbackData {
|
||||||
responseCode?: number;
|
responseCode?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface ITransferNodeTypes {
|
export interface ITransferNodeTypes {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
className: string;
|
className: string;
|
||||||
|
@ -436,7 +451,6 @@ export interface ITransferNodeTypes {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface IWorkflowErrorData {
|
export interface IWorkflowErrorData {
|
||||||
[key: string]: IDataObject | string | number | ExecutionError;
|
[key: string]: IDataObject | string | number | ExecutionError;
|
||||||
execution: {
|
execution: {
|
||||||
|
@ -453,11 +467,11 @@ export interface IWorkflowErrorData {
|
||||||
|
|
||||||
export interface IProcessMessageDataHook {
|
export interface IProcessMessageDataHook {
|
||||||
hook: string;
|
hook: string;
|
||||||
parameters: any[]; // tslint:disable-line:no-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
parameters: any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IWorkflowExecutionDataProcess {
|
export interface IWorkflowExecutionDataProcess {
|
||||||
credentials: IWorkflowCredentials;
|
|
||||||
destinationNode?: string;
|
destinationNode?: string;
|
||||||
executionMode: WorkflowExecuteMode;
|
executionMode: WorkflowExecuteMode;
|
||||||
executionData?: IRunExecutionData;
|
executionData?: IRunExecutionData;
|
||||||
|
@ -468,7 +482,6 @@ export interface IWorkflowExecutionDataProcess {
|
||||||
workflowData: IWorkflowBase;
|
workflowData: IWorkflowBase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface IWorkflowExecutionDataProcessWithExecution extends IWorkflowExecutionDataProcess {
|
export interface IWorkflowExecutionDataProcessWithExecution extends IWorkflowExecutionDataProcess {
|
||||||
credentialsOverwrite: ICredentialsOverwrite;
|
credentialsOverwrite: ICredentialsOverwrite;
|
||||||
credentialsTypeData: ICredentialsTypeData;
|
credentialsTypeData: ICredentialsTypeData;
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
import {
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
CUSTOM_EXTENSION_ENV,
|
/* eslint-disable no-prototype-builtins */
|
||||||
UserSettings,
|
/* eslint-disable no-param-reassign */
|
||||||
} from 'n8n-core';
|
/* eslint-disable @typescript-eslint/prefer-optional-chain */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
|
/* eslint-disable no-continue */
|
||||||
|
/* eslint-disable no-restricted-syntax */
|
||||||
|
import { CUSTOM_EXTENSION_ENV, UserSettings } from 'n8n-core';
|
||||||
import {
|
import {
|
||||||
CodexData,
|
CodexData,
|
||||||
ICredentialType,
|
ICredentialType,
|
||||||
|
@ -11,32 +18,28 @@ import {
|
||||||
LoggerProxy,
|
LoggerProxy,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import * as config from '../config';
|
|
||||||
|
|
||||||
import {
|
|
||||||
getLogger,
|
|
||||||
} from '../src/Logger';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
access as fsAccess,
|
access as fsAccess,
|
||||||
readdir as fsReaddir,
|
readdir as fsReaddir,
|
||||||
readFile as fsReadFile,
|
readFile as fsReadFile,
|
||||||
stat as fsStat,
|
stat as fsStat,
|
||||||
} from 'fs/promises';
|
} from 'fs/promises';
|
||||||
import * as glob from 'glob-promise';
|
import * as glob from 'fast-glob';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import { getLogger } from './Logger';
|
||||||
|
import * as config from '../config';
|
||||||
|
|
||||||
const CUSTOM_NODES_CATEGORY = 'Custom Nodes';
|
const CUSTOM_NODES_CATEGORY = 'Custom Nodes';
|
||||||
|
|
||||||
|
|
||||||
class LoadNodesAndCredentialsClass {
|
class LoadNodesAndCredentialsClass {
|
||||||
nodeTypes: INodeTypeData = {};
|
nodeTypes: INodeTypeData = {};
|
||||||
|
|
||||||
credentialTypes: {
|
credentialTypes: {
|
||||||
[key: string]: ICredentialType
|
[key: string]: ICredentialType;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
excludeNodes: string[] | undefined = undefined;
|
excludeNodes: string[] | undefined = undefined;
|
||||||
|
|
||||||
includeNodes: string[] | undefined = undefined;
|
includeNodes: string[] | undefined = undefined;
|
||||||
|
|
||||||
nodeModulesPath = '';
|
nodeModulesPath = '';
|
||||||
|
@ -64,6 +67,7 @@ class LoadNodesAndCredentialsClass {
|
||||||
break;
|
break;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Folder does not exist so get next one
|
// Folder does not exist so get next one
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -90,7 +94,9 @@ class LoadNodesAndCredentialsClass {
|
||||||
|
|
||||||
// Add folders from special environment variable
|
// Add folders from special environment variable
|
||||||
if (process.env[CUSTOM_EXTENSION_ENV] !== undefined) {
|
if (process.env[CUSTOM_EXTENSION_ENV] !== undefined) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const customExtensionFolders = process.env[CUSTOM_EXTENSION_ENV]!.split(';');
|
const customExtensionFolders = process.env[CUSTOM_EXTENSION_ENV]!.split(';');
|
||||||
|
// eslint-disable-next-line prefer-spread
|
||||||
customDirectories.push.apply(customDirectories, customExtensionFolders);
|
customDirectories.push.apply(customDirectories, customExtensionFolders);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,7 +105,6 @@ class LoadNodesAndCredentialsClass {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all the names of the packages which could
|
* Returns all the names of the packages which could
|
||||||
* contain n8n nodes
|
* contain n8n nodes
|
||||||
|
@ -120,9 +125,11 @@ class LoadNodesAndCredentialsClass {
|
||||||
if (!(await fsStat(nodeModulesPath)).isDirectory()) {
|
if (!(await fsStat(nodeModulesPath)).isDirectory()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (isN8nNodesPackage) { results.push(`${relativePath}${file}`); }
|
if (isN8nNodesPackage) {
|
||||||
|
results.push(`${relativePath}${file}`);
|
||||||
|
}
|
||||||
if (isNpmScopedPackage) {
|
if (isNpmScopedPackage) {
|
||||||
results.push(...await getN8nNodePackagesRecursive(`${relativePath}${file}/`));
|
results.push(...(await getN8nNodePackagesRecursive(`${relativePath}${file}/`)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
|
@ -138,6 +145,7 @@ class LoadNodesAndCredentialsClass {
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async loadCredentialsFromFile(credentialName: string, filePath: string): Promise<void> {
|
async loadCredentialsFromFile(credentialName: string, filePath: string): Promise<void> {
|
||||||
|
// eslint-disable-next-line import/no-dynamic-require, global-require, @typescript-eslint/no-var-requires
|
||||||
const tempModule = require(filePath);
|
const tempModule = require(filePath);
|
||||||
|
|
||||||
let tempCredential: ICredentialType;
|
let tempCredential: ICredentialType;
|
||||||
|
@ -145,7 +153,9 @@ class LoadNodesAndCredentialsClass {
|
||||||
tempCredential = new tempModule[credentialName]() as ICredentialType;
|
tempCredential = new tempModule[credentialName]() as ICredentialType;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof TypeError) {
|
if (e instanceof TypeError) {
|
||||||
throw new Error(`Class with name "${credentialName}" could not be found. Please check if the class is named correctly!`);
|
throw new Error(
|
||||||
|
`Class with name "${credentialName}" could not be found. Please check if the class is named correctly!`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
@ -154,7 +164,6 @@ class LoadNodesAndCredentialsClass {
|
||||||
this.credentialTypes[tempCredential.name] = tempCredential;
|
this.credentialTypes[tempCredential.name] = tempCredential;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads a node from a file
|
* Loads a node from a file
|
||||||
*
|
*
|
||||||
|
@ -167,26 +176,34 @@ class LoadNodesAndCredentialsClass {
|
||||||
let tempNode: INodeType;
|
let tempNode: INodeType;
|
||||||
let fullNodeName: string;
|
let fullNodeName: string;
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-dynamic-require, global-require, @typescript-eslint/no-var-requires
|
||||||
const tempModule = require(filePath);
|
const tempModule = require(filePath);
|
||||||
try {
|
try {
|
||||||
tempNode = new tempModule[nodeName]() as INodeType;
|
tempNode = new tempModule[nodeName]() as INodeType;
|
||||||
this.addCodex({ node: tempNode, filePath, isCustom: packageName === 'CUSTOM' });
|
this.addCodex({ node: tempNode, filePath, isCustom: packageName === 'CUSTOM' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.error(`Error loading node "${nodeName}" from: "${filePath}"`);
|
console.error(`Error loading node "${nodeName}" from: "${filePath}"`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
fullNodeName = packageName + '.' + tempNode.description.name;
|
// eslint-disable-next-line prefer-const
|
||||||
|
fullNodeName = `${packageName}.${tempNode.description.name}`;
|
||||||
tempNode.description.name = fullNodeName;
|
tempNode.description.name = fullNodeName;
|
||||||
|
|
||||||
if (tempNode.description.icon !== undefined &&
|
if (tempNode.description.icon !== undefined && tempNode.description.icon.startsWith('file:')) {
|
||||||
tempNode.description.icon.startsWith('file:')) {
|
|
||||||
// If a file icon gets used add the full path
|
// If a file icon gets used add the full path
|
||||||
tempNode.description.icon = 'file:' + path.join(path.dirname(filePath), tempNode.description.icon.substr(5));
|
tempNode.description.icon = `file:${path.join(
|
||||||
|
path.dirname(filePath),
|
||||||
|
tempNode.description.icon.substr(5),
|
||||||
|
)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tempNode.executeSingle) {
|
if (tempNode.executeSingle) {
|
||||||
this.logger.warn(`"executeSingle" will get deprecated soon. Please update the code of node "${packageName}.${nodeName}" to use "execute" instead!`, { filePath });
|
this.logger.warn(
|
||||||
|
`"executeSingle" will get deprecated soon. Please update the code of node "${packageName}.${nodeName}" to use "execute" instead!`,
|
||||||
|
{ filePath },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.includeNodes !== undefined && !this.includeNodes.includes(fullNodeName)) {
|
if (this.includeNodes !== undefined && !this.includeNodes.includes(fullNodeName)) {
|
||||||
|
@ -212,7 +229,9 @@ class LoadNodesAndCredentialsClass {
|
||||||
* @returns {CodexData}
|
* @returns {CodexData}
|
||||||
*/
|
*/
|
||||||
getCodex(filePath: string): CodexData {
|
getCodex(filePath: string): CodexData {
|
||||||
|
// eslint-disable-next-line global-require, import/no-dynamic-require, @typescript-eslint/no-var-requires
|
||||||
const { categories, subcategories, alias } = require(`${filePath}on`); // .js to .json
|
const { categories, subcategories, alias } = require(`${filePath}on`); // .js to .json
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
return {
|
return {
|
||||||
...(categories && { categories }),
|
...(categories && { categories }),
|
||||||
...(subcategories && { subcategories }),
|
...(subcategories && { subcategories }),
|
||||||
|
@ -230,11 +249,7 @@ class LoadNodesAndCredentialsClass {
|
||||||
* @param obj.isCustom Whether the node is custom
|
* @param obj.isCustom Whether the node is custom
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
addCodex({ node, filePath, isCustom }: {
|
addCodex({ node, filePath, isCustom }: { node: INodeType; filePath: string; isCustom: boolean }) {
|
||||||
node: INodeType;
|
|
||||||
filePath: string;
|
|
||||||
isCustom: boolean;
|
|
||||||
}) {
|
|
||||||
try {
|
try {
|
||||||
const codex = this.getCodex(filePath);
|
const codex = this.getCodex(filePath);
|
||||||
|
|
||||||
|
@ -246,6 +261,7 @@ class LoadNodesAndCredentialsClass {
|
||||||
|
|
||||||
node.description.codex = codex;
|
node.description.codex = codex;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
this.logger.debug(`No codex available for: ${filePath.split('/').pop()}`);
|
this.logger.debug(`No codex available for: ${filePath.split('/').pop()}`);
|
||||||
|
|
||||||
if (isCustom) {
|
if (isCustom) {
|
||||||
|
@ -264,7 +280,7 @@ class LoadNodesAndCredentialsClass {
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async loadDataFromDirectory(setPackageName: string, directory: string): Promise<void> {
|
async loadDataFromDirectory(setPackageName: string, directory: string): Promise<void> {
|
||||||
const files = await glob(path.join(directory, '**/*\.@(node|credentials)\.js'));
|
const files = await glob(path.join(directory, '**/*.@(node|credentials).js'));
|
||||||
|
|
||||||
let fileName: string;
|
let fileName: string;
|
||||||
let type: string;
|
let type: string;
|
||||||
|
@ -283,7 +299,6 @@ class LoadNodesAndCredentialsClass {
|
||||||
await Promise.all(loadPromises);
|
await Promise.all(loadPromises);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads nodes and credentials from the package with the given name
|
* Loads nodes and credentials from the package with the given name
|
||||||
*
|
*
|
||||||
|
@ -301,10 +316,12 @@ class LoadNodesAndCredentialsClass {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let tempPath: string, filePath: string;
|
let tempPath: string;
|
||||||
|
let filePath: string;
|
||||||
|
|
||||||
// Read all node types
|
// Read all node types
|
||||||
let fileName: string, type: string;
|
let fileName: string;
|
||||||
|
let type: string;
|
||||||
if (packageFile.n8n.hasOwnProperty('nodes') && Array.isArray(packageFile.n8n.nodes)) {
|
if (packageFile.n8n.hasOwnProperty('nodes') && Array.isArray(packageFile.n8n.nodes)) {
|
||||||
for (filePath of packageFile.n8n.nodes) {
|
for (filePath of packageFile.n8n.nodes) {
|
||||||
tempPath = path.join(packagePath, filePath);
|
tempPath = path.join(packagePath, filePath);
|
||||||
|
@ -314,18 +331,21 @@ class LoadNodesAndCredentialsClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read all credential types
|
// Read all credential types
|
||||||
if (packageFile.n8n.hasOwnProperty('credentials') && Array.isArray(packageFile.n8n.credentials)) {
|
if (
|
||||||
|
packageFile.n8n.hasOwnProperty('credentials') &&
|
||||||
|
Array.isArray(packageFile.n8n.credentials)
|
||||||
|
) {
|
||||||
for (filePath of packageFile.n8n.credentials) {
|
for (filePath of packageFile.n8n.credentials) {
|
||||||
tempPath = path.join(packagePath, filePath);
|
tempPath = path.join(packagePath, filePath);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
[fileName, type] = path.parse(filePath).name.split('.');
|
[fileName, type] = path.parse(filePath).name.split('.');
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.loadCredentialsFromFile(fileName, tempPath);
|
this.loadCredentialsFromFile(fileName, tempPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let packagesInformationInstance: LoadNodesAndCredentialsClass | undefined;
|
let packagesInformationInstance: LoadNodesAndCredentialsClass | undefined;
|
||||||
|
|
||||||
export function LoadNodesAndCredentials(): LoadNodesAndCredentialsClass {
|
export function LoadNodesAndCredentials(): LoadNodesAndCredentialsClass {
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
import config = require('../config');
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
import * as winston from 'winston';
|
import * as winston from 'winston';
|
||||||
|
|
||||||
import {
|
import { IDataObject, ILogger, LogTypes } from 'n8n-workflow';
|
||||||
IDataObject,
|
|
||||||
ILogger,
|
|
||||||
LogTypes,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
import * as callsites from 'callsites';
|
import * as callsites from 'callsites';
|
||||||
import { basename } from 'path';
|
import { basename } from 'path';
|
||||||
|
import config = require('../config');
|
||||||
|
|
||||||
class Logger implements ILogger {
|
class Logger implements ILogger {
|
||||||
private logger: winston.Logger;
|
private logger: winston.Logger;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
const level = config.get('logs.level');
|
const level = config.get('logs.level');
|
||||||
const output = (config.get('logs.output') as string).split(',').map(output => output.trim());
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
|
const output = (config.get('logs.output') as string).split(',').map((output) => output.trim());
|
||||||
|
|
||||||
this.logger = winston.createLogger({
|
this.logger = winston.createLogger({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
level,
|
level,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -28,18 +28,22 @@ class Logger implements ILogger {
|
||||||
winston.format.metadata(),
|
winston.format.metadata(),
|
||||||
winston.format.timestamp(),
|
winston.format.timestamp(),
|
||||||
winston.format.colorize({ all: true }),
|
winston.format.colorize({ all: true }),
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
winston.format.printf(({ level, message, timestamp, metadata }) => {
|
winston.format.printf(({ level, message, timestamp, metadata }) => {
|
||||||
return `${timestamp} | ${level.padEnd(18)} | ${message}` + (Object.keys(metadata).length ? ` ${JSON.stringify(metadata)}` : '');
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
}) as winston.Logform.Format
|
return `${timestamp} | ${level.padEnd(18)} | ${message}${
|
||||||
|
Object.keys(metadata).length ? ` ${JSON.stringify(metadata)}` : ''
|
||||||
|
}`;
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
format = winston.format.printf(({ message }) => message) as winston.Logform.Format;
|
format = winston.format.printf(({ message }) => message);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.add(
|
this.logger.add(
|
||||||
new winston.transports.Console({
|
new winston.transports.Console({
|
||||||
format,
|
format,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,15 +51,15 @@ class Logger implements ILogger {
|
||||||
const fileLogFormat = winston.format.combine(
|
const fileLogFormat = winston.format.combine(
|
||||||
winston.format.timestamp(),
|
winston.format.timestamp(),
|
||||||
winston.format.metadata(),
|
winston.format.metadata(),
|
||||||
winston.format.json()
|
winston.format.json(),
|
||||||
);
|
);
|
||||||
this.logger.add(
|
this.logger.add(
|
||||||
new winston.transports.File({
|
new winston.transports.File({
|
||||||
filename: config.get('logs.file.location'),
|
filename: config.get('logs.file.location'),
|
||||||
format: fileLogFormat,
|
format: fileLogFormat,
|
||||||
maxsize: config.get('logs.file.fileSizeMax') as number * 1048576, // config * 1mb
|
maxsize: (config.get('logs.file.fileSizeMax') as number) * 1048576, // config * 1mb
|
||||||
maxFiles: config.get('logs.file.fileCountMax'),
|
maxFiles: config.get('logs.file.fileCountMax'),
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,13 +74,14 @@ class Logger implements ILogger {
|
||||||
// We are in runtime, so it means we are looking at compiled js files
|
// We are in runtime, so it means we are looking at compiled js files
|
||||||
const logDetails = {} as IDataObject;
|
const logDetails = {} as IDataObject;
|
||||||
if (callsite[2] !== undefined) {
|
if (callsite[2] !== undefined) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
logDetails.file = basename(callsite[2].getFileName() || '');
|
logDetails.file = basename(callsite[2].getFileName() || '');
|
||||||
const functionName = callsite[2].getFunctionName();
|
const functionName = callsite[2].getFunctionName();
|
||||||
if (functionName) {
|
if (functionName) {
|
||||||
logDetails.function = functionName;
|
logDetails.function = functionName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.logger.log(type, message, {...meta, ...logDetails});
|
this.logger.log(type, message, { ...meta, ...logDetails });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convenience methods below
|
// Convenience methods below
|
||||||
|
@ -100,11 +105,11 @@ class Logger implements ILogger {
|
||||||
warn(message: string, meta: object = {}) {
|
warn(message: string, meta: object = {}) {
|
||||||
this.log('warn', message, meta);
|
this.log('warn', message, meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let activeLoggerInstance: Logger | undefined;
|
let activeLoggerInstance: Logger | undefined;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
export function getLogger() {
|
export function getLogger() {
|
||||||
if (activeLoggerInstance === undefined) {
|
if (activeLoggerInstance === undefined) {
|
||||||
activeLoggerInstance = new Logger();
|
activeLoggerInstance = new Logger();
|
||||||
|
|
|
@ -1,24 +1,21 @@
|
||||||
import {
|
import { INodeType, INodeTypeData, INodeTypes, NodeHelpers } from 'n8n-workflow';
|
||||||
INodeType,
|
|
||||||
INodeTypeData,
|
|
||||||
INodeTypes,
|
|
||||||
NodeHelpers,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
|
|
||||||
class NodeTypesClass implements INodeTypes {
|
class NodeTypesClass implements INodeTypes {
|
||||||
|
|
||||||
nodeTypes: INodeTypeData = {};
|
nodeTypes: INodeTypeData = {};
|
||||||
|
|
||||||
|
|
||||||
async init(nodeTypes: INodeTypeData): Promise<void> {
|
async init(nodeTypes: INodeTypeData): Promise<void> {
|
||||||
// Some nodeTypes need to get special parameters applied like the
|
// Some nodeTypes need to get special parameters applied like the
|
||||||
// polling nodes the polling times
|
// polling nodes the polling times
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const nodeTypeData of Object.values(nodeTypes)) {
|
for (const nodeTypeData of Object.values(nodeTypes)) {
|
||||||
const applyParameters = NodeHelpers.getSpecialNodeParameters(nodeTypeData.type);
|
const applyParameters = NodeHelpers.getSpecialNodeParameters(nodeTypeData.type);
|
||||||
|
|
||||||
if (applyParameters.length) {
|
if (applyParameters.length) {
|
||||||
nodeTypeData.type.description.properties.unshift.apply(nodeTypeData.type.description.properties, applyParameters);
|
// eslint-disable-next-line prefer-spread
|
||||||
|
nodeTypeData.type.description.properties.unshift.apply(
|
||||||
|
nodeTypeData.type.description.properties,
|
||||||
|
applyParameters,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.nodeTypes = nodeTypes;
|
this.nodeTypes = nodeTypes;
|
||||||
|
@ -36,10 +33,9 @@ class NodeTypesClass implements INodeTypes {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let nodeTypesInstance: NodeTypesClass | undefined;
|
let nodeTypesInstance: NodeTypesClass | undefined;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
export function NodeTypes(): NodeTypesClass {
|
export function NodeTypes(): NodeTypesClass {
|
||||||
if (nodeTypesInstance === undefined) {
|
if (nodeTypesInstance === undefined) {
|
||||||
nodeTypesInstance = new NodeTypesClass();
|
nodeTypesInstance = new NodeTypesClass();
|
||||||
|
|
|
@ -1,24 +1,22 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import * as sseChannel from 'sse-channel';
|
import * as sseChannel from 'sse-channel';
|
||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
|
|
||||||
import {
|
import { LoggerProxy as Logger } from 'n8n-workflow';
|
||||||
IPushData,
|
// eslint-disable-next-line import/no-cycle
|
||||||
IPushDataType,
|
import { IPushData, IPushDataType } from '.';
|
||||||
} from '.';
|
|
||||||
|
|
||||||
import {
|
|
||||||
LoggerProxy as Logger,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
export class Push {
|
export class Push {
|
||||||
private channel: sseChannel;
|
private channel: sseChannel;
|
||||||
|
|
||||||
private connections: {
|
private connections: {
|
||||||
[key: string]: express.Response;
|
[key: string]: express.Response;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, new-cap
|
||||||
this.channel = new sseChannel({
|
this.channel = new sseChannel({
|
||||||
cors: {
|
cors: {
|
||||||
// Allow access also from frontend when developing
|
// Allow access also from frontend when developing
|
||||||
|
@ -26,6 +24,7 @@ export class Push {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
this.channel.on('disconnect', (channel: string, res: express.Response) => {
|
this.channel.on('disconnect', (channel: string, res: express.Response) => {
|
||||||
if (res.req !== undefined) {
|
if (res.req !== undefined) {
|
||||||
Logger.debug(`Remove editor-UI session`, { sessionId: res.req.query.sessionId });
|
Logger.debug(`Remove editor-UI session`, { sessionId: res.req.query.sessionId });
|
||||||
|
@ -34,7 +33,6 @@ export class Push {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a new push connection
|
* Adds a new push connection
|
||||||
*
|
*
|
||||||
|
@ -43,6 +41,7 @@ export class Push {
|
||||||
* @param {express.Response} res The response
|
* @param {express.Response} res The response
|
||||||
* @memberof Push
|
* @memberof Push
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
add(sessionId: string, req: express.Request, res: express.Response) {
|
add(sessionId: string, req: express.Request, res: express.Response) {
|
||||||
Logger.debug(`Add editor-UI session`, { sessionId });
|
Logger.debug(`Add editor-UI session`, { sessionId });
|
||||||
|
|
||||||
|
@ -57,7 +56,6 @@ export class Push {
|
||||||
this.channel.addClient(req, res);
|
this.channel.addClient(req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends data to the client which is connected via a specific session
|
* Sends data to the client which is connected via a specific session
|
||||||
*
|
*
|
||||||
|
@ -67,9 +65,8 @@ export class Push {
|
||||||
* @memberof Push
|
* @memberof Push
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
|
||||||
|
send(type: IPushDataType, data: any, sessionId?: string) {
|
||||||
send(type: IPushDataType, data: any, sessionId?: string) { // tslint:disable-line:no-any
|
|
||||||
if (sessionId !== undefined && this.connections[sessionId] === undefined) {
|
if (sessionId !== undefined && this.connections[sessionId] === undefined) {
|
||||||
Logger.error(`The session "${sessionId}" is not registred.`, { sessionId });
|
Logger.error(`The session "${sessionId}" is not registred.`, { sessionId });
|
||||||
return;
|
return;
|
||||||
|
@ -79,6 +76,7 @@ export class Push {
|
||||||
|
|
||||||
const sendData: IPushData = {
|
const sendData: IPushData = {
|
||||||
type,
|
type,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
data,
|
data,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -89,7 +87,6 @@ export class Push {
|
||||||
// Send only to a specific client
|
// Send only to a specific client
|
||||||
this.channel.send(JSON.stringify(sendData), [this.connections[sessionId]]);
|
this.channel.send(JSON.stringify(sendData), [this.connections[sessionId]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
import * as Bull from 'bull';
|
import * as Bull from 'bull';
|
||||||
import * as config from '../config';
|
import * as config from '../config';
|
||||||
|
// eslint-disable-next-line import/no-cycle
|
||||||
import { IBullJobData } from './Interfaces';
|
import { IBullJobData } from './Interfaces';
|
||||||
|
|
||||||
export class Queue {
|
export class Queue {
|
||||||
private jobQueue: Bull.Queue;
|
private jobQueue: Bull.Queue;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const prefix = config.get('queue.bull.prefix') as string;
|
const prefix = config.get('queue.bull.prefix') as string;
|
||||||
const redisOptions = config.get('queue.bull.redis') as object;
|
const redisOptions = config.get('queue.bull.redis') as object;
|
||||||
// Disabling ready check is necessary as it allows worker to
|
// Disabling ready check is necessary as it allows worker to
|
||||||
// quickly reconnect to Redis if Redis crashes or is unreachable
|
// quickly reconnect to Redis if Redis crashes or is unreachable
|
||||||
// for some time. With it enabled, worker might take minutes to realize
|
// for some time. With it enabled, worker might take minutes to realize
|
||||||
// redis is back up and resume working.
|
// redis is back up and resume working.
|
||||||
|
@ -16,25 +17,25 @@ export class Queue {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.jobQueue = new Bull('jobs', { prefix, redis: redisOptions, enableReadyCheck: false });
|
this.jobQueue = new Bull('jobs', { prefix, redis: redisOptions, enableReadyCheck: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
async add(jobData: IBullJobData, jobOptions: object): Promise<Bull.Job> {
|
async add(jobData: IBullJobData, jobOptions: object): Promise<Bull.Job> {
|
||||||
return await this.jobQueue.add(jobData,jobOptions);
|
return this.jobQueue.add(jobData, jobOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getJob(jobId: Bull.JobId): Promise<Bull.Job | null> {
|
async getJob(jobId: Bull.JobId): Promise<Bull.Job | null> {
|
||||||
return await this.jobQueue.getJob(jobId);
|
return this.jobQueue.getJob(jobId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getJobs(jobTypes: Bull.JobStatus[]): Promise<Bull.Job[]> {
|
async getJobs(jobTypes: Bull.JobStatus[]): Promise<Bull.Job[]> {
|
||||||
return await this.jobQueue.getJobs(jobTypes);
|
return this.jobQueue.getJobs(jobTypes);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBullObjectInstance(): Bull.Queue {
|
getBullObjectInstance(): Bull.Queue {
|
||||||
return this.jobQueue;
|
return this.jobQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param job A Bull.Job instance
|
* @param job A Bull.Job instance
|
||||||
* @returns boolean true if we were able to securely stop the job
|
* @returns boolean true if we were able to securely stop the job
|
||||||
*/
|
*/
|
||||||
|
@ -43,15 +44,15 @@ export class Queue {
|
||||||
// Job is already running so tell it to stop
|
// Job is already running so tell it to stop
|
||||||
await job.progress(-1);
|
await job.progress(-1);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
|
||||||
// Job did not get started yet so remove from queue
|
|
||||||
try {
|
|
||||||
await job.remove();
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
await job.progress(-1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// Job did not get started yet so remove from queue
|
||||||
|
try {
|
||||||
|
await job.remove();
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
await job.progress(-1);
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,6 +63,6 @@ export function getInstance(): Queue {
|
||||||
if (activeQueueInstance === undefined) {
|
if (activeQueueInstance === undefined) {
|
||||||
activeQueueInstance = new Queue();
|
activeQueueInstance = new Queue();
|
||||||
}
|
}
|
||||||
|
|
||||||
return activeQueueInstance;
|
return activeQueueInstance;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { parse, stringify } from 'flatted';
|
import { parse, stringify } from 'flatted';
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-cycle
|
||||||
import {
|
import {
|
||||||
IExecutionDb,
|
IExecutionDb,
|
||||||
IExecutionFlatted,
|
IExecutionFlatted,
|
||||||
IExecutionFlattedDb,
|
IExecutionFlattedDb,
|
||||||
IExecutionResponse,
|
IExecutionResponse,
|
||||||
IWorkflowDb,
|
IWorkflowDb,
|
||||||
} from './';
|
} from '.';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Special Error which allows to return also an error code and http status code
|
* Special Error which allows to return also an error code and http status code
|
||||||
|
@ -17,7 +23,6 @@ import {
|
||||||
* @extends {Error}
|
* @extends {Error}
|
||||||
*/
|
*/
|
||||||
export class ResponseError extends Error {
|
export class ResponseError extends Error {
|
||||||
|
|
||||||
// The HTTP status code of response
|
// The HTTP status code of response
|
||||||
httpStatusCode?: number;
|
httpStatusCode?: number;
|
||||||
|
|
||||||
|
@ -35,7 +40,7 @@ export class ResponseError extends Error {
|
||||||
* @param {string} [hint] The error hint to provide a context (webhook related)
|
* @param {string} [hint] The error hint to provide a context (webhook related)
|
||||||
* @memberof ResponseError
|
* @memberof ResponseError
|
||||||
*/
|
*/
|
||||||
constructor(message: string, errorCode?: number, httpStatusCode?: number, hint?:string) {
|
constructor(message: string, errorCode?: number, httpStatusCode?: number, hint?: string) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = 'ResponseError';
|
this.name = 'ResponseError';
|
||||||
|
|
||||||
|
@ -51,21 +56,23 @@ export class ResponseError extends Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function basicAuthAuthorizationError(resp: Response, realm: string, message?: string) {
|
export function basicAuthAuthorizationError(resp: Response, realm: string, message?: string) {
|
||||||
resp.statusCode = 401;
|
resp.statusCode = 401;
|
||||||
resp.setHeader('WWW-Authenticate', `Basic realm="${realm}"`);
|
resp.setHeader('WWW-Authenticate', `Basic realm="${realm}"`);
|
||||||
resp.json({code: resp.statusCode, message});
|
resp.json({ code: resp.statusCode, message });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function jwtAuthAuthorizationError(resp: Response, message?: string) {
|
export function jwtAuthAuthorizationError(resp: Response, message?: string) {
|
||||||
resp.statusCode = 403;
|
resp.statusCode = 403;
|
||||||
resp.json({code: resp.statusCode, message});
|
resp.json({ code: resp.statusCode, message });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sendSuccessResponse(
|
||||||
export function sendSuccessResponse(res: Response, data: any, raw?: boolean, responseCode?: number) { // tslint:disable-line:no-any
|
res: Response,
|
||||||
|
data: any,
|
||||||
|
raw?: boolean,
|
||||||
|
responseCode?: number,
|
||||||
|
) {
|
||||||
if (responseCode !== undefined) {
|
if (responseCode !== undefined) {
|
||||||
res.status(responseCode);
|
res.status(responseCode);
|
||||||
}
|
}
|
||||||
|
@ -83,7 +90,6 @@ export function sendSuccessResponse(res: Response, data: any, raw?: boolean, res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function sendErrorResponse(res: Response, error: ResponseError) {
|
export function sendErrorResponse(res: Response, error: ResponseError) {
|
||||||
let httpStatusCode = 500;
|
let httpStatusCode = 500;
|
||||||
if (error.httpStatusCode) {
|
if (error.httpStatusCode) {
|
||||||
|
@ -122,7 +128,6 @@ export function sendErrorResponse(res: Response, error: ResponseError) {
|
||||||
res.status(httpStatusCode).json(response);
|
res.status(httpStatusCode).json(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A helper function which does not just allow to return Promises it also makes sure that
|
* A helper function which does not just allow to return Promises it also makes sure that
|
||||||
* all the responses have the same format
|
* all the responses have the same format
|
||||||
|
@ -133,8 +138,7 @@ export function sendErrorResponse(res: Response, error: ResponseError) {
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function send(processFunction: (req: Request, res: Response) => Promise<any>) { // tslint:disable-line:no-any
|
export function send(processFunction: (req: Request, res: Response) => Promise<any>) {
|
||||||
|
|
||||||
return async (req: Request, res: Response) => {
|
return async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const data = await processFunction(req, res);
|
const data = await processFunction(req, res);
|
||||||
|
@ -148,7 +152,6 @@ export function send(processFunction: (req: Request, res: Response) => Promise<a
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flattens the Execution data.
|
* Flattens the Execution data.
|
||||||
* As it contains a lot of references which normally would be saved as duplicate data
|
* As it contains a lot of references which normally would be saved as duplicate data
|
||||||
|
@ -160,32 +163,34 @@ export function send(processFunction: (req: Request, res: Response) => Promise<a
|
||||||
*/
|
*/
|
||||||
export function flattenExecutionData(fullExecutionData: IExecutionDb): IExecutionFlatted {
|
export function flattenExecutionData(fullExecutionData: IExecutionDb): IExecutionFlatted {
|
||||||
// Flatten the data
|
// Flatten the data
|
||||||
const returnData: IExecutionFlatted = Object.assign({}, {
|
const returnData: IExecutionFlatted = {
|
||||||
data: stringify(fullExecutionData.data),
|
data: stringify(fullExecutionData.data),
|
||||||
mode: fullExecutionData.mode,
|
mode: fullExecutionData.mode,
|
||||||
|
// @ts-ignore
|
||||||
|
waitTill: fullExecutionData.waitTill,
|
||||||
startedAt: fullExecutionData.startedAt,
|
startedAt: fullExecutionData.startedAt,
|
||||||
stoppedAt: fullExecutionData.stoppedAt,
|
stoppedAt: fullExecutionData.stoppedAt,
|
||||||
finished: fullExecutionData.finished ? fullExecutionData.finished : false,
|
finished: fullExecutionData.finished ? fullExecutionData.finished : false,
|
||||||
workflowId: fullExecutionData.workflowId,
|
workflowId: fullExecutionData.workflowId,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
workflowData: fullExecutionData.workflowData!,
|
workflowData: fullExecutionData.workflowData!,
|
||||||
});
|
};
|
||||||
|
|
||||||
if (fullExecutionData.id !== undefined) {
|
if (fullExecutionData.id !== undefined) {
|
||||||
returnData.id = fullExecutionData.id!.toString();
|
returnData.id = fullExecutionData.id.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fullExecutionData.retryOf !== undefined) {
|
if (fullExecutionData.retryOf !== undefined) {
|
||||||
returnData.retryOf = fullExecutionData.retryOf!.toString();
|
returnData.retryOf = fullExecutionData.retryOf.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fullExecutionData.retrySuccessId !== undefined) {
|
if (fullExecutionData.retrySuccessId !== undefined) {
|
||||||
returnData.retrySuccessId = fullExecutionData.retrySuccessId!.toString();
|
returnData.retrySuccessId = fullExecutionData.retrySuccessId.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return returnData;
|
return returnData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unflattens the Execution data.
|
* Unflattens the Execution data.
|
||||||
*
|
*
|
||||||
|
@ -194,17 +199,17 @@ export function flattenExecutionData(fullExecutionData: IExecutionDb): IExecutio
|
||||||
* @returns {IExecutionResponse}
|
* @returns {IExecutionResponse}
|
||||||
*/
|
*/
|
||||||
export function unflattenExecutionData(fullExecutionData: IExecutionFlattedDb): IExecutionResponse {
|
export function unflattenExecutionData(fullExecutionData: IExecutionFlattedDb): IExecutionResponse {
|
||||||
|
const returnData: IExecutionResponse = {
|
||||||
const returnData: IExecutionResponse = Object.assign({}, {
|
|
||||||
id: fullExecutionData.id.toString(),
|
id: fullExecutionData.id.toString(),
|
||||||
workflowData: fullExecutionData.workflowData as IWorkflowDb,
|
workflowData: fullExecutionData.workflowData as IWorkflowDb,
|
||||||
data: parse(fullExecutionData.data),
|
data: parse(fullExecutionData.data),
|
||||||
mode: fullExecutionData.mode,
|
mode: fullExecutionData.mode,
|
||||||
|
waitTill: fullExecutionData.waitTill ? fullExecutionData.waitTill : undefined,
|
||||||
startedAt: fullExecutionData.startedAt,
|
startedAt: fullExecutionData.startedAt,
|
||||||
stoppedAt: fullExecutionData.stoppedAt,
|
stoppedAt: fullExecutionData.stoppedAt,
|
||||||
finished: fullExecutionData.finished ? fullExecutionData.finished : false,
|
finished: fullExecutionData.finished ? fullExecutionData.finished : false,
|
||||||
workflowId: fullExecutionData.workflowId,
|
workflowId: fullExecutionData.workflowId,
|
||||||
});
|
};
|
||||||
|
|
||||||
return returnData;
|
return returnData;
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,18 +1,14 @@
|
||||||
import { getConnection } from "typeorm";
|
/* eslint-disable no-param-reassign */
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
/* eslint-disable import/no-cycle */
|
||||||
|
import { getConnection } from 'typeorm';
|
||||||
import { validate } from 'class-validator';
|
import { validate } from 'class-validator';
|
||||||
|
|
||||||
import {
|
import { ResponseHelper } from '.';
|
||||||
ResponseHelper,
|
|
||||||
} from ".";
|
|
||||||
|
|
||||||
import {
|
import { TagEntity } from './databases/entities/TagEntity';
|
||||||
TagEntity,
|
|
||||||
} from "./databases/entities/TagEntity";
|
|
||||||
|
|
||||||
import {
|
|
||||||
ITagWithCountDb,
|
|
||||||
} from "./Interfaces";
|
|
||||||
|
|
||||||
|
import { ITagWithCountDb } from './Interfaces';
|
||||||
|
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
// utils
|
// utils
|
||||||
|
@ -29,7 +25,7 @@ export function sortByRequestOrder(tagsDb: TagEntity[], tagIds: string[]) {
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as { [key: string]: TagEntity });
|
}, {} as { [key: string]: TagEntity });
|
||||||
|
|
||||||
return tagIds.map(tagId => tagMap[tagId]);
|
return tagIds.map((tagId) => tagMap[tagId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
|
@ -43,6 +39,7 @@ export async function validateTag(newTag: TagEntity) {
|
||||||
const errors = await validate(newTag);
|
const errors = await validate(newTag);
|
||||||
|
|
||||||
if (errors.length) {
|
if (errors.length) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const validationErrorMessage = Object.values(errors[0].constraints!)[0];
|
const validationErrorMessage = Object.values(errors[0].constraints!)[0];
|
||||||
throw new ResponseHelper.ResponseError(validationErrorMessage, undefined, 400);
|
throw new ResponseHelper.ResponseError(validationErrorMessage, undefined, 400);
|
||||||
}
|
}
|
||||||
|
@ -64,23 +61,30 @@ export function throwDuplicateEntryError(error: Error) {
|
||||||
/**
|
/**
|
||||||
* Retrieve all tags and the number of workflows each tag is related to.
|
* Retrieve all tags and the number of workflows each tag is related to.
|
||||||
*/
|
*/
|
||||||
export function getTagsWithCountDb(tablePrefix: string): Promise<ITagWithCountDb[]> {
|
export async function getTagsWithCountDb(tablePrefix: string): Promise<ITagWithCountDb[]> {
|
||||||
return getConnection()
|
return getConnection()
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
.select(`${tablePrefix}tag_entity.id`, 'id')
|
.select(`${tablePrefix}tag_entity.id`, 'id')
|
||||||
.addSelect(`${tablePrefix}tag_entity.name`, 'name')
|
.addSelect(`${tablePrefix}tag_entity.name`, 'name')
|
||||||
.addSelect(`COUNT(${tablePrefix}workflows_tags.workflowId)`, 'usageCount')
|
.addSelect(`COUNT(${tablePrefix}workflows_tags.workflowId)`, 'usageCount')
|
||||||
.from(`${tablePrefix}tag_entity`, 'tag_entity')
|
.from(`${tablePrefix}tag_entity`, 'tag_entity')
|
||||||
.leftJoin(`${tablePrefix}workflows_tags`, 'workflows_tags', `${tablePrefix}workflows_tags.tagId = tag_entity.id`)
|
.leftJoin(
|
||||||
.groupBy(`${tablePrefix}tag_entity.id`)
|
`${tablePrefix}workflows_tags`,
|
||||||
.getRawMany()
|
'workflows_tags',
|
||||||
.then(tagsWithCount => {
|
`${tablePrefix}workflows_tags.tagId = tag_entity.id`,
|
||||||
tagsWithCount.forEach(tag => {
|
)
|
||||||
tag.id = tag.id.toString();
|
.groupBy(`${tablePrefix}tag_entity.id`)
|
||||||
tag.usageCount = Number(tag.usageCount);
|
.getRawMany()
|
||||||
|
.then((tagsWithCount) => {
|
||||||
|
tagsWithCount.forEach((tag) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
||||||
|
tag.id = tag.id.toString();
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
tag.usageCount = Number(tag.usageCount);
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
|
return tagsWithCount;
|
||||||
});
|
});
|
||||||
return tagsWithCount;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
|
@ -90,19 +94,19 @@ export function getTagsWithCountDb(tablePrefix: string): Promise<ITagWithCountDb
|
||||||
/**
|
/**
|
||||||
* Relate a workflow to one or more tags.
|
* Relate a workflow to one or more tags.
|
||||||
*/
|
*/
|
||||||
export function createRelations(workflowId: string, tagIds: string[], tablePrefix: string) {
|
export async function createRelations(workflowId: string, tagIds: string[], tablePrefix: string) {
|
||||||
return getConnection()
|
return getConnection()
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
.insert()
|
.insert()
|
||||||
.into(`${tablePrefix}workflows_tags`)
|
.into(`${tablePrefix}workflows_tags`)
|
||||||
.values(tagIds.map(tagId => ({ workflowId, tagId })))
|
.values(tagIds.map((tagId) => ({ workflowId, tagId })))
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove all tags for a workflow during a tag update operation.
|
* Remove all tags for a workflow during a tag update operation.
|
||||||
*/
|
*/
|
||||||
export function removeRelations(workflowId: string, tablePrefix: string) {
|
export async function removeRelations(workflowId: string, tablePrefix: string) {
|
||||||
return getConnection()
|
return getConnection()
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
.delete()
|
.delete()
|
||||||
|
|
|
@ -1,16 +1,9 @@
|
||||||
|
/* eslint-disable consistent-return */
|
||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
|
/* eslint-disable no-param-reassign */
|
||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
|
|
||||||
import {
|
import { ActiveWebhooks } from 'n8n-core';
|
||||||
IResponseCallbackData,
|
|
||||||
IWorkflowDb,
|
|
||||||
Push,
|
|
||||||
ResponseHelper,
|
|
||||||
WebhookHelpers,
|
|
||||||
} from './';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ActiveWebhooks,
|
|
||||||
} from 'n8n-core';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IWebhookData,
|
IWebhookData,
|
||||||
|
@ -20,28 +13,28 @@ import {
|
||||||
WorkflowActivateMode,
|
WorkflowActivateMode,
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
// eslint-disable-next-line import/no-cycle
|
||||||
|
import { IResponseCallbackData, IWorkflowDb, Push, ResponseHelper, WebhookHelpers } from '.';
|
||||||
|
|
||||||
const WEBHOOK_TEST_UNREGISTERED_HINT = `Click the 'Execute workflow' button on the canvas, then try again. (In test mode, the webhook only works for one call after you click this button)`;
|
const WEBHOOK_TEST_UNREGISTERED_HINT = `Click the 'Execute workflow' button on the canvas, then try again. (In test mode, the webhook only works for one call after you click this button)`;
|
||||||
|
|
||||||
export class TestWebhooks {
|
export class TestWebhooks {
|
||||||
|
|
||||||
private testWebhookData: {
|
private testWebhookData: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
timeout: NodeJS.Timeout,
|
timeout: NodeJS.Timeout;
|
||||||
workflowData: IWorkflowDb;
|
workflowData: IWorkflowDb;
|
||||||
workflow: Workflow;
|
workflow: Workflow;
|
||||||
};
|
};
|
||||||
} = {};
|
} = {};
|
||||||
private activeWebhooks: ActiveWebhooks | null = null;
|
|
||||||
|
|
||||||
|
private activeWebhooks: ActiveWebhooks | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.activeWebhooks = new ActiveWebhooks();
|
this.activeWebhooks = new ActiveWebhooks();
|
||||||
this.activeWebhooks.testWebhooks = true;
|
this.activeWebhooks.testWebhooks = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes a test-webhook and returns the data. It also makes sure that the
|
* Executes a test-webhook and returns the data. It also makes sure that the
|
||||||
* data gets additionally send to the UI. After the request got handled it
|
* data gets additionally send to the UI. After the request got handled it
|
||||||
|
@ -54,7 +47,12 @@ export class TestWebhooks {
|
||||||
* @returns {Promise<object>}
|
* @returns {Promise<object>}
|
||||||
* @memberof TestWebhooks
|
* @memberof TestWebhooks
|
||||||
*/
|
*/
|
||||||
async callTestWebhook(httpMethod: WebhookHttpMethod, path: string, request: express.Request, response: express.Response): Promise<IResponseCallbackData> {
|
async callTestWebhook(
|
||||||
|
httpMethod: WebhookHttpMethod,
|
||||||
|
path: string,
|
||||||
|
request: express.Request,
|
||||||
|
response: express.Response,
|
||||||
|
): Promise<IResponseCallbackData> {
|
||||||
// Reset request parameters
|
// Reset request parameters
|
||||||
request.params = {};
|
request.params = {};
|
||||||
|
|
||||||
|
@ -69,10 +67,16 @@ export class TestWebhooks {
|
||||||
if (webhookData === undefined) {
|
if (webhookData === undefined) {
|
||||||
const pathElements = path.split('/');
|
const pathElements = path.split('/');
|
||||||
const webhookId = pathElements.shift();
|
const webhookId = pathElements.shift();
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
webhookData = this.activeWebhooks!.get(httpMethod, pathElements.join('/'), webhookId);
|
webhookData = this.activeWebhooks!.get(httpMethod, pathElements.join('/'), webhookId);
|
||||||
if (webhookData === undefined) {
|
if (webhookData === undefined) {
|
||||||
// The requested webhook is not registered
|
// The requested webhook is not registered
|
||||||
throw new ResponseHelper.ResponseError(`The requested webhook "${httpMethod} ${path}" is not registered.`, 404, 404, WEBHOOK_TEST_UNREGISTERED_HINT);
|
throw new ResponseHelper.ResponseError(
|
||||||
|
`The requested webhook "${httpMethod} ${path}" is not registered.`,
|
||||||
|
404,
|
||||||
|
404,
|
||||||
|
WEBHOOK_TEST_UNREGISTERED_HINT,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
path = webhookData.path;
|
path = webhookData.path;
|
||||||
|
@ -85,15 +89,24 @@ export class TestWebhooks {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const webhookKey = this.activeWebhooks!.getWebhookKey(webhookData.httpMethod, webhookData.path, webhookData.webhookId) + `|${webhookData.workflowId}`;
|
const webhookKey = `${this.activeWebhooks!.getWebhookKey(
|
||||||
|
webhookData.httpMethod,
|
||||||
|
webhookData.path,
|
||||||
|
webhookData.webhookId,
|
||||||
|
)}|${webhookData.workflowId}`;
|
||||||
|
|
||||||
// TODO: Clean that duplication up one day and improve code generally
|
// TODO: Clean that duplication up one day and improve code generally
|
||||||
if (this.testWebhookData[webhookKey] === undefined) {
|
if (this.testWebhookData[webhookKey] === undefined) {
|
||||||
// The requested webhook is not registered
|
// The requested webhook is not registered
|
||||||
throw new ResponseHelper.ResponseError(`The requested webhook "${httpMethod} ${path}" is not registered.`, 404, 404, WEBHOOK_TEST_UNREGISTERED_HINT);
|
throw new ResponseHelper.ResponseError(
|
||||||
|
`The requested webhook "${httpMethod} ${path}" is not registered.`,
|
||||||
|
404,
|
||||||
|
404,
|
||||||
|
WEBHOOK_TEST_UNREGISTERED_HINT,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const workflow = this.testWebhookData[webhookKey].workflow;
|
const { workflow } = this.testWebhookData[webhookKey];
|
||||||
|
|
||||||
// Get the node which has the webhook defined to know where to start from and to
|
// Get the node which has the webhook defined to know where to start from and to
|
||||||
// get additional data
|
// get additional data
|
||||||
|
@ -102,15 +115,28 @@ export class TestWebhooks {
|
||||||
throw new ResponseHelper.ResponseError('Could not find node to process webhook.', 404, 404);
|
throw new ResponseHelper.ResponseError('Could not find node to process webhook.', 404, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-async-promise-executor
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const executionMode = 'manual';
|
const executionMode = 'manual';
|
||||||
const executionId = await WebhookHelpers.executeWebhook(workflow, webhookData!, this.testWebhookData[webhookKey].workflowData, workflowStartNode, executionMode, this.testWebhookData[webhookKey].sessionId, request, response, (error: Error | null, data: IResponseCallbackData) => {
|
const executionId = await WebhookHelpers.executeWebhook(
|
||||||
if (error !== null) {
|
workflow,
|
||||||
return reject(error);
|
webhookData!,
|
||||||
}
|
this.testWebhookData[webhookKey].workflowData,
|
||||||
resolve(data);
|
workflowStartNode,
|
||||||
});
|
executionMode,
|
||||||
|
this.testWebhookData[webhookKey].sessionId,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
request,
|
||||||
|
response,
|
||||||
|
(error: Error | null, data: IResponseCallbackData) => {
|
||||||
|
if (error !== null) {
|
||||||
|
return reject(error);
|
||||||
|
}
|
||||||
|
resolve(data);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (executionId === undefined) {
|
if (executionId === undefined) {
|
||||||
// The workflow did not run as the request was probably setup related
|
// The workflow did not run as the request was probably setup related
|
||||||
|
@ -122,9 +148,12 @@ export class TestWebhooks {
|
||||||
// Inform editor-ui that webhook got received
|
// Inform editor-ui that webhook got received
|
||||||
if (this.testWebhookData[webhookKey].sessionId !== undefined) {
|
if (this.testWebhookData[webhookKey].sessionId !== undefined) {
|
||||||
const pushInstance = Push.getInstance();
|
const pushInstance = Push.getInstance();
|
||||||
pushInstance.send('testWebhookReceived', { workflowId: webhookData!.workflowId, executionId }, this.testWebhookData[webhookKey].sessionId!);
|
pushInstance.send(
|
||||||
|
'testWebhookReceived',
|
||||||
|
{ workflowId: webhookData!.workflowId, executionId },
|
||||||
|
this.testWebhookData[webhookKey].sessionId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Delete webhook also if an error is thrown
|
// Delete webhook also if an error is thrown
|
||||||
}
|
}
|
||||||
|
@ -132,6 +161,7 @@ export class TestWebhooks {
|
||||||
// Remove the webhook
|
// Remove the webhook
|
||||||
clearTimeout(this.testWebhookData[webhookKey].timeout);
|
clearTimeout(this.testWebhookData[webhookKey].timeout);
|
||||||
delete this.testWebhookData[webhookKey];
|
delete this.testWebhookData[webhookKey];
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.activeWebhooks!.removeWorkflow(workflow);
|
this.activeWebhooks!.removeWorkflow(workflow);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -140,18 +170,22 @@ export class TestWebhooks {
|
||||||
* Gets all request methods associated with a single test webhook
|
* Gets all request methods associated with a single test webhook
|
||||||
* @param path webhook path
|
* @param path webhook path
|
||||||
*/
|
*/
|
||||||
async getWebhookMethods(path : string) : Promise<string[]> {
|
async getWebhookMethods(path: string): Promise<string[]> {
|
||||||
const webhookMethods: string[] = this.activeWebhooks!.getWebhookMethods(path);
|
const webhookMethods: string[] = this.activeWebhooks!.getWebhookMethods(path);
|
||||||
|
|
||||||
if (webhookMethods === undefined) {
|
if (webhookMethods === undefined) {
|
||||||
// The requested webhook is not registered
|
// The requested webhook is not registered
|
||||||
throw new ResponseHelper.ResponseError(`The requested webhook "${path}" is not registered.`, 404, 404, WEBHOOK_TEST_UNREGISTERED_HINT);
|
throw new ResponseHelper.ResponseError(
|
||||||
|
`The requested webhook "${path}" is not registered.`,
|
||||||
|
404,
|
||||||
|
404,
|
||||||
|
WEBHOOK_TEST_UNREGISTERED_HINT,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return webhookMethods;
|
return webhookMethods;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if it has to wait for webhook data to execute the workflow. If yes it waits
|
* Checks if it has to wait for webhook data to execute the workflow. If yes it waits
|
||||||
* for it and resolves with the result of the workflow if not it simply resolves
|
* for it and resolves with the result of the workflow if not it simply resolves
|
||||||
|
@ -162,11 +196,23 @@ export class TestWebhooks {
|
||||||
* @returns {(Promise<IExecutionDb | undefined>)}
|
* @returns {(Promise<IExecutionDb | undefined>)}
|
||||||
* @memberof TestWebhooks
|
* @memberof TestWebhooks
|
||||||
*/
|
*/
|
||||||
async needsWebhookData(workflowData: IWorkflowDb, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode, sessionId?: string, destinationNode?: string): Promise<boolean> {
|
async needsWebhookData(
|
||||||
const webhooks = WebhookHelpers.getWorkflowWebhooks(workflow, additionalData, destinationNode);
|
workflowData: IWorkflowDb,
|
||||||
|
workflow: Workflow,
|
||||||
if (webhooks.length === 0) {
|
additionalData: IWorkflowExecuteAdditionalData,
|
||||||
// No Webhooks found
|
mode: WorkflowExecuteMode,
|
||||||
|
activation: WorkflowActivateMode,
|
||||||
|
sessionId?: string,
|
||||||
|
destinationNode?: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const webhooks = WebhookHelpers.getWorkflowWebhooks(
|
||||||
|
workflow,
|
||||||
|
additionalData,
|
||||||
|
destinationNode,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
if (!webhooks.find((webhook) => webhook.webhookDescription.restartWebhook !== true)) {
|
||||||
|
// No webhooks found to start a workflow
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,8 +227,13 @@ export class TestWebhooks {
|
||||||
|
|
||||||
let key: string;
|
let key: string;
|
||||||
const activatedKey: string[] = [];
|
const activatedKey: string[] = [];
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const webhookData of webhooks) {
|
for (const webhookData of webhooks) {
|
||||||
key = this.activeWebhooks!.getWebhookKey(webhookData.httpMethod, webhookData.path, webhookData.webhookId) + `|${workflowData.id}`;
|
key = `${this.activeWebhooks!.getWebhookKey(
|
||||||
|
webhookData.httpMethod,
|
||||||
|
webhookData.path,
|
||||||
|
webhookData.webhookId,
|
||||||
|
)}|${workflowData.id}`;
|
||||||
|
|
||||||
activatedKey.push(key);
|
activatedKey.push(key);
|
||||||
|
|
||||||
|
@ -194,17 +245,18 @@ export class TestWebhooks {
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await this.activeWebhooks!.add(workflow, webhookData, mode, activation);
|
await this.activeWebhooks!.add(workflow, webhookData, mode, activation);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
activatedKey.forEach(deleteKey => delete this.testWebhookData[deleteKey] );
|
activatedKey.forEach((deleteKey) => delete this.testWebhookData[deleteKey]);
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await this.activeWebhooks!.removeWorkflow(workflow);
|
await this.activeWebhooks!.removeWorkflow(workflow);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a test webhook of the workflow with the given id
|
* Removes a test webhook of the workflow with the given id
|
||||||
|
@ -215,10 +267,12 @@ export class TestWebhooks {
|
||||||
*/
|
*/
|
||||||
cancelTestWebhook(workflowId: string): boolean {
|
cancelTestWebhook(workflowId: string): boolean {
|
||||||
let foundWebhook = false;
|
let foundWebhook = false;
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const webhookKey of Object.keys(this.testWebhookData)) {
|
for (const webhookKey of Object.keys(this.testWebhookData)) {
|
||||||
const webhookData = this.testWebhookData[webhookKey];
|
const webhookData = this.testWebhookData[webhookKey];
|
||||||
|
|
||||||
if (webhookData.workflowData.id.toString() !== workflowId) {
|
if (webhookData.workflowData.id.toString() !== workflowId) {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,19 +282,24 @@ export class TestWebhooks {
|
||||||
if (this.testWebhookData[webhookKey].sessionId !== undefined) {
|
if (this.testWebhookData[webhookKey].sessionId !== undefined) {
|
||||||
try {
|
try {
|
||||||
const pushInstance = Push.getInstance();
|
const pushInstance = Push.getInstance();
|
||||||
pushInstance.send('testWebhookDeleted', { workflowId }, this.testWebhookData[webhookKey].sessionId!);
|
pushInstance.send(
|
||||||
|
'testWebhookDeleted',
|
||||||
|
{ workflowId },
|
||||||
|
this.testWebhookData[webhookKey].sessionId,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Could not inform editor, probably is not connected anymore. So sipmly go on.
|
// Could not inform editor, probably is not connected anymore. So sipmly go on.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const workflow = this.testWebhookData[webhookKey].workflow;
|
const { workflow } = this.testWebhookData[webhookKey];
|
||||||
|
|
||||||
// Remove the webhook
|
// Remove the webhook
|
||||||
delete this.testWebhookData[webhookKey];
|
delete this.testWebhookData[webhookKey];
|
||||||
|
|
||||||
if (foundWebhook === false) {
|
if (!foundWebhook) {
|
||||||
// As it removes all webhooks of the workflow execute only once
|
// As it removes all webhooks of the workflow execute only once
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.activeWebhooks!.removeWorkflow(workflow);
|
this.activeWebhooks!.removeWorkflow(workflow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -250,7 +309,6 @@ export class TestWebhooks {
|
||||||
return foundWebhook;
|
return foundWebhook;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes all the currently active test webhooks
|
* Removes all the currently active test webhooks
|
||||||
*/
|
*/
|
||||||
|
@ -261,6 +319,7 @@ export class TestWebhooks {
|
||||||
|
|
||||||
let workflow: Workflow;
|
let workflow: Workflow;
|
||||||
const workflows: Workflow[] = [];
|
const workflows: Workflow[] = [];
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const webhookKey of Object.keys(this.testWebhookData)) {
|
for (const webhookKey of Object.keys(this.testWebhookData)) {
|
||||||
workflow = this.testWebhookData[webhookKey].workflow;
|
workflow = this.testWebhookData[webhookKey].workflow;
|
||||||
workflows.push(workflow);
|
workflows.push(workflow);
|
||||||
|
|
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';
|
import * as express from 'express';
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
|
|
||||||
import {
|
import { BINARY_ENCODING, NodeExecuteFunctions } from 'n8n-core';
|
||||||
ActiveExecutions,
|
|
||||||
ExternalHooks,
|
|
||||||
GenericHelpers,
|
|
||||||
IExecutionDb,
|
|
||||||
IResponseCallbackData,
|
|
||||||
IWorkflowDb,
|
|
||||||
IWorkflowExecutionDataProcess,
|
|
||||||
ResponseHelper,
|
|
||||||
WorkflowCredentials,
|
|
||||||
WorkflowExecuteAdditionalData,
|
|
||||||
WorkflowHelpers,
|
|
||||||
WorkflowRunner,
|
|
||||||
} from './';
|
|
||||||
|
|
||||||
import {
|
|
||||||
BINARY_ENCODING,
|
|
||||||
NodeExecuteFunctions,
|
|
||||||
} from 'n8n-core';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IBinaryKeyData,
|
IBinaryKeyData,
|
||||||
|
@ -29,13 +25,28 @@ import {
|
||||||
IRunExecutionData,
|
IRunExecutionData,
|
||||||
IWebhookData,
|
IWebhookData,
|
||||||
IWebhookResponseData,
|
IWebhookResponseData,
|
||||||
|
IWorkflowDataProxyAdditionalKeys,
|
||||||
IWorkflowExecuteAdditionalData,
|
IWorkflowExecuteAdditionalData,
|
||||||
LoggerProxy as Logger,
|
LoggerProxy as Logger,
|
||||||
NodeHelpers,
|
NodeHelpers,
|
||||||
Workflow,
|
Workflow,
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
// eslint-disable-next-line import/no-cycle
|
||||||
|
import {
|
||||||
|
ActiveExecutions,
|
||||||
|
GenericHelpers,
|
||||||
|
IExecutionDb,
|
||||||
|
IResponseCallbackData,
|
||||||
|
IWorkflowDb,
|
||||||
|
IWorkflowExecutionDataProcess,
|
||||||
|
ResponseHelper,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
WorkflowCredentials,
|
||||||
|
WorkflowExecuteAdditionalData,
|
||||||
|
WorkflowHelpers,
|
||||||
|
WorkflowRunner,
|
||||||
|
} from '.';
|
||||||
|
|
||||||
const activeExecutions = ActiveExecutions.getInstance();
|
const activeExecutions = ActiveExecutions.getInstance();
|
||||||
|
|
||||||
|
@ -47,7 +58,12 @@ const activeExecutions = ActiveExecutions.getInstance();
|
||||||
* @param {Workflow} workflow
|
* @param {Workflow} workflow
|
||||||
* @returns {IWebhookData[]}
|
* @returns {IWebhookData[]}
|
||||||
*/
|
*/
|
||||||
export function getWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, destinationNode?: string): IWebhookData[] {
|
export function getWorkflowWebhooks(
|
||||||
|
workflow: Workflow,
|
||||||
|
additionalData: IWorkflowExecuteAdditionalData,
|
||||||
|
destinationNode?: string,
|
||||||
|
ignoreRestartWehbooks = false,
|
||||||
|
): IWebhookData[] {
|
||||||
// Check all the nodes in the workflow if they have webhooks
|
// Check all the nodes in the workflow if they have webhooks
|
||||||
|
|
||||||
const returnData: IWebhookData[] = [];
|
const returnData: IWebhookData[] = [];
|
||||||
|
@ -63,9 +79,13 @@ export function getWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflo
|
||||||
if (parentNodes !== undefined && !parentNodes.includes(node.name)) {
|
if (parentNodes !== undefined && !parentNodes.includes(node.name)) {
|
||||||
// If parentNodes are given check only them if they have webhooks
|
// If parentNodes are given check only them if they have webhooks
|
||||||
// and no other ones
|
// and no other ones
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
returnData.push.apply(returnData, NodeHelpers.getNodeWebhooks(workflow, node, additionalData));
|
returnData.push.apply(
|
||||||
|
returnData,
|
||||||
|
NodeHelpers.getNodeWebhooks(workflow, node, additionalData, ignoreRestartWehbooks),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return returnData;
|
return returnData;
|
||||||
|
@ -91,22 +111,33 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
|
||||||
return returnData;
|
return returnData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
/**
|
* Executes a webhook
|
||||||
* Executes a webhook
|
*
|
||||||
*
|
* @export
|
||||||
* @export
|
* @param {IWebhookData} webhookData
|
||||||
* @param {IWebhookData} webhookData
|
* @param {IWorkflowDb} workflowData
|
||||||
* @param {IWorkflowDb} workflowData
|
* @param {INode} workflowStartNode
|
||||||
* @param {INode} workflowStartNode
|
* @param {WorkflowExecuteMode} executionMode
|
||||||
* @param {WorkflowExecuteMode} executionMode
|
* @param {(string | undefined)} sessionId
|
||||||
* @param {(string | undefined)} sessionId
|
* @param {express.Request} req
|
||||||
* @param {express.Request} req
|
* @param {express.Response} res
|
||||||
* @param {express.Response} res
|
* @param {((error: Error | null, data: IResponseCallbackData) => void)} responseCallback
|
||||||
* @param {((error: Error | null, data: IResponseCallbackData) => void)} responseCallback
|
* @returns {(Promise<string | undefined>)}
|
||||||
* @returns {(Promise<string | undefined>)}
|
*/
|
||||||
*/
|
export async function executeWebhook(
|
||||||
export async function executeWebhook(workflow: Workflow, webhookData: IWebhookData, workflowData: IWorkflowDb, workflowStartNode: INode, executionMode: WorkflowExecuteMode, sessionId: string | undefined, req: express.Request, res: express.Response, responseCallback: (error: Error | null, data: IResponseCallbackData) => void): Promise<string | undefined> {
|
workflow: Workflow,
|
||||||
|
webhookData: IWebhookData,
|
||||||
|
workflowData: IWorkflowDb,
|
||||||
|
workflowStartNode: INode,
|
||||||
|
executionMode: WorkflowExecuteMode,
|
||||||
|
sessionId: string | undefined,
|
||||||
|
runExecutionData: IRunExecutionData | undefined,
|
||||||
|
executionId: string | undefined,
|
||||||
|
req: express.Request,
|
||||||
|
res: express.Response,
|
||||||
|
responseCallback: (error: Error | null, data: IResponseCallbackData) => void,
|
||||||
|
): Promise<string | undefined> {
|
||||||
// Get the nodeType to know which responseMode is set
|
// Get the nodeType to know which responseMode is set
|
||||||
const nodeType = workflow.nodeTypes.getByName(workflowStartNode.type);
|
const nodeType = workflow.nodeTypes.getByName(workflowStartNode.type);
|
||||||
if (nodeType === undefined) {
|
if (nodeType === undefined) {
|
||||||
|
@ -115,9 +146,25 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
|
||||||
throw new ResponseHelper.ResponseError(errorMessage, 500, 500);
|
throw new ResponseHelper.ResponseError(errorMessage, 500, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const additionalKeys: IWorkflowDataProxyAdditionalKeys = {
|
||||||
|
$executionId: executionId,
|
||||||
|
};
|
||||||
|
|
||||||
// Get the responseMode
|
// Get the responseMode
|
||||||
const responseMode = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseMode'], executionMode, 'onReceived');
|
const responseMode = workflow.expression.getSimpleParameterValue(
|
||||||
const responseCode = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseCode'], executionMode, 200) as number;
|
workflowStartNode,
|
||||||
|
webhookData.webhookDescription.responseMode,
|
||||||
|
executionMode,
|
||||||
|
additionalKeys,
|
||||||
|
'onReceived',
|
||||||
|
);
|
||||||
|
const responseCode = workflow.expression.getSimpleParameterValue(
|
||||||
|
workflowStartNode,
|
||||||
|
webhookData.webhookDescription.responseCode,
|
||||||
|
executionMode,
|
||||||
|
additionalKeys,
|
||||||
|
200,
|
||||||
|
) as number;
|
||||||
|
|
||||||
if (!['onReceived', 'lastNode'].includes(responseMode as string)) {
|
if (!['onReceived', 'lastNode'].includes(responseMode as string)) {
|
||||||
// If the mode is not known we error. Is probably best like that instead of using
|
// If the mode is not known we error. Is probably best like that instead of using
|
||||||
|
@ -129,8 +176,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare everything that is needed to run the workflow
|
// Prepare everything that is needed to run the workflow
|
||||||
const credentials = await WorkflowCredentials(workflowData.nodes);
|
const additionalData = await WorkflowExecuteAdditionalData.getBase();
|
||||||
const additionalData = await WorkflowExecuteAdditionalData.getBase(credentials);
|
|
||||||
|
|
||||||
// Add the Response and Request so that this data can be accessed in the node
|
// Add the Response and Request so that this data can be accessed in the node
|
||||||
additionalData.httpRequest = req;
|
additionalData.httpRequest = req;
|
||||||
|
@ -144,7 +190,13 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
|
||||||
let webhookResultData: IWebhookResponseData;
|
let webhookResultData: IWebhookResponseData;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
webhookResultData = await workflow.runWebhook(webhookData, workflowStartNode, additionalData, NodeExecuteFunctions, executionMode);
|
webhookResultData = await workflow.runWebhook(
|
||||||
|
webhookData,
|
||||||
|
workflowStartNode,
|
||||||
|
additionalData,
|
||||||
|
NodeExecuteFunctions,
|
||||||
|
executionMode,
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Send error response to webhook caller
|
// Send error response to webhook caller
|
||||||
const errorMessage = 'Workflow Webhook Error: Workflow could not be started!';
|
const errorMessage = 'Workflow Webhook Error: Workflow could not be started!';
|
||||||
|
@ -168,29 +220,41 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
|
||||||
noWebhookResponse: true,
|
noWebhookResponse: true,
|
||||||
// Add empty data that it at least tries to "execute" the webhook
|
// Add empty data that it at least tries to "execute" the webhook
|
||||||
// which then so gets the chance to throw the error.
|
// which then so gets the chance to throw the error.
|
||||||
workflowData: [[{json: {}}]],
|
workflowData: [[{ json: {} }]],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save static data if it changed
|
// Save static data if it changed
|
||||||
await WorkflowHelpers.saveStaticData(workflow);
|
await WorkflowHelpers.saveStaticData(workflow);
|
||||||
|
|
||||||
if (webhookData.webhookDescription['responseHeaders'] !== undefined) {
|
const additionalKeys: IWorkflowDataProxyAdditionalKeys = {
|
||||||
const responseHeaders = workflow.expression.getComplexParameterValue(workflowStartNode, webhookData.webhookDescription['responseHeaders'], executionMode, undefined) as {
|
$executionId: executionId,
|
||||||
entries?: Array<{
|
};
|
||||||
name: string;
|
|
||||||
value: string;
|
if (webhookData.webhookDescription.responseHeaders !== undefined) {
|
||||||
}> | undefined;
|
const responseHeaders = workflow.expression.getComplexParameterValue(
|
||||||
|
workflowStartNode,
|
||||||
|
webhookData.webhookDescription.responseHeaders,
|
||||||
|
executionMode,
|
||||||
|
additionalKeys,
|
||||||
|
undefined,
|
||||||
|
) as {
|
||||||
|
entries?:
|
||||||
|
| Array<{
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}>
|
||||||
|
| undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (responseHeaders !== undefined && responseHeaders['entries'] !== undefined) {
|
if (responseHeaders !== undefined && responseHeaders.entries !== undefined) {
|
||||||
for (const item of responseHeaders['entries']) {
|
for (const item of responseHeaders.entries) {
|
||||||
res.setHeader(item['name'], item['value']);
|
res.setHeader(item.name, item.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (webhookResultData.noWebhookResponse === true && didSendResponse === false) {
|
if (webhookResultData.noWebhookResponse === true && !didSendResponse) {
|
||||||
// The response got already send
|
// The response got already send
|
||||||
responseCallback(null, {
|
responseCallback(null, {
|
||||||
noWebhookResponse: true,
|
noWebhookResponse: true,
|
||||||
|
@ -202,7 +266,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
|
||||||
// Workflow should not run
|
// Workflow should not run
|
||||||
if (webhookResultData.webhookResponse !== undefined) {
|
if (webhookResultData.webhookResponse !== undefined) {
|
||||||
// Data to respond with is given
|
// Data to respond with is given
|
||||||
if (didSendResponse === false) {
|
if (!didSendResponse) {
|
||||||
responseCallback(null, {
|
responseCallback(null, {
|
||||||
data: webhookResultData.webhookResponse,
|
data: webhookResultData.webhookResponse,
|
||||||
responseCode,
|
responseCode,
|
||||||
|
@ -211,7 +275,8 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Send default response
|
// Send default response
|
||||||
if (didSendResponse === false) {
|
// eslint-disable-next-line no-lonely-if
|
||||||
|
if (!didSendResponse) {
|
||||||
responseCallback(null, {
|
responseCallback(null, {
|
||||||
data: {
|
data: {
|
||||||
message: 'Webhook call got received.',
|
message: 'Webhook call got received.',
|
||||||
|
@ -226,7 +291,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
|
||||||
|
|
||||||
// Now that we know that the workflow should run we can return the default response
|
// Now that we know that the workflow should run we can return the default response
|
||||||
// directly if responseMode it set to "onReceived" and a respone should be sent
|
// directly if responseMode it set to "onReceived" and a respone should be sent
|
||||||
if (responseMode === 'onReceived' && didSendResponse === false) {
|
if (responseMode === 'onReceived' && !didSendResponse) {
|
||||||
// Return response directly and do not wait for the workflow to finish
|
// Return response directly and do not wait for the workflow to finish
|
||||||
if (webhookResultData.webhookResponse !== undefined) {
|
if (webhookResultData.webhookResponse !== undefined) {
|
||||||
// Data to respond with is given
|
// Data to respond with is given
|
||||||
|
@ -248,27 +313,33 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
|
||||||
|
|
||||||
// Initialize the data of the webhook node
|
// Initialize the data of the webhook node
|
||||||
const nodeExecutionStack: IExecuteData[] = [];
|
const nodeExecutionStack: IExecuteData[] = [];
|
||||||
nodeExecutionStack.push(
|
nodeExecutionStack.push({
|
||||||
{
|
node: workflowStartNode,
|
||||||
node: workflowStartNode,
|
data: {
|
||||||
data: {
|
main: webhookResultData.workflowData,
|
||||||
main: webhookResultData.workflowData,
|
},
|
||||||
},
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const runExecutionData: IRunExecutionData = {
|
runExecutionData =
|
||||||
startData: {
|
runExecutionData ||
|
||||||
},
|
({
|
||||||
resultData: {
|
startData: {},
|
||||||
runData: {},
|
resultData: {
|
||||||
},
|
runData: {},
|
||||||
executionData: {
|
},
|
||||||
contextData: {},
|
executionData: {
|
||||||
nodeExecutionStack,
|
contextData: {},
|
||||||
waitingExecution: {},
|
nodeExecutionStack,
|
||||||
},
|
waitingExecution: {},
|
||||||
};
|
},
|
||||||
|
} as IRunExecutionData);
|
||||||
|
|
||||||
|
if (executionId !== undefined) {
|
||||||
|
// Set the data the webhook node did return on the waiting node if executionId
|
||||||
|
// already exists as it means that we are restarting an existing execution.
|
||||||
|
runExecutionData.executionData!.nodeExecutionStack[0].data.main =
|
||||||
|
webhookResultData.workflowData;
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(runExecutionDataMerge).length !== 0) {
|
if (Object.keys(runExecutionDataMerge).length !== 0) {
|
||||||
// If data to merge got defined add it to the execution data
|
// If data to merge got defined add it to the execution data
|
||||||
|
@ -276,7 +347,6 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
|
||||||
}
|
}
|
||||||
|
|
||||||
const runData: IWorkflowExecutionDataProcess = {
|
const runData: IWorkflowExecutionDataProcess = {
|
||||||
credentials,
|
|
||||||
executionMode,
|
executionMode,
|
||||||
executionData: runExecutionData,
|
executionData: runExecutionData,
|
||||||
sessionId,
|
sessionId,
|
||||||
|
@ -285,161 +355,205 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
|
||||||
|
|
||||||
// Start now to run the workflow
|
// Start now to run the workflow
|
||||||
const workflowRunner = new WorkflowRunner();
|
const workflowRunner = new WorkflowRunner();
|
||||||
const executionId = await workflowRunner.run(runData, true, !didSendResponse);
|
executionId = await workflowRunner.run(runData, true, !didSendResponse, executionId);
|
||||||
|
|
||||||
Logger.verbose(`Started execution of workflow "${workflow.name}" from webhook with execution ID ${executionId}`, { executionId });
|
Logger.verbose(
|
||||||
|
`Started execution of workflow "${workflow.name}" from webhook with execution ID ${executionId}`,
|
||||||
|
{ executionId },
|
||||||
|
);
|
||||||
|
|
||||||
// Get a promise which resolves when the workflow did execute and send then response
|
// Get a promise which resolves when the workflow did execute and send then response
|
||||||
const executePromise = activeExecutions.getPostExecutePromise(executionId) as Promise<IExecutionDb | undefined>;
|
const executePromise = activeExecutions.getPostExecutePromise(executionId) as Promise<
|
||||||
executePromise.then((data) => {
|
IExecutionDb | undefined
|
||||||
if (data === undefined) {
|
>;
|
||||||
if (didSendResponse === false) {
|
executePromise
|
||||||
responseCallback(null, {
|
.then((data) => {
|
||||||
data: {
|
if (data === undefined) {
|
||||||
message: 'Workflow did execute sucessfully but no data got returned.',
|
if (!didSendResponse) {
|
||||||
},
|
responseCallback(null, {
|
||||||
responseCode,
|
data: {
|
||||||
});
|
message: 'Workflow did execute sucessfully but no data got returned.',
|
||||||
didSendResponse = true;
|
},
|
||||||
}
|
responseCode,
|
||||||
return undefined;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data);
|
|
||||||
if(data.data.resultData.error || returnData?.error !== undefined) {
|
|
||||||
if (didSendResponse === false) {
|
|
||||||
responseCallback(null, {
|
|
||||||
data: {
|
|
||||||
message: 'Workflow did error.',
|
|
||||||
},
|
|
||||||
responseCode: 500,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
didSendResponse = true;
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (returnData === undefined) {
|
|
||||||
if (didSendResponse === false) {
|
|
||||||
responseCallback(null, {
|
|
||||||
data: {
|
|
||||||
message: 'Workflow did execute sucessfully but the last node did not return any data.',
|
|
||||||
},
|
|
||||||
responseCode,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
didSendResponse = true;
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
const responseData = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseData'], executionMode, 'firstEntryJson');
|
|
||||||
|
|
||||||
if (didSendResponse === false) {
|
|
||||||
let data: IDataObject | IDataObject[];
|
|
||||||
|
|
||||||
if (responseData === 'firstEntryJson') {
|
|
||||||
// Return the JSON data of the first entry
|
|
||||||
|
|
||||||
if (returnData.data!.main[0]![0] === undefined) {
|
|
||||||
responseCallback(new Error('No item to return got found.'), {});
|
|
||||||
didSendResponse = true;
|
didSendResponse = true;
|
||||||
}
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
data = returnData.data!.main[0]![0].json;
|
const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data);
|
||||||
|
if (data.data.resultData.error || returnData?.error !== undefined) {
|
||||||
const responsePropertyName = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responsePropertyName'], executionMode, undefined);
|
if (!didSendResponse) {
|
||||||
|
responseCallback(null, {
|
||||||
if (responsePropertyName !== undefined) {
|
data: {
|
||||||
data = get(data, responsePropertyName as string) as IDataObject;
|
message: 'Workflow did error.',
|
||||||
|
},
|
||||||
|
responseCode: 500,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
didSendResponse = true;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
const responseContentType = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseContentType'], executionMode, undefined);
|
if (returnData === undefined) {
|
||||||
|
if (!didSendResponse) {
|
||||||
|
responseCallback(null, {
|
||||||
|
data: {
|
||||||
|
message:
|
||||||
|
'Workflow did execute sucessfully but the last node did not return any data.',
|
||||||
|
},
|
||||||
|
responseCode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
didSendResponse = true;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
if (responseContentType !== undefined) {
|
const additionalKeys: IWorkflowDataProxyAdditionalKeys = {
|
||||||
// Send the webhook response manually to be able to set the content-type
|
$executionId: executionId,
|
||||||
res.setHeader('Content-Type', responseContentType as string);
|
};
|
||||||
|
|
||||||
// Returning an object, boolean, number, ... causes problems so make sure to stringify if needed
|
const responseData = workflow.expression.getSimpleParameterValue(
|
||||||
if (data !== null && data !== undefined && ['Buffer', 'String'].includes(data.constructor.name)) {
|
workflowStartNode,
|
||||||
res.end(data);
|
webhookData.webhookDescription.responseData,
|
||||||
} else {
|
executionMode,
|
||||||
res.end(JSON.stringify(data));
|
additionalKeys,
|
||||||
|
'firstEntryJson',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!didSendResponse) {
|
||||||
|
let data: IDataObject | IDataObject[];
|
||||||
|
|
||||||
|
if (responseData === 'firstEntryJson') {
|
||||||
|
// Return the JSON data of the first entry
|
||||||
|
|
||||||
|
if (returnData.data!.main[0]![0] === undefined) {
|
||||||
|
responseCallback(new Error('No item to return got found.'), {});
|
||||||
|
didSendResponse = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data = returnData.data!.main[0]![0].json;
|
||||||
|
|
||||||
|
const responsePropertyName = workflow.expression.getSimpleParameterValue(
|
||||||
|
workflowStartNode,
|
||||||
|
webhookData.webhookDescription.responsePropertyName,
|
||||||
|
executionMode,
|
||||||
|
additionalKeys,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (responsePropertyName !== undefined) {
|
||||||
|
data = get(data, responsePropertyName as string) as IDataObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseContentType = workflow.expression.getSimpleParameterValue(
|
||||||
|
workflowStartNode,
|
||||||
|
webhookData.webhookDescription.responseContentType,
|
||||||
|
executionMode,
|
||||||
|
additionalKeys,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (responseContentType !== undefined) {
|
||||||
|
// Send the webhook response manually to be able to set the content-type
|
||||||
|
res.setHeader('Content-Type', responseContentType as string);
|
||||||
|
|
||||||
|
// Returning an object, boolean, number, ... causes problems so make sure to stringify if needed
|
||||||
|
if (
|
||||||
|
data !== null &&
|
||||||
|
data !== undefined &&
|
||||||
|
['Buffer', 'String'].includes(data.constructor.name)
|
||||||
|
) {
|
||||||
|
res.end(data);
|
||||||
|
} else {
|
||||||
|
res.end(JSON.stringify(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
responseCallback(null, {
|
||||||
|
noWebhookResponse: true,
|
||||||
|
});
|
||||||
|
didSendResponse = true;
|
||||||
|
}
|
||||||
|
} else if (responseData === 'firstEntryBinary') {
|
||||||
|
// Return the binary data of the first entry
|
||||||
|
data = returnData.data!.main[0]![0];
|
||||||
|
|
||||||
|
if (data === undefined) {
|
||||||
|
responseCallback(new Error('No item to return got found.'), {});
|
||||||
|
didSendResponse = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.binary === undefined) {
|
||||||
|
responseCallback(new Error('No binary data to return got found.'), {});
|
||||||
|
didSendResponse = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseBinaryPropertyName = workflow.expression.getSimpleParameterValue(
|
||||||
|
workflowStartNode,
|
||||||
|
webhookData.webhookDescription.responseBinaryPropertyName,
|
||||||
|
executionMode,
|
||||||
|
additionalKeys,
|
||||||
|
'data',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (responseBinaryPropertyName === undefined && !didSendResponse) {
|
||||||
|
responseCallback(new Error('No "responseBinaryPropertyName" is set.'), {});
|
||||||
|
didSendResponse = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const binaryData = (data.binary as IBinaryKeyData)[
|
||||||
|
responseBinaryPropertyName as string
|
||||||
|
];
|
||||||
|
if (binaryData === undefined && !didSendResponse) {
|
||||||
|
responseCallback(
|
||||||
|
new Error(
|
||||||
|
`The binary property "${responseBinaryPropertyName}" which should be returned does not exist.`,
|
||||||
|
),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
didSendResponse = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!didSendResponse) {
|
||||||
|
// Send the webhook response manually
|
||||||
|
res.setHeader('Content-Type', binaryData.mimeType);
|
||||||
|
res.end(Buffer.from(binaryData.data, BINARY_ENCODING));
|
||||||
|
|
||||||
|
responseCallback(null, {
|
||||||
|
noWebhookResponse: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Return the JSON data of all the entries
|
||||||
|
data = [];
|
||||||
|
for (const entry of returnData.data!.main[0]!) {
|
||||||
|
data.push(entry.json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!didSendResponse) {
|
||||||
responseCallback(null, {
|
responseCallback(null, {
|
||||||
noWebhookResponse: true,
|
data,
|
||||||
});
|
responseCode,
|
||||||
didSendResponse = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (responseData === 'firstEntryBinary') {
|
|
||||||
// Return the binary data of the first entry
|
|
||||||
data = returnData.data!.main[0]![0];
|
|
||||||
|
|
||||||
if (data === undefined) {
|
|
||||||
responseCallback(new Error('No item to return got found.'), {});
|
|
||||||
didSendResponse = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.binary === undefined) {
|
|
||||||
responseCallback(new Error('No binary data to return got found.'), {});
|
|
||||||
didSendResponse = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const responseBinaryPropertyName = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseBinaryPropertyName'], executionMode, 'data');
|
|
||||||
|
|
||||||
if (responseBinaryPropertyName === undefined && didSendResponse === false) {
|
|
||||||
responseCallback(new Error('No "responseBinaryPropertyName" is set.'), {});
|
|
||||||
didSendResponse = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const binaryData = (data.binary as IBinaryKeyData)[responseBinaryPropertyName as string];
|
|
||||||
if (binaryData === undefined && didSendResponse === false) {
|
|
||||||
responseCallback(new Error(`The binary property "${responseBinaryPropertyName}" which should be returned does not exist.`), {});
|
|
||||||
didSendResponse = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (didSendResponse === false) {
|
|
||||||
// Send the webhook response manually
|
|
||||||
res.setHeader('Content-Type', binaryData.mimeType);
|
|
||||||
res.end(Buffer.from(binaryData.data, BINARY_ENCODING));
|
|
||||||
|
|
||||||
responseCallback(null, {
|
|
||||||
noWebhookResponse: true,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
didSendResponse = true;
|
||||||
|
|
||||||
} else {
|
return data;
|
||||||
// Return the JSON data of all the entries
|
})
|
||||||
data = [];
|
.catch((e) => {
|
||||||
for (const entry of returnData.data!.main[0]!) {
|
if (!didSendResponse) {
|
||||||
data.push(entry.json);
|
responseCallback(new Error('There was a problem executing the workflow.'), {});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (didSendResponse === false) {
|
throw new ResponseHelper.ResponseError(e.message, 500, 500);
|
||||||
responseCallback(null, {
|
});
|
||||||
data,
|
|
||||||
responseCode,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
didSendResponse = true;
|
|
||||||
|
|
||||||
return data;
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
if (didSendResponse === false) {
|
|
||||||
responseCallback(new Error('There was a problem executing the workflow.'), {});
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new ResponseHelper.ResponseError(e.message, 500, 500);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
return executionId;
|
return executionId;
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (didSendResponse === false) {
|
if (!didSendResponse) {
|
||||||
responseCallback(new Error('There was a problem executing the workflow.'), {});
|
responseCallback(new Error('There was a problem executing the workflow.'), {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -447,7 +561,6 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the base URL of the webhooks
|
* Returns the base URL of the webhooks
|
||||||
*
|
*
|
||||||
|
@ -463,6 +576,9 @@ export function getWebhookBaseUrl() {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
urlBaseWebhook = process.env.WEBHOOK_TUNNEL_URL || process.env.WEBHOOK_URL;
|
urlBaseWebhook = process.env.WEBHOOK_TUNNEL_URL || process.env.WEBHOOK_URL;
|
||||||
}
|
}
|
||||||
|
if (!urlBaseWebhook.endsWith('/')) {
|
||||||
|
urlBaseWebhook += '/';
|
||||||
|
}
|
||||||
|
|
||||||
return urlBaseWebhook;
|
return urlBaseWebhook;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,22 @@
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
/* eslint-disable consistent-return */
|
||||||
|
/* eslint-disable @typescript-eslint/restrict-plus-operands */
|
||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
|
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
import {
|
import { readFileSync } from 'fs';
|
||||||
readFileSync,
|
import { getConnectionManager } from 'typeorm';
|
||||||
} from 'fs';
|
|
||||||
import {
|
|
||||||
getConnectionManager,
|
|
||||||
} from 'typeorm';
|
|
||||||
import * as bodyParser from 'body-parser';
|
import * as bodyParser from 'body-parser';
|
||||||
require('body-parser-xml')(bodyParser);
|
// eslint-disable-next-line import/no-extraneous-dependencies, @typescript-eslint/no-unused-vars
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
|
import * as compression from 'compression';
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
import * as parseUrl from 'parseurl';
|
||||||
|
// eslint-disable-next-line import/no-cycle
|
||||||
import {
|
import {
|
||||||
ActiveExecutions,
|
ActiveExecutions,
|
||||||
ActiveWorkflowRunner,
|
ActiveWorkflowRunner,
|
||||||
|
@ -19,123 +27,253 @@ import {
|
||||||
IExternalHooksClass,
|
IExternalHooksClass,
|
||||||
IPackageVersions,
|
IPackageVersions,
|
||||||
ResponseHelper,
|
ResponseHelper,
|
||||||
} from './';
|
WaitingWebhooks,
|
||||||
|
} from '.';
|
||||||
|
|
||||||
import * as compression from 'compression';
|
|
||||||
import * as config from '../config';
|
import * as config from '../config';
|
||||||
import * as parseUrl from 'parseurl';
|
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-call
|
||||||
|
require('body-parser-xml')(bodyParser);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
export function registerProductionWebhooks() {
|
export function registerProductionWebhooks() {
|
||||||
|
// ----------------------------------------
|
||||||
|
// Regular Webhooks
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
// HEAD webhook requests
|
// HEAD webhook requests
|
||||||
this.app.head(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => {
|
this.app.head(
|
||||||
// Cut away the "/webhook/" to get the registred part of the url
|
`/${this.endpointWebhook}/*`,
|
||||||
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhook.length + 2);
|
async (req: express.Request, res: express.Response) => {
|
||||||
|
// Cut away the "/webhook/" to get the registred part of the url
|
||||||
|
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
|
||||||
|
this.endpointWebhook.length + 2,
|
||||||
|
);
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
try {
|
try {
|
||||||
response = await this.activeWorkflowRunner.executeWebhook('HEAD', requestUrl, req, res);
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
} catch (error) {
|
response = await this.activeWorkflowRunner.executeWebhook('HEAD', requestUrl, req, res);
|
||||||
ResponseHelper.sendErrorResponse(res, error);
|
} catch (error) {
|
||||||
return;
|
ResponseHelper.sendErrorResponse(res, error);
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (response.noWebhookResponse === true) {
|
if (response.noWebhookResponse === true) {
|
||||||
// Nothing else to do as the response got already sent
|
// Nothing else to do as the response got already sent
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
|
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// OPTIONS webhook requests
|
// OPTIONS webhook requests
|
||||||
this.app.options(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => {
|
this.app.options(
|
||||||
// Cut away the "/webhook/" to get the registred part of the url
|
`/${this.endpointWebhook}/*`,
|
||||||
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhook.length + 2);
|
async (req: express.Request, res: express.Response) => {
|
||||||
|
// Cut away the "/webhook/" to get the registred part of the url
|
||||||
|
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
|
||||||
|
this.endpointWebhook.length + 2,
|
||||||
|
);
|
||||||
|
|
||||||
let allowedMethods: string[];
|
let allowedMethods: string[];
|
||||||
try {
|
try {
|
||||||
allowedMethods = await this.activeWorkflowRunner.getWebhookMethods(requestUrl);
|
allowedMethods = await this.activeWorkflowRunner.getWebhookMethods(requestUrl);
|
||||||
allowedMethods.push('OPTIONS');
|
allowedMethods.push('OPTIONS');
|
||||||
|
|
||||||
// Add custom "Allow" header to satisfy OPTIONS response.
|
// Add custom "Allow" header to satisfy OPTIONS response.
|
||||||
res.append('Allow', allowedMethods);
|
res.append('Allow', allowedMethods);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ResponseHelper.sendErrorResponse(res, error);
|
ResponseHelper.sendErrorResponse(res, error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ResponseHelper.sendSuccessResponse(res, {}, true, 204);
|
ResponseHelper.sendSuccessResponse(res, {}, true, 204);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// GET webhook requests
|
// GET webhook requests
|
||||||
this.app.get(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => {
|
this.app.get(
|
||||||
// Cut away the "/webhook/" to get the registred part of the url
|
`/${this.endpointWebhook}/*`,
|
||||||
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhook.length + 2);
|
async (req: express.Request, res: express.Response) => {
|
||||||
|
// Cut away the "/webhook/" to get the registred part of the url
|
||||||
|
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
|
||||||
|
this.endpointWebhook.length + 2,
|
||||||
|
);
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
try {
|
try {
|
||||||
response = await this.activeWorkflowRunner.executeWebhook('GET', requestUrl, req, res);
|
response = await this.activeWorkflowRunner.executeWebhook('GET', requestUrl, req, res);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ResponseHelper.sendErrorResponse(res, error);
|
ResponseHelper.sendErrorResponse(res, error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.noWebhookResponse === true) {
|
if (response.noWebhookResponse === true) {
|
||||||
// Nothing else to do as the response got already sent
|
// Nothing else to do as the response got already sent
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
|
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// POST webhook requests
|
// POST webhook requests
|
||||||
this.app.post(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => {
|
this.app.post(
|
||||||
// Cut away the "/webhook/" to get the registred part of the url
|
`/${this.endpointWebhook}/*`,
|
||||||
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhook.length + 2);
|
async (req: express.Request, res: express.Response) => {
|
||||||
|
// Cut away the "/webhook/" to get the registred part of the url
|
||||||
|
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
|
||||||
|
this.endpointWebhook.length + 2,
|
||||||
|
);
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
try {
|
try {
|
||||||
response = await this.activeWorkflowRunner.executeWebhook('POST', requestUrl, req, res);
|
response = await this.activeWorkflowRunner.executeWebhook('POST', requestUrl, req, res);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ResponseHelper.sendErrorResponse(res, error);
|
ResponseHelper.sendErrorResponse(res, error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.noWebhookResponse === true) {
|
if (response.noWebhookResponse === true) {
|
||||||
// Nothing else to do as the response got already sent
|
// Nothing else to do as the response got already sent
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
|
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// Waiting Webhooks
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
|
const waitingWebhooks = new WaitingWebhooks();
|
||||||
|
|
||||||
|
// HEAD webhook-waiting requests
|
||||||
|
this.app.head(
|
||||||
|
`/${this.endpointWebhookWaiting}/*`,
|
||||||
|
async (req: express.Request, res: express.Response) => {
|
||||||
|
// Cut away the "/webhook-waiting/" to get the registred part of the url
|
||||||
|
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
|
||||||
|
this.endpointWebhookWaiting.length + 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
response = await waitingWebhooks.executeWebhook('HEAD', requestUrl, req, res);
|
||||||
|
} catch (error) {
|
||||||
|
ResponseHelper.sendErrorResponse(res, error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.noWebhookResponse === true) {
|
||||||
|
// Nothing else to do as the response got already sent
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET webhook-waiting requests
|
||||||
|
this.app.get(
|
||||||
|
`/${this.endpointWebhookWaiting}/*`,
|
||||||
|
async (req: express.Request, res: express.Response) => {
|
||||||
|
// Cut away the "/webhook-waiting/" to get the registred part of the url
|
||||||
|
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
|
||||||
|
this.endpointWebhookWaiting.length + 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
response = await waitingWebhooks.executeWebhook('GET', requestUrl, req, res);
|
||||||
|
} catch (error) {
|
||||||
|
ResponseHelper.sendErrorResponse(res, error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.noWebhookResponse === true) {
|
||||||
|
// Nothing else to do as the response got already sent
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// POST webhook-waiting requests
|
||||||
|
this.app.post(
|
||||||
|
`/${this.endpointWebhookWaiting}/*`,
|
||||||
|
async (req: express.Request, res: express.Response) => {
|
||||||
|
// Cut away the "/webhook-waiting/" to get the registred part of the url
|
||||||
|
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
|
||||||
|
this.endpointWebhookWaiting.length + 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
response = await waitingWebhooks.executeWebhook('POST', requestUrl, req, res);
|
||||||
|
} catch (error) {
|
||||||
|
ResponseHelper.sendErrorResponse(res, error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.noWebhookResponse === true) {
|
||||||
|
// Nothing else to do as the response got already sent
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class App {
|
class App {
|
||||||
|
|
||||||
app: express.Application;
|
app: express.Application;
|
||||||
|
|
||||||
activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner;
|
activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner;
|
||||||
|
|
||||||
endpointWebhook: string;
|
endpointWebhook: string;
|
||||||
|
|
||||||
|
endpointWebhookWaiting: string;
|
||||||
|
|
||||||
endpointPresetCredentials: string;
|
endpointPresetCredentials: string;
|
||||||
|
|
||||||
externalHooks: IExternalHooksClass;
|
externalHooks: IExternalHooksClass;
|
||||||
|
|
||||||
saveDataErrorExecution: string;
|
saveDataErrorExecution: string;
|
||||||
|
|
||||||
saveDataSuccessExecution: string;
|
saveDataSuccessExecution: string;
|
||||||
|
|
||||||
saveManualExecutions: boolean;
|
saveManualExecutions: boolean;
|
||||||
|
|
||||||
executionTimeout: number;
|
executionTimeout: number;
|
||||||
|
|
||||||
maxExecutionTimeout: number;
|
maxExecutionTimeout: number;
|
||||||
|
|
||||||
timezone: string;
|
timezone: string;
|
||||||
|
|
||||||
activeExecutionsInstance: ActiveExecutions.ActiveExecutions;
|
activeExecutionsInstance: ActiveExecutions.ActiveExecutions;
|
||||||
|
|
||||||
versions: IPackageVersions | undefined;
|
versions: IPackageVersions | undefined;
|
||||||
|
|
||||||
restEndpoint: string;
|
restEndpoint: string;
|
||||||
|
|
||||||
protocol: string;
|
protocol: string;
|
||||||
|
|
||||||
sslKey: string;
|
sslKey: string;
|
||||||
|
|
||||||
sslCert: string;
|
sslCert: string;
|
||||||
|
|
||||||
presetCredentialsLoaded: boolean;
|
presetCredentialsLoaded: boolean;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.app = express();
|
this.app = express();
|
||||||
|
|
||||||
this.endpointWebhook = config.get('endpoints.webhook') as string;
|
this.endpointWebhook = config.get('endpoints.webhook') as string;
|
||||||
|
this.endpointWebhookWaiting = config.get('endpoints.webhookWaiting') as string;
|
||||||
this.saveDataErrorExecution = config.get('executions.saveDataOnError') as string;
|
this.saveDataErrorExecution = config.get('executions.saveDataOnError') as string;
|
||||||
this.saveDataSuccessExecution = config.get('executions.saveDataOnSuccess') as string;
|
this.saveDataSuccessExecution = config.get('executions.saveDataOnSuccess') as string;
|
||||||
this.saveManualExecutions = config.get('executions.saveDataManualExecutions') as boolean;
|
this.saveManualExecutions = config.get('executions.saveDataManualExecutions') as boolean;
|
||||||
|
@ -143,22 +281,21 @@ class App {
|
||||||
this.maxExecutionTimeout = config.get('executions.maxTimeout') as number;
|
this.maxExecutionTimeout = config.get('executions.maxTimeout') as number;
|
||||||
this.timezone = config.get('generic.timezone') as string;
|
this.timezone = config.get('generic.timezone') as string;
|
||||||
this.restEndpoint = config.get('endpoints.rest') as string;
|
this.restEndpoint = config.get('endpoints.rest') as string;
|
||||||
|
|
||||||
this.activeWorkflowRunner = ActiveWorkflowRunner.getInstance();
|
this.activeWorkflowRunner = ActiveWorkflowRunner.getInstance();
|
||||||
|
|
||||||
this.activeExecutionsInstance = ActiveExecutions.getInstance();
|
this.activeExecutionsInstance = ActiveExecutions.getInstance();
|
||||||
|
|
||||||
this.protocol = config.get('protocol');
|
this.protocol = config.get('protocol');
|
||||||
this.sslKey = config.get('ssl_key');
|
this.sslKey = config.get('ssl_key');
|
||||||
this.sslCert = config.get('ssl_cert');
|
this.sslCert = config.get('ssl_cert');
|
||||||
|
|
||||||
this.externalHooks = ExternalHooks();
|
this.externalHooks = ExternalHooks();
|
||||||
|
|
||||||
this.presetCredentialsLoaded = false;
|
this.presetCredentialsLoaded = false;
|
||||||
this.endpointPresetCredentials = config.get('credentials.overwrite.endpoint') as string;
|
this.endpointPresetCredentials = config.get('credentials.overwrite.endpoint') as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the current epoch time
|
* Returns the current epoch time
|
||||||
*
|
*
|
||||||
|
@ -168,15 +305,13 @@ class App {
|
||||||
getCurrentDate(): Date {
|
getCurrentDate(): Date {
|
||||||
return new Date();
|
return new Date();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async config(): Promise<void> {
|
async config(): Promise<void> {
|
||||||
|
|
||||||
this.versions = await GenericHelpers.getVersions();
|
this.versions = await GenericHelpers.getVersions();
|
||||||
|
|
||||||
// Compress the response data
|
// Compress the response data
|
||||||
this.app.use(compression());
|
this.app.use(compression());
|
||||||
|
|
||||||
// Make sure that each request has the "parsedUrl" parameter
|
// Make sure that each request has the "parsedUrl" parameter
|
||||||
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
|
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
(req as ICustomRequest).parsedUrl = parseUrl(req);
|
(req as ICustomRequest).parsedUrl = parseUrl(req);
|
||||||
|
@ -184,123 +319,135 @@ class App {
|
||||||
req.rawBody = Buffer.from('', 'base64');
|
req.rawBody = Buffer.from('', 'base64');
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Support application/json type post data
|
// Support application/json type post data
|
||||||
this.app.use(bodyParser.json({
|
this.app.use(
|
||||||
limit: '16mb', verify: (req, res, buf) => {
|
bodyParser.json({
|
||||||
// @ts-ignore
|
limit: '16mb',
|
||||||
req.rawBody = buf;
|
verify: (req, res, buf) => {
|
||||||
},
|
// @ts-ignore
|
||||||
}));
|
req.rawBody = buf;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// Support application/xml type post data
|
// Support application/xml type post data
|
||||||
// @ts-ignore
|
this.app.use(
|
||||||
this.app.use(bodyParser.xml({
|
// @ts-ignore
|
||||||
limit: '16mb', xmlParseOptions: {
|
bodyParser.xml({
|
||||||
normalize: true, // Trim whitespace inside text nodes
|
limit: '16mb',
|
||||||
normalizeTags: true, // Transform tags to lowercase
|
xmlParseOptions: {
|
||||||
explicitArray: false, // Only put properties in array if length > 1
|
normalize: true, // Trim whitespace inside text nodes
|
||||||
},
|
normalizeTags: true, // Transform tags to lowercase
|
||||||
}));
|
explicitArray: false, // Only put properties in array if length > 1
|
||||||
|
},
|
||||||
this.app.use(bodyParser.text({
|
}),
|
||||||
limit: '16mb', verify: (req, res, buf) => {
|
);
|
||||||
// @ts-ignore
|
|
||||||
req.rawBody = buf;
|
this.app.use(
|
||||||
},
|
bodyParser.text({
|
||||||
}));
|
limit: '16mb',
|
||||||
|
verify: (req, res, buf) => {
|
||||||
//support application/x-www-form-urlencoded post data
|
// @ts-ignore
|
||||||
this.app.use(bodyParser.urlencoded({ extended: false,
|
req.rawBody = buf;
|
||||||
verify: (req, res, buf) => {
|
},
|
||||||
// @ts-ignore
|
}),
|
||||||
req.rawBody = buf;
|
);
|
||||||
},
|
|
||||||
}));
|
// support application/x-www-form-urlencoded post data
|
||||||
|
this.app.use(
|
||||||
if (process.env['NODE_ENV'] !== 'production') {
|
bodyParser.urlencoded({
|
||||||
|
extended: false,
|
||||||
|
verify: (req, res, buf) => {
|
||||||
|
// @ts-ignore
|
||||||
|
req.rawBody = buf;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
|
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
// Allow access also from frontend when developing
|
// Allow access also from frontend when developing
|
||||||
res.header('Access-Control-Allow-Origin', 'http://localhost:8080');
|
res.header('Access-Control-Allow-Origin', 'http://localhost:8080');
|
||||||
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
|
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
|
||||||
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, sessionid');
|
res.header(
|
||||||
|
'Access-Control-Allow-Headers',
|
||||||
|
'Origin, X-Requested-With, Content-Type, Accept, sessionid',
|
||||||
|
);
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
|
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
if (Db.collections.Workflow === null) {
|
if (Db.collections.Workflow === null) {
|
||||||
const error = new ResponseHelper.ResponseError('Database is not ready!', undefined, 503);
|
const error = new ResponseHelper.ResponseError('Database is not ready!', undefined, 503);
|
||||||
return ResponseHelper.sendErrorResponse(res, error);
|
return ResponseHelper.sendErrorResponse(res, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
// Healthcheck
|
// Healthcheck
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
|
||||||
|
|
||||||
// Does very basic health check
|
// Does very basic health check
|
||||||
this.app.get('/healthz', async (req: express.Request, res: express.Response) => {
|
this.app.get('/healthz', async (req: express.Request, res: express.Response) => {
|
||||||
|
const connection = getConnectionManager().get();
|
||||||
const connectionManager = getConnectionManager();
|
|
||||||
|
try {
|
||||||
if (connectionManager.connections.length === 0) {
|
if (!connection.isConnected) {
|
||||||
const error = new ResponseHelper.ResponseError('No Database connection found!', undefined, 503);
|
// Connection is not active
|
||||||
|
throw new Error('No active database connection!');
|
||||||
|
}
|
||||||
|
// DB ping
|
||||||
|
await connection.query('SELECT 1');
|
||||||
|
// eslint-disable-next-line id-denylist
|
||||||
|
} catch (err) {
|
||||||
|
const error = new ResponseHelper.ResponseError('No Database connection!', undefined, 503);
|
||||||
return ResponseHelper.sendErrorResponse(res, error);
|
return ResponseHelper.sendErrorResponse(res, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connectionManager.connections[0].isConnected === false) {
|
|
||||||
// Connection is not active
|
|
||||||
const error = new ResponseHelper.ResponseError('Database connection not active!', undefined, 503);
|
|
||||||
return ResponseHelper.sendErrorResponse(res, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Everything fine
|
// Everything fine
|
||||||
const responseData = {
|
const responseData = {
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
};
|
};
|
||||||
|
|
||||||
ResponseHelper.sendSuccessResponse(res, responseData, true, 200);
|
ResponseHelper.sendSuccessResponse(res, responseData, true, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
registerProductionWebhooks.apply(this);
|
registerProductionWebhooks.apply(this);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function start(): Promise<void> {
|
export async function start(): Promise<void> {
|
||||||
const PORT = config.get('port');
|
const PORT = config.get('port');
|
||||||
const ADDRESS = config.get('listen_address');
|
const ADDRESS = config.get('listen_address');
|
||||||
|
|
||||||
const app = new App();
|
const app = new App();
|
||||||
|
|
||||||
await app.config();
|
await app.config();
|
||||||
|
|
||||||
let server;
|
let server;
|
||||||
|
|
||||||
if (app.protocol === 'https' && app.sslKey && app.sslCert) {
|
if (app.protocol === 'https' && app.sslKey && app.sslCert) {
|
||||||
|
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
const privateKey = readFileSync(app.sslKey, 'utf8');
|
const privateKey = readFileSync(app.sslKey, 'utf8');
|
||||||
const cert = readFileSync(app.sslCert, 'utf8');
|
const cert = readFileSync(app.sslCert, 'utf8');
|
||||||
const credentials = { key: privateKey, cert };
|
const credentials = { key: privateKey, cert };
|
||||||
server = https.createServer(credentials, app.app);
|
server = https.createServer(credentials, app.app);
|
||||||
} else {
|
} else {
|
||||||
|
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
server = http.createServer(app.app);
|
server = http.createServer(app.app);
|
||||||
}
|
}
|
||||||
|
|
||||||
server.listen(PORT, ADDRESS, async () => {
|
server.listen(PORT, ADDRESS, async () => {
|
||||||
const versions = await GenericHelpers.getVersions();
|
const versions = await GenericHelpers.getVersions();
|
||||||
console.log(`n8n ready on ${ADDRESS}, port ${PORT}`);
|
console.log(`n8n ready on ${ADDRESS}, port ${PORT}`);
|
||||||
console.log(`Version: ${versions.cli}`);
|
console.log(`Version: ${versions.cli}`);
|
||||||
|
|
||||||
await app.externalHooks.run('n8n.ready', [app]);
|
await app.externalHooks.run('n8n.ready', [app]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,25 @@
|
||||||
import {
|
/* eslint-disable no-prototype-builtins */
|
||||||
Db,
|
import { INode, IWorkflowCredentials } from 'n8n-workflow';
|
||||||
} from './';
|
// eslint-disable-next-line import/no-cycle
|
||||||
import {
|
import { Db } from '.';
|
||||||
INode,
|
|
||||||
IWorkflowCredentials
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
export async function WorkflowCredentials(nodes: INode[]): Promise<IWorkflowCredentials> {
|
export async function WorkflowCredentials(nodes: INode[]): Promise<IWorkflowCredentials> {
|
||||||
// Go through all nodes to find which credentials are needed to execute the workflow
|
// Go through all nodes to find which credentials are needed to execute the workflow
|
||||||
const returnCredentials: IWorkflowCredentials = {};
|
const returnCredentials: IWorkflowCredentials = {};
|
||||||
|
|
||||||
let node, type, name, foundCredentials;
|
let node;
|
||||||
|
let type;
|
||||||
|
let name;
|
||||||
|
let foundCredentials;
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (node of nodes) {
|
for (node of nodes) {
|
||||||
if (node.disabled === true || !node.credentials) {
|
if (node.disabled === true || !node.credentials) {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (type of Object.keys(node.credentials)) {
|
for (type of Object.keys(node.credentials)) {
|
||||||
if (!returnCredentials.hasOwnProperty(type)) {
|
if (!returnCredentials.hasOwnProperty(type)) {
|
||||||
returnCredentials[type] = {};
|
returnCredentials[type] = {};
|
||||||
|
@ -24,14 +27,15 @@ export async function WorkflowCredentials(nodes: INode[]): Promise<IWorkflowCred
|
||||||
name = node.credentials[type];
|
name = node.credentials[type];
|
||||||
|
|
||||||
if (!returnCredentials[type].hasOwnProperty(name)) {
|
if (!returnCredentials[type].hasOwnProperty(name)) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-non-null-assertion
|
||||||
foundCredentials = await Db.collections.Credentials!.find({ name, type });
|
foundCredentials = await Db.collections.Credentials!.find({ name, type });
|
||||||
if (!foundCredentials.length) {
|
if (!foundCredentials.length) {
|
||||||
throw new Error(`Could not find credentials for type "${type}" with name "${name}".`);
|
throw new Error(`Could not find credentials for type "${type}" with name "${name}".`);
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line prefer-destructuring
|
||||||
returnCredentials[type][name] = foundCredentials[0];
|
returnCredentials[type][name] = foundCredentials[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return returnCredentials;
|
return returnCredentials;
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,3 +1,24 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
|
/* eslint-disable no-underscore-dangle */
|
||||||
|
/* eslint-disable no-continue */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||||
|
/* eslint-disable no-restricted-syntax */
|
||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
import {
|
||||||
|
IDataObject,
|
||||||
|
IExecuteData,
|
||||||
|
INode,
|
||||||
|
IRun,
|
||||||
|
IRunExecutionData,
|
||||||
|
ITaskData,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
IWorkflowCredentials,
|
||||||
|
LoggerProxy as Logger,
|
||||||
|
Workflow,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
import { validate } from 'class-validator';
|
||||||
|
// eslint-disable-next-line import/no-cycle
|
||||||
import {
|
import {
|
||||||
CredentialTypes,
|
CredentialTypes,
|
||||||
Db,
|
Db,
|
||||||
|
@ -7,28 +28,17 @@ import {
|
||||||
IWorkflowExecutionDataProcess,
|
IWorkflowExecutionDataProcess,
|
||||||
NodeTypes,
|
NodeTypes,
|
||||||
ResponseHelper,
|
ResponseHelper,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
WorkflowCredentials,
|
WorkflowCredentials,
|
||||||
WorkflowRunner,
|
WorkflowRunner,
|
||||||
} from './';
|
} from '.';
|
||||||
|
|
||||||
import {
|
|
||||||
IDataObject,
|
|
||||||
IExecuteData,
|
|
||||||
INode,
|
|
||||||
IRun,
|
|
||||||
IRunExecutionData,
|
|
||||||
ITaskData,
|
|
||||||
IWorkflowCredentials,
|
|
||||||
LoggerProxy as Logger,
|
|
||||||
Workflow,} from 'n8n-workflow';
|
|
||||||
|
|
||||||
import * as config from '../config';
|
import * as config from '../config';
|
||||||
|
// eslint-disable-next-line import/no-cycle
|
||||||
import { WorkflowEntity } from './databases/entities/WorkflowEntity';
|
import { WorkflowEntity } from './databases/entities/WorkflowEntity';
|
||||||
import { validate } from 'class-validator';
|
|
||||||
|
|
||||||
const ERROR_TRIGGER_TYPE = config.get('nodes.errorTriggerType') as string;
|
const ERROR_TRIGGER_TYPE = config.get('nodes.errorTriggerType') as string;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the data of the last executed node
|
* Returns the data of the last executed node
|
||||||
*
|
*
|
||||||
|
@ -37,8 +47,8 @@ const ERROR_TRIGGER_TYPE = config.get('nodes.errorTriggerType') as string;
|
||||||
* @returns {(ITaskData | undefined)}
|
* @returns {(ITaskData | undefined)}
|
||||||
*/
|
*/
|
||||||
export function getDataLastExecutedNodeData(inputData: IRun): ITaskData | undefined {
|
export function getDataLastExecutedNodeData(inputData: IRun): ITaskData | undefined {
|
||||||
const runData = inputData.data.resultData.runData;
|
const { runData } = inputData.data.resultData;
|
||||||
const lastNodeExecuted = inputData.data.resultData.lastNodeExecuted;
|
const { lastNodeExecuted } = inputData.data.resultData;
|
||||||
|
|
||||||
if (lastNodeExecuted === undefined) {
|
if (lastNodeExecuted === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -51,8 +61,6 @@ export function getDataLastExecutedNodeData(inputData: IRun): ITaskData | undefi
|
||||||
return runData[lastNodeExecuted][runData[lastNodeExecuted].length - 1];
|
return runData[lastNodeExecuted][runData[lastNodeExecuted].length - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns if the given id is a valid workflow id
|
* Returns if the given id is a valid workflow id
|
||||||
*
|
*
|
||||||
|
@ -60,20 +68,18 @@ export function getDataLastExecutedNodeData(inputData: IRun): ITaskData | undefi
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
* @memberof App
|
* @memberof App
|
||||||
*/
|
*/
|
||||||
export function isWorkflowIdValid (id: string | null | undefined | number): boolean {
|
export function isWorkflowIdValid(id: string | null | undefined | number): boolean {
|
||||||
if (typeof id === 'string') {
|
if (typeof id === 'string') {
|
||||||
id = parseInt(id, 10);
|
id = parseInt(id, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals
|
||||||
if (isNaN(id as number)) {
|
if (isNaN(id as number)) {
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes the error workflow
|
* Executes the error workflow
|
||||||
*
|
*
|
||||||
|
@ -82,21 +88,37 @@ export function isWorkflowIdValid (id: string | null | undefined | number): bool
|
||||||
* @param {IWorkflowErrorData} workflowErrorData The error data
|
* @param {IWorkflowErrorData} workflowErrorData The error data
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
export async function executeErrorWorkflow(workflowId: string, workflowErrorData: IWorkflowErrorData): Promise<void> {
|
export async function executeErrorWorkflow(
|
||||||
|
workflowId: string,
|
||||||
|
workflowErrorData: IWorkflowErrorData,
|
||||||
|
): Promise<void> {
|
||||||
// Wrap everything in try/catch to make sure that no errors bubble up and all get caught here
|
// Wrap everything in try/catch to make sure that no errors bubble up and all get caught here
|
||||||
try {
|
try {
|
||||||
const workflowData = await Db.collections.Workflow!.findOne({ id: Number(workflowId) });
|
const workflowData = await Db.collections.Workflow!.findOne({ id: Number(workflowId) });
|
||||||
|
|
||||||
if (workflowData === undefined) {
|
if (workflowData === undefined) {
|
||||||
// The error workflow could not be found
|
// The error workflow could not be found
|
||||||
Logger.error(`Calling Error Workflow for "${workflowErrorData.workflow.id}". Could not find error workflow "${workflowId}"`, { workflowId });
|
Logger.error(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
|
`Calling Error Workflow for "${workflowErrorData.workflow.id}". Could not find error workflow "${workflowId}"`,
|
||||||
|
{ workflowId },
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const executionMode = 'error';
|
const executionMode = 'error';
|
||||||
const nodeTypes = NodeTypes();
|
const nodeTypes = NodeTypes();
|
||||||
|
|
||||||
const workflowInstance = new Workflow({ id: workflowId, name: workflowData.name, nodeTypes, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, staticData: workflowData.staticData, settings: workflowData.settings});
|
const workflowInstance = new Workflow({
|
||||||
|
id: workflowId,
|
||||||
|
name: workflowData.name,
|
||||||
|
nodeTypes,
|
||||||
|
nodes: workflowData.nodes,
|
||||||
|
connections: workflowData.connections,
|
||||||
|
active: workflowData.active,
|
||||||
|
staticData: workflowData.staticData,
|
||||||
|
settings: workflowData.settings,
|
||||||
|
});
|
||||||
|
|
||||||
let node: INode;
|
let node: INode;
|
||||||
let workflowStartNode: INode | undefined;
|
let workflowStartNode: INode | undefined;
|
||||||
|
@ -108,7 +130,9 @@ export async function executeErrorWorkflow(workflowId: string, workflowErrorData
|
||||||
}
|
}
|
||||||
|
|
||||||
if (workflowStartNode === undefined) {
|
if (workflowStartNode === undefined) {
|
||||||
Logger.error(`Calling Error Workflow for "${workflowErrorData.workflow.id}". Could not find "${ERROR_TRIGGER_TYPE}" in workflow "${workflowId}"`);
|
Logger.error(
|
||||||
|
`Calling Error Workflow for "${workflowErrorData.workflow.id}". Could not find "${ERROR_TRIGGER_TYPE}" in workflow "${workflowId}"`,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,24 +140,21 @@ export async function executeErrorWorkflow(workflowId: string, workflowErrorData
|
||||||
|
|
||||||
// Initialize the data of the webhook node
|
// Initialize the data of the webhook node
|
||||||
const nodeExecutionStack: IExecuteData[] = [];
|
const nodeExecutionStack: IExecuteData[] = [];
|
||||||
nodeExecutionStack.push(
|
nodeExecutionStack.push({
|
||||||
{
|
node: workflowStartNode,
|
||||||
node: workflowStartNode,
|
data: {
|
||||||
data: {
|
main: [
|
||||||
main: [
|
[
|
||||||
[
|
{
|
||||||
{
|
json: workflowErrorData,
|
||||||
json: workflowErrorData,
|
},
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
},
|
],
|
||||||
}
|
},
|
||||||
);
|
});
|
||||||
|
|
||||||
const runExecutionData: IRunExecutionData = {
|
const runExecutionData: IRunExecutionData = {
|
||||||
startData: {
|
startData: {},
|
||||||
},
|
|
||||||
resultData: {
|
resultData: {
|
||||||
runData: {},
|
runData: {},
|
||||||
},
|
},
|
||||||
|
@ -144,10 +165,7 @@ export async function executeErrorWorkflow(workflowId: string, workflowErrorData
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const credentials = await WorkflowCredentials(workflowData.nodes);
|
|
||||||
|
|
||||||
const runData: IWorkflowExecutionDataProcess = {
|
const runData: IWorkflowExecutionDataProcess = {
|
||||||
credentials,
|
|
||||||
executionMode,
|
executionMode,
|
||||||
executionData: runExecutionData,
|
executionData: runExecutionData,
|
||||||
workflowData,
|
workflowData,
|
||||||
|
@ -156,12 +174,13 @@ export async function executeErrorWorkflow(workflowId: string, workflowErrorData
|
||||||
const workflowRunner = new WorkflowRunner();
|
const workflowRunner = new WorkflowRunner();
|
||||||
await workflowRunner.run(runData);
|
await workflowRunner.run(runData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(`Calling Error Workflow for "${workflowErrorData.workflow.id}": "${error.message}"`, { workflowId: workflowErrorData.workflow.id });
|
Logger.error(
|
||||||
|
`Calling Error Workflow for "${workflowErrorData.workflow.id}": "${error.message}"`,
|
||||||
|
{ workflowId: workflowErrorData.workflow.id },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all the defined NodeTypes
|
* Returns all the defined NodeTypes
|
||||||
*
|
*
|
||||||
|
@ -188,8 +207,6 @@ export function getAllNodeTypeData(): ITransferNodeTypes {
|
||||||
return returnData;
|
return returnData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the data of the node types that are needed
|
* Returns the data of the node types that are needed
|
||||||
* to execute the given nodes
|
* to execute the given nodes
|
||||||
|
@ -202,6 +219,7 @@ export function getNodeTypeData(nodes: INode[]): ITransferNodeTypes {
|
||||||
const nodeTypes = NodeTypes();
|
const nodeTypes = NodeTypes();
|
||||||
|
|
||||||
// Check which node-types have to be loaded
|
// Check which node-types have to be loaded
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||||
const neededNodeTypes = getNeededNodeTypes(nodes);
|
const neededNodeTypes = getNeededNodeTypes(nodes);
|
||||||
|
|
||||||
// Get all the data of the needed node types that they
|
// Get all the data of the needed node types that they
|
||||||
|
@ -221,8 +239,6 @@ export function getNodeTypeData(nodes: INode[]): ITransferNodeTypes {
|
||||||
return returnData;
|
return returnData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the credentials data of the given type and its parent types
|
* Returns the credentials data of the given type and its parent types
|
||||||
* it extends
|
* it extends
|
||||||
|
@ -254,8 +270,6 @@ export function getCredentialsDataWithParents(type: string): ICredentialsTypeDat
|
||||||
return credentialTypeData;
|
return credentialTypeData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all the credentialTypes which are needed to resolve
|
* Returns all the credentialTypes which are needed to resolve
|
||||||
* the given workflow credentials
|
* the given workflow credentials
|
||||||
|
@ -264,22 +278,26 @@ export function getCredentialsDataWithParents(type: string): ICredentialsTypeDat
|
||||||
* @param {IWorkflowCredentials} credentials The credentials which have to be able to be resolved
|
* @param {IWorkflowCredentials} credentials The credentials which have to be able to be resolved
|
||||||
* @returns {ICredentialsTypeData}
|
* @returns {ICredentialsTypeData}
|
||||||
*/
|
*/
|
||||||
export function getCredentialsData(credentials: IWorkflowCredentials): ICredentialsTypeData {
|
export function getCredentialsDataByNodes(nodes: INode[]): ICredentialsTypeData {
|
||||||
const credentialTypeData: ICredentialsTypeData = {};
|
const credentialTypeData: ICredentialsTypeData = {};
|
||||||
|
|
||||||
for (const credentialType of Object.keys(credentials)) {
|
for (const node of nodes) {
|
||||||
if (credentialTypeData[credentialType] !== undefined) {
|
const credentialsUsedByThisNode = node.credentials;
|
||||||
continue;
|
if (credentialsUsedByThisNode) {
|
||||||
}
|
// const credentialTypesUsedByThisNode = Object.keys(credentialsUsedByThisNode!);
|
||||||
|
for (const credentialType of Object.keys(credentialsUsedByThisNode)) {
|
||||||
|
if (credentialTypeData[credentialType] !== undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
Object.assign(credentialTypeData, getCredentialsDataWithParents(credentialType));
|
Object.assign(credentialTypeData, getCredentialsDataWithParents(credentialType));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return credentialTypeData;
|
return credentialTypeData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the names of the NodeTypes which are are needed
|
* Returns the names of the NodeTypes which are are needed
|
||||||
* to execute the gives nodes
|
* to execute the gives nodes
|
||||||
|
@ -300,8 +318,6 @@ export function getNeededNodeTypes(nodes: INode[]): string[] {
|
||||||
return neededNodeTypes;
|
return neededNodeTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the static data if it changed
|
* Saves the static data if it changed
|
||||||
*
|
*
|
||||||
|
@ -309,23 +325,25 @@ export function getNeededNodeTypes(nodes: INode[]): string[] {
|
||||||
* @param {Workflow} workflow
|
* @param {Workflow} workflow
|
||||||
* @returns {Promise <void>}
|
* @returns {Promise <void>}
|
||||||
*/
|
*/
|
||||||
export async function saveStaticData(workflow: Workflow): Promise <void> {
|
export async function saveStaticData(workflow: Workflow): Promise<void> {
|
||||||
if (workflow.staticData.__dataChanged === true) {
|
if (workflow.staticData.__dataChanged === true) {
|
||||||
// Static data of workflow changed and so has to be saved
|
// Static data of workflow changed and so has to be saved
|
||||||
if (isWorkflowIdValid(workflow.id) === true) {
|
if (isWorkflowIdValid(workflow.id)) {
|
||||||
// Workflow is saved so update in database
|
// Workflow is saved so update in database
|
||||||
try {
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||||
await saveStaticDataById(workflow.id!, workflow.staticData);
|
await saveStaticDataById(workflow.id!, workflow.staticData);
|
||||||
workflow.staticData.__dataChanged = false;
|
workflow.staticData.__dataChanged = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger.error(`There was a problem saving the workflow with id "${workflow.id}" to save changed staticData: "${e.message}"`, { workflowId: workflow.id });
|
Logger.error(
|
||||||
|
`There was a problem saving the workflow with id "${workflow.id}" to save changed staticData: "${e.message}"`,
|
||||||
|
{ workflowId: workflow.id },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the given static data on workflow
|
* Saves the given static data on workflow
|
||||||
*
|
*
|
||||||
|
@ -334,15 +352,15 @@ export async function saveStaticData(workflow: Workflow): Promise <void> {
|
||||||
* @param {IDataObject} newStaticData The static data to save
|
* @param {IDataObject} newStaticData The static data to save
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
export async function saveStaticDataById(workflowId: string | number, newStaticData: IDataObject): Promise<void> {
|
export async function saveStaticDataById(
|
||||||
await Db.collections.Workflow!
|
workflowId: string | number,
|
||||||
.update(workflowId, {
|
newStaticData: IDataObject,
|
||||||
staticData: newStaticData,
|
): Promise<void> {
|
||||||
});
|
await Db.collections.Workflow!.update(workflowId, {
|
||||||
|
staticData: newStaticData,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the static data of workflow
|
* Returns the static data of workflow
|
||||||
*
|
*
|
||||||
|
@ -350,20 +368,23 @@ export async function saveStaticDataById(workflowId: string | number, newStaticD
|
||||||
* @param {(string | number)} workflowId The id of the workflow to get static data of
|
* @param {(string | number)} workflowId The id of the workflow to get static data of
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
export async function getStaticDataById(workflowId: string | number) {
|
export async function getStaticDataById(workflowId: string | number) {
|
||||||
const workflowData = await Db.collections.Workflow!
|
const workflowData = await Db.collections.Workflow!.findOne(workflowId, {
|
||||||
.findOne(workflowId, { select: ['staticData']});
|
select: ['staticData'],
|
||||||
|
});
|
||||||
|
|
||||||
if (workflowData === undefined) {
|
if (workflowData === undefined) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
return workflowData.staticData || {};
|
return workflowData.staticData || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// TODO: Deduplicate `validateWorkflow` and `throwDuplicateEntryError` with TagHelpers?
|
// TODO: Deduplicate `validateWorkflow` and `throwDuplicateEntryError` with TagHelpers?
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
export async function validateWorkflow(newWorkflow: WorkflowEntity) {
|
export async function validateWorkflow(newWorkflow: WorkflowEntity) {
|
||||||
const errors = await validate(newWorkflow);
|
const errors = await validate(newWorkflow);
|
||||||
|
|
||||||
|
@ -373,10 +394,15 @@ export async function validateWorkflow(newWorkflow: WorkflowEntity) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
export function throwDuplicateEntryError(error: Error) {
|
export function throwDuplicateEntryError(error: Error) {
|
||||||
const errorMessage = error.message.toLowerCase();
|
const errorMessage = error.message.toLowerCase();
|
||||||
if (errorMessage.includes('unique') || errorMessage.includes('duplicate')) {
|
if (errorMessage.includes('unique') || errorMessage.includes('duplicate')) {
|
||||||
throw new ResponseHelper.ResponseError('There is already a workflow with this name', undefined, 400);
|
throw new ResponseHelper.ResponseError(
|
||||||
|
'There is already a workflow with this name',
|
||||||
|
undefined,
|
||||||
|
400,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new ResponseHelper.ResponseError(errorMessage, undefined, 400);
|
throw new ResponseHelper.ResponseError(errorMessage, undefined, 400);
|
||||||
|
@ -386,6 +412,5 @@ export type WorkflowNameRequest = Express.Request & {
|
||||||
query: {
|
query: {
|
||||||
name?: string;
|
name?: string;
|
||||||
offset?: string;
|
offset?: string;
|
||||||
}
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,37 @@
|
||||||
|
/* eslint-disable no-restricted-syntax */
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||||
|
/* eslint-disable @typescript-eslint/no-shadow */
|
||||||
|
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
|
/* eslint-disable @typescript-eslint/prefer-optional-chain */
|
||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
/* eslint-disable import/no-cycle */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
import { IProcessMessage, WorkflowExecute } from 'n8n-core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ExecutionError,
|
||||||
|
IRun,
|
||||||
|
IWorkflowBase,
|
||||||
|
LoggerProxy as Logger,
|
||||||
|
Workflow,
|
||||||
|
WorkflowExecuteMode,
|
||||||
|
WorkflowHooks,
|
||||||
|
WorkflowOperationError,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
import * as PCancelable from 'p-cancelable';
|
||||||
|
import { join as pathJoin } from 'path';
|
||||||
|
import { fork } from 'child_process';
|
||||||
|
|
||||||
|
import * as Bull from 'bull';
|
||||||
|
import * as config from '../config';
|
||||||
|
// eslint-disable-next-line import/no-cycle
|
||||||
import {
|
import {
|
||||||
ActiveExecutions,
|
ActiveExecutions,
|
||||||
CredentialsOverwrites,
|
CredentialsOverwrites,
|
||||||
|
@ -20,38 +54,17 @@ import {
|
||||||
ResponseHelper,
|
ResponseHelper,
|
||||||
WorkflowExecuteAdditionalData,
|
WorkflowExecuteAdditionalData,
|
||||||
WorkflowHelpers,
|
WorkflowHelpers,
|
||||||
} from './';
|
} from '.';
|
||||||
|
|
||||||
import {
|
|
||||||
IProcessMessage,
|
|
||||||
WorkflowExecute,
|
|
||||||
} from 'n8n-core';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ExecutionError,
|
|
||||||
IRun,
|
|
||||||
IWorkflowBase,
|
|
||||||
LoggerProxy as Logger,
|
|
||||||
Workflow,
|
|
||||||
WorkflowExecuteMode,
|
|
||||||
WorkflowHooks,
|
|
||||||
WorkflowOperationError,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
import * as config from '../config';
|
|
||||||
import * as PCancelable from 'p-cancelable';
|
|
||||||
import { join as pathJoin } from 'path';
|
|
||||||
import { fork } from 'child_process';
|
|
||||||
|
|
||||||
import * as Bull from 'bull';
|
|
||||||
import * as Queue from './Queue';
|
import * as Queue from './Queue';
|
||||||
|
|
||||||
export class WorkflowRunner {
|
export class WorkflowRunner {
|
||||||
activeExecutions: ActiveExecutions.ActiveExecutions;
|
activeExecutions: ActiveExecutions.ActiveExecutions;
|
||||||
credentialsOverwrites: ICredentialsOverwrite;
|
|
||||||
push: Push.Push;
|
|
||||||
jobQueue: Bull.Queue;
|
|
||||||
|
|
||||||
|
credentialsOverwrites: ICredentialsOverwrite;
|
||||||
|
|
||||||
|
push: Push.Push;
|
||||||
|
|
||||||
|
jobQueue: Bull.Queue;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.push = Push.getInstance();
|
this.push = Push.getInstance();
|
||||||
|
@ -65,7 +78,6 @@ export class WorkflowRunner {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The process did send a hook message so execute the appropiate hook
|
* The process did send a hook message so execute the appropiate hook
|
||||||
*
|
*
|
||||||
|
@ -74,10 +86,10 @@ export class WorkflowRunner {
|
||||||
* @memberof WorkflowRunner
|
* @memberof WorkflowRunner
|
||||||
*/
|
*/
|
||||||
processHookMessage(workflowHooks: WorkflowHooks, hookData: IProcessMessageDataHook) {
|
processHookMessage(workflowHooks: WorkflowHooks, hookData: IProcessMessageDataHook) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
workflowHooks.executeHookFunctions(hookData.hook, hookData.parameters);
|
workflowHooks.executeHookFunctions(hookData.hook, hookData.parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The process did error
|
* The process did error
|
||||||
*
|
*
|
||||||
|
@ -87,7 +99,13 @@ export class WorkflowRunner {
|
||||||
* @param {string} executionId
|
* @param {string} executionId
|
||||||
* @memberof WorkflowRunner
|
* @memberof WorkflowRunner
|
||||||
*/
|
*/
|
||||||
async processError(error: ExecutionError, startedAt: Date, executionMode: WorkflowExecuteMode, executionId: string, hooks?: WorkflowHooks) {
|
async processError(
|
||||||
|
error: ExecutionError,
|
||||||
|
startedAt: Date,
|
||||||
|
executionMode: WorkflowExecuteMode,
|
||||||
|
executionId: string,
|
||||||
|
hooks?: WorkflowHooks,
|
||||||
|
) {
|
||||||
const fullRunData: IRun = {
|
const fullRunData: IRun = {
|
||||||
data: {
|
data: {
|
||||||
resultData: {
|
resultData: {
|
||||||
|
@ -123,28 +141,33 @@ export class WorkflowRunner {
|
||||||
* @returns {Promise<string>}
|
* @returns {Promise<string>}
|
||||||
* @memberof WorkflowRunner
|
* @memberof WorkflowRunner
|
||||||
*/
|
*/
|
||||||
async run(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean, realtime?: boolean): Promise<string> {
|
async run(
|
||||||
|
data: IWorkflowExecutionDataProcess,
|
||||||
|
loadStaticData?: boolean,
|
||||||
|
realtime?: boolean,
|
||||||
|
executionId?: string,
|
||||||
|
): Promise<string> {
|
||||||
const executionsProcess = config.get('executions.process') as string;
|
const executionsProcess = config.get('executions.process') as string;
|
||||||
const executionsMode = config.get('executions.mode') as string;
|
const executionsMode = config.get('executions.mode') as string;
|
||||||
|
|
||||||
let executionId: string;
|
|
||||||
if (executionsMode === 'queue' && data.executionMode !== 'manual') {
|
if (executionsMode === 'queue' && data.executionMode !== 'manual') {
|
||||||
// Do not run "manual" executions in bull because sending events to the
|
// Do not run "manual" executions in bull because sending events to the
|
||||||
// frontend would not be possible
|
// frontend would not be possible
|
||||||
executionId = await this.runBull(data, loadStaticData, realtime);
|
executionId = await this.runBull(data, loadStaticData, realtime, executionId);
|
||||||
} else if (executionsProcess === 'main') {
|
} else if (executionsProcess === 'main') {
|
||||||
executionId = await this.runMainProcess(data, loadStaticData);
|
executionId = await this.runMainProcess(data, loadStaticData, executionId);
|
||||||
} else {
|
} else {
|
||||||
executionId = await this.runSubprocess(data, loadStaticData);
|
executionId = await this.runSubprocess(data, loadStaticData, executionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const externalHooks = ExternalHooks();
|
const externalHooks = ExternalHooks();
|
||||||
if (externalHooks.exists('workflow.postExecute')) {
|
if (externalHooks.exists('workflow.postExecute')) {
|
||||||
this.activeExecutions.getPostExecutePromise(executionId)
|
this.activeExecutions
|
||||||
|
.getPostExecutePromise(executionId)
|
||||||
.then(async (executionData) => {
|
.then(async (executionData) => {
|
||||||
await externalHooks.run('workflow.postExecute', [executionData, data.workflowData]);
|
await externalHooks.run('workflow.postExecute', [executionData, data.workflowData]);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
console.error('There was a problem running hook "workflow.postExecute"', error);
|
console.error('There was a problem running hook "workflow.postExecute"', error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -152,7 +175,6 @@ export class WorkflowRunner {
|
||||||
return executionId;
|
return executionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run the workflow in current process
|
* Run the workflow in current process
|
||||||
*
|
*
|
||||||
|
@ -162,9 +184,15 @@ export class WorkflowRunner {
|
||||||
* @returns {Promise<string>}
|
* @returns {Promise<string>}
|
||||||
* @memberof WorkflowRunner
|
* @memberof WorkflowRunner
|
||||||
*/
|
*/
|
||||||
async runMainProcess(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean): Promise<string> {
|
async runMainProcess(
|
||||||
|
data: IWorkflowExecutionDataProcess,
|
||||||
|
loadStaticData?: boolean,
|
||||||
|
restartExecutionId?: string,
|
||||||
|
): Promise<string> {
|
||||||
if (loadStaticData === true && data.workflowData.id) {
|
if (loadStaticData === true && data.workflowData.id) {
|
||||||
data.workflowData.staticData = await WorkflowHelpers.getStaticDataById(data.workflowData.id as string);
|
data.workflowData.staticData = await WorkflowHelpers.getStaticDataById(
|
||||||
|
data.workflowData.id as string,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodeTypes = NodeTypes();
|
const nodeTypes = NodeTypes();
|
||||||
|
@ -175,64 +203,120 @@ export class WorkflowRunner {
|
||||||
let executionTimeout: NodeJS.Timeout;
|
let executionTimeout: NodeJS.Timeout;
|
||||||
let workflowTimeout = config.get('executions.timeout') as number; // initialize with default
|
let workflowTimeout = config.get('executions.timeout') as number; // initialize with default
|
||||||
if (data.workflowData.settings && data.workflowData.settings.executionTimeout) {
|
if (data.workflowData.settings && data.workflowData.settings.executionTimeout) {
|
||||||
workflowTimeout = data.workflowData.settings!.executionTimeout as number; // preference on workflow setting
|
workflowTimeout = data.workflowData.settings.executionTimeout as number; // preference on workflow setting
|
||||||
}
|
}
|
||||||
|
|
||||||
if (workflowTimeout > 0) {
|
if (workflowTimeout > 0) {
|
||||||
workflowTimeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number);
|
workflowTimeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number);
|
||||||
}
|
}
|
||||||
|
|
||||||
const workflow = new Workflow({ id: data.workflowData.id as string | undefined, name: data.workflowData.name, nodes: data.workflowData!.nodes, connections: data.workflowData!.connections, active: data.workflowData!.active, nodeTypes, staticData: data.workflowData!.staticData });
|
const workflow = new Workflow({
|
||||||
const additionalData = await WorkflowExecuteAdditionalData.getBase(data.credentials, undefined, workflowTimeout <= 0 ? undefined : Date.now() + workflowTimeout * 1000);
|
id: data.workflowData.id as string | undefined,
|
||||||
|
name: data.workflowData.name,
|
||||||
|
nodes: data.workflowData.nodes,
|
||||||
|
connections: data.workflowData.connections,
|
||||||
|
active: data.workflowData.active,
|
||||||
|
nodeTypes,
|
||||||
|
staticData: data.workflowData.staticData,
|
||||||
|
});
|
||||||
|
const additionalData = await WorkflowExecuteAdditionalData.getBase(
|
||||||
|
undefined,
|
||||||
|
workflowTimeout <= 0 ? undefined : Date.now() + workflowTimeout * 1000,
|
||||||
|
);
|
||||||
|
|
||||||
// Register the active execution
|
// Register the active execution
|
||||||
const executionId = await this.activeExecutions.add(data, undefined);
|
const executionId = await this.activeExecutions.add(data, undefined, restartExecutionId);
|
||||||
|
additionalData.executionId = executionId;
|
||||||
|
|
||||||
|
Logger.verbose(
|
||||||
|
`Execution for workflow ${data.workflowData.name} was assigned id ${executionId}`,
|
||||||
|
{ executionId },
|
||||||
|
);
|
||||||
let workflowExecution: PCancelable<IRun>;
|
let workflowExecution: PCancelable<IRun>;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Logger.verbose(`Execution for workflow ${data.workflowData.name} was assigned id ${executionId}`, { executionId });
|
Logger.verbose(
|
||||||
additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId, true);
|
`Execution for workflow ${data.workflowData.name} was assigned id ${executionId}`,
|
||||||
additionalData.sendMessageToUI = WorkflowExecuteAdditionalData.sendMessageToUI.bind({sessionId: data.sessionId});
|
{ executionId },
|
||||||
|
);
|
||||||
|
additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(
|
||||||
|
data,
|
||||||
|
executionId,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
additionalData.sendMessageToUI = WorkflowExecuteAdditionalData.sendMessageToUI.bind({
|
||||||
|
sessionId: data.sessionId,
|
||||||
|
});
|
||||||
|
|
||||||
if (data.executionData !== undefined) {
|
if (data.executionData !== undefined) {
|
||||||
Logger.debug(`Execution ID ${executionId} had Execution data. Running with payload.`, {executionId});
|
Logger.debug(`Execution ID ${executionId} had Execution data. Running with payload.`, {
|
||||||
const workflowExecute = new WorkflowExecute(additionalData, data.executionMode, data.executionData);
|
executionId,
|
||||||
|
});
|
||||||
|
const workflowExecute = new WorkflowExecute(
|
||||||
|
additionalData,
|
||||||
|
data.executionMode,
|
||||||
|
data.executionData,
|
||||||
|
);
|
||||||
workflowExecution = workflowExecute.processRunExecutionData(workflow);
|
workflowExecution = workflowExecute.processRunExecutionData(workflow);
|
||||||
} else if (data.runData === undefined || data.startNodes === undefined || data.startNodes.length === 0 || data.destinationNode === undefined) {
|
} else if (
|
||||||
Logger.debug(`Execution ID ${executionId} will run executing all nodes.`, {executionId});
|
data.runData === undefined ||
|
||||||
|
data.startNodes === undefined ||
|
||||||
|
data.startNodes.length === 0 ||
|
||||||
|
data.destinationNode === undefined
|
||||||
|
) {
|
||||||
|
Logger.debug(`Execution ID ${executionId} will run executing all nodes.`, { executionId });
|
||||||
// Execute all nodes
|
// Execute all nodes
|
||||||
|
|
||||||
// Can execute without webhook so go on
|
// Can execute without webhook so go on
|
||||||
const workflowExecute = new WorkflowExecute(additionalData, data.executionMode);
|
const workflowExecute = new WorkflowExecute(additionalData, data.executionMode);
|
||||||
workflowExecution = workflowExecute.run(workflow, undefined, data.destinationNode);
|
workflowExecution = workflowExecute.run(workflow, undefined, data.destinationNode);
|
||||||
} else {
|
} else {
|
||||||
Logger.debug(`Execution ID ${executionId} is a partial execution.`, {executionId});
|
Logger.debug(`Execution ID ${executionId} is a partial execution.`, { executionId });
|
||||||
// Execute only the nodes between start and destination nodes
|
// Execute only the nodes between start and destination nodes
|
||||||
const workflowExecute = new WorkflowExecute(additionalData, data.executionMode);
|
const workflowExecute = new WorkflowExecute(additionalData, data.executionMode);
|
||||||
workflowExecution = workflowExecute.runPartialWorkflow(workflow, data.runData, data.startNodes, data.destinationNode);
|
workflowExecution = workflowExecute.runPartialWorkflow(
|
||||||
|
workflow,
|
||||||
|
data.runData,
|
||||||
|
data.startNodes,
|
||||||
|
data.destinationNode,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.activeExecutions.attachWorkflowExecution(executionId, workflowExecution);
|
this.activeExecutions.attachWorkflowExecution(executionId, workflowExecution);
|
||||||
|
|
||||||
if (workflowTimeout > 0) {
|
if (workflowTimeout > 0) {
|
||||||
const timeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number) * 1000; // as seconds
|
const timeout =
|
||||||
|
Math.min(workflowTimeout, config.get('executions.maxTimeout') as number) * 1000; // as seconds
|
||||||
executionTimeout = setTimeout(() => {
|
executionTimeout = setTimeout(() => {
|
||||||
this.activeExecutions.stopExecution(executionId, 'timeout');
|
this.activeExecutions.stopExecution(executionId, 'timeout');
|
||||||
}, timeout);
|
}, timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
workflowExecution.then((fullRunData) => {
|
workflowExecution
|
||||||
clearTimeout(executionTimeout);
|
.then((fullRunData) => {
|
||||||
if (workflowExecution.isCanceled) {
|
clearTimeout(executionTimeout);
|
||||||
fullRunData.finished = false;
|
if (workflowExecution.isCanceled) {
|
||||||
}
|
fullRunData.finished = false;
|
||||||
this.activeExecutions.remove(executionId, fullRunData);
|
}
|
||||||
}).catch((error) => {
|
this.activeExecutions.remove(executionId, fullRunData);
|
||||||
this.processError(error, new Date(), data.executionMode, executionId, additionalData.hooks);
|
})
|
||||||
});
|
.catch((error) => {
|
||||||
|
this.processError(
|
||||||
|
error,
|
||||||
|
new Date(),
|
||||||
|
data.executionMode,
|
||||||
|
executionId,
|
||||||
|
additionalData.hooks,
|
||||||
|
);
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await this.processError(error, new Date(), data.executionMode, executionId, additionalData.hooks);
|
await this.processError(
|
||||||
|
error,
|
||||||
|
new Date(),
|
||||||
|
data.executionMode,
|
||||||
|
executionId,
|
||||||
|
additionalData.hooks,
|
||||||
|
);
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
@ -240,12 +324,16 @@ export class WorkflowRunner {
|
||||||
return executionId;
|
return executionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
async runBull(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean, realtime?: boolean): Promise<string> {
|
async runBull(
|
||||||
|
data: IWorkflowExecutionDataProcess,
|
||||||
|
loadStaticData?: boolean,
|
||||||
|
realtime?: boolean,
|
||||||
|
restartExecutionId?: string,
|
||||||
|
): Promise<string> {
|
||||||
// TODO: If "loadStaticData" is set to true it has to load data new on worker
|
// TODO: If "loadStaticData" is set to true it has to load data new on worker
|
||||||
|
|
||||||
// Register the active execution
|
// Register the active execution
|
||||||
const executionId = await this.activeExecutions.add(data, undefined);
|
const executionId = await this.activeExecutions.add(data, undefined, restartExecutionId);
|
||||||
|
|
||||||
const jobData: IBullJobData = {
|
const jobData: IBullJobData = {
|
||||||
executionId,
|
executionId,
|
||||||
|
@ -269,9 +357,14 @@ export class WorkflowRunner {
|
||||||
try {
|
try {
|
||||||
job = await this.jobQueue.add(jobData, jobOptions);
|
job = await this.jobQueue.add(jobData, jobOptions);
|
||||||
|
|
||||||
console.log('Started with ID: ' + job.id.toString());
|
console.log(`Started with ID: ${job.id.toString()}`);
|
||||||
|
|
||||||
hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerMain(data.executionMode, executionId, data.workflowData, { retryOf: data.retryOf ? data.retryOf.toString() : undefined });
|
hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerMain(
|
||||||
|
data.executionMode,
|
||||||
|
executionId,
|
||||||
|
data.workflowData,
|
||||||
|
{ retryOf: data.retryOf ? data.retryOf.toString() : undefined },
|
||||||
|
);
|
||||||
|
|
||||||
// Normally also workflow should be supplied here but as it only used for sending
|
// Normally also workflow should be supplied here but as it only used for sending
|
||||||
// data to editor-UI is not needed.
|
// data to editor-UI is not needed.
|
||||||
|
@ -279,130 +372,154 @@ export class WorkflowRunner {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the
|
// We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the
|
||||||
// "workflowExecuteAfter" which we require.
|
// "workflowExecuteAfter" which we require.
|
||||||
const hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(data.executionMode, executionId, data.workflowData, { retryOf: data.retryOf ? data.retryOf.toString() : undefined });
|
const hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(
|
||||||
|
data.executionMode,
|
||||||
|
executionId,
|
||||||
|
data.workflowData,
|
||||||
|
{ retryOf: data.retryOf ? data.retryOf.toString() : undefined },
|
||||||
|
);
|
||||||
await this.processError(error, new Date(), data.executionMode, executionId, hooks);
|
await this.processError(error, new Date(), data.executionMode, executionId, hooks);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const workflowExecution: PCancelable<IRun> = new PCancelable(async (resolve, reject, onCancel) => {
|
const workflowExecution: PCancelable<IRun> = new PCancelable(
|
||||||
onCancel.shouldReject = false;
|
async (resolve, reject, onCancel) => {
|
||||||
onCancel(async () => {
|
onCancel.shouldReject = false;
|
||||||
await Queue.getInstance().stopJob(job);
|
onCancel(async () => {
|
||||||
|
await Queue.getInstance().stopJob(job);
|
||||||
|
|
||||||
// We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the
|
// We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the
|
||||||
// "workflowExecuteAfter" which we require.
|
// "workflowExecuteAfter" which we require.
|
||||||
const hooksWorker = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(data.executionMode, executionId, data.workflowData, { retryOf: data.retryOf ? data.retryOf.toString() : undefined });
|
const hooksWorker = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(
|
||||||
|
data.executionMode,
|
||||||
|
executionId,
|
||||||
|
data.workflowData,
|
||||||
|
{ retryOf: data.retryOf ? data.retryOf.toString() : undefined },
|
||||||
|
);
|
||||||
|
|
||||||
const error = new WorkflowOperationError('Workflow-Execution has been canceled!');
|
const error = new WorkflowOperationError('Workflow-Execution has been canceled!');
|
||||||
await this.processError(error, new Date(), data.executionMode, executionId, hooksWorker);
|
await this.processError(error, new Date(), data.executionMode, executionId, hooksWorker);
|
||||||
|
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
|
||||||
|
|
||||||
const jobData: Promise<IBullJobResponse> = job.finished();
|
|
||||||
|
|
||||||
const queueRecoveryInterval = config.get('queue.bull.queueRecoveryInterval') as number;
|
|
||||||
|
|
||||||
const racingPromises: Array<Promise<IBullJobResponse | object>> = [jobData];
|
|
||||||
|
|
||||||
let clearWatchdogInterval;
|
|
||||||
if (queueRecoveryInterval > 0) {
|
|
||||||
/*************************************************
|
|
||||||
* Long explanation about what this solves: *
|
|
||||||
* This only happens in a very specific scenario *
|
|
||||||
* when Redis crashes and recovers shortly *
|
|
||||||
* but during this time, some execution(s) *
|
|
||||||
* finished. The end result is that the main *
|
|
||||||
* process will wait indefinitively and never *
|
|
||||||
* get a response. This adds an active polling to*
|
|
||||||
* the queue that allows us to identify that the *
|
|
||||||
* execution finished and get information from *
|
|
||||||
* the database. *
|
|
||||||
*************************************************/
|
|
||||||
let watchDogInterval: NodeJS.Timeout | undefined;
|
|
||||||
|
|
||||||
const watchDog: Promise<object> = new Promise((res) => {
|
|
||||||
watchDogInterval = setInterval(async () => {
|
|
||||||
const currentJob = await this.jobQueue.getJob(job.id);
|
|
||||||
// When null means job is finished (not found in queue)
|
|
||||||
if (currentJob === null) {
|
|
||||||
// Mimic worker's success message
|
|
||||||
res({success: true});
|
|
||||||
}
|
|
||||||
}, queueRecoveryInterval * 1000);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
racingPromises.push(watchDog);
|
const jobData: Promise<IBullJobResponse> = job.finished();
|
||||||
|
|
||||||
clearWatchdogInterval = () => {
|
const queueRecoveryInterval = config.get('queue.bull.queueRecoveryInterval') as number;
|
||||||
if (watchDogInterval) {
|
|
||||||
clearInterval(watchDogInterval);
|
const racingPromises: Array<Promise<IBullJobResponse | object>> = [jobData];
|
||||||
watchDogInterval = undefined;
|
|
||||||
|
let clearWatchdogInterval;
|
||||||
|
if (queueRecoveryInterval > 0) {
|
||||||
|
/** ***********************************************
|
||||||
|
* Long explanation about what this solves: *
|
||||||
|
* This only happens in a very specific scenario *
|
||||||
|
* when Redis crashes and recovers shortly *
|
||||||
|
* but during this time, some execution(s) *
|
||||||
|
* finished. The end result is that the main *
|
||||||
|
* process will wait indefinitively and never *
|
||||||
|
* get a response. This adds an active polling to*
|
||||||
|
* the queue that allows us to identify that the *
|
||||||
|
* execution finished and get information from *
|
||||||
|
* the database. *
|
||||||
|
************************************************ */
|
||||||
|
let watchDogInterval: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
|
const watchDog: Promise<object> = new Promise((res) => {
|
||||||
|
watchDogInterval = setInterval(async () => {
|
||||||
|
const currentJob = await this.jobQueue.getJob(job.id);
|
||||||
|
// When null means job is finished (not found in queue)
|
||||||
|
if (currentJob === null) {
|
||||||
|
// Mimic worker's success message
|
||||||
|
res({ success: true });
|
||||||
|
}
|
||||||
|
}, queueRecoveryInterval * 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
racingPromises.push(watchDog);
|
||||||
|
|
||||||
|
clearWatchdogInterval = () => {
|
||||||
|
if (watchDogInterval) {
|
||||||
|
clearInterval(watchDogInterval);
|
||||||
|
watchDogInterval = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.race(racingPromises);
|
||||||
|
if (clearWatchdogInterval !== undefined) {
|
||||||
|
clearWatchdogInterval();
|
||||||
}
|
}
|
||||||
};
|
} catch (error) {
|
||||||
}
|
// We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the
|
||||||
|
// "workflowExecuteAfter" which we require.
|
||||||
|
const hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(
|
||||||
|
data.executionMode,
|
||||||
|
executionId,
|
||||||
|
data.workflowData,
|
||||||
|
{ retryOf: data.retryOf ? data.retryOf.toString() : undefined },
|
||||||
|
);
|
||||||
|
Logger.error(`Problem with execution ${executionId}: ${error.message}. Aborting.`);
|
||||||
|
if (clearWatchdogInterval !== undefined) {
|
||||||
|
clearWatchdogInterval();
|
||||||
|
}
|
||||||
|
await this.processError(error, new Date(), data.executionMode, executionId, hooks);
|
||||||
|
|
||||||
try {
|
reject(error);
|
||||||
await Promise.race(racingPromises);
|
|
||||||
if (clearWatchdogInterval !== undefined) {
|
|
||||||
clearWatchdogInterval();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the
|
|
||||||
// "workflowExecuteAfter" which we require.
|
|
||||||
const hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(data.executionMode, executionId, data.workflowData, { retryOf: data.retryOf ? data.retryOf.toString() : undefined });
|
|
||||||
Logger.error(`Problem with execution ${executionId}: ${error.message}. Aborting.`);
|
|
||||||
if (clearWatchdogInterval !== undefined) {
|
|
||||||
clearWatchdogInterval();
|
|
||||||
}
|
|
||||||
await this.processError(error, new Date(), data.executionMode, executionId, hooks);
|
|
||||||
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const executionDb = await Db.collections.Execution!.findOne(executionId) as IExecutionFlattedDb;
|
|
||||||
const fullExecutionData = ResponseHelper.unflattenExecutionData(executionDb) as IExecutionResponse;
|
|
||||||
const runData = {
|
|
||||||
data: fullExecutionData.data,
|
|
||||||
finished: fullExecutionData.finished,
|
|
||||||
mode: fullExecutionData.mode,
|
|
||||||
startedAt: fullExecutionData.startedAt,
|
|
||||||
stoppedAt: fullExecutionData.stoppedAt,
|
|
||||||
} as IRun;
|
|
||||||
|
|
||||||
this.activeExecutions.remove(executionId, runData);
|
|
||||||
// Normally also static data should be supplied here but as it only used for sending
|
|
||||||
// data to editor-UI is not needed.
|
|
||||||
hooks.executeHookFunctions('workflowExecuteAfter', [runData]);
|
|
||||||
try {
|
|
||||||
// Check if this execution data has to be removed from database
|
|
||||||
// based on workflow settings.
|
|
||||||
let saveDataErrorExecution = config.get('executions.saveDataOnError') as string;
|
|
||||||
let saveDataSuccessExecution = config.get('executions.saveDataOnSuccess') as string;
|
|
||||||
if (data.workflowData.settings !== undefined) {
|
|
||||||
saveDataErrorExecution = (data.workflowData.settings.saveDataErrorExecution as string) || saveDataErrorExecution;
|
|
||||||
saveDataSuccessExecution = (data.workflowData.settings.saveDataSuccessExecution as string) || saveDataSuccessExecution;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const workflowDidSucceed = !runData.data.resultData.error;
|
const executionDb = (await Db.collections.Execution!.findOne(
|
||||||
if (workflowDidSucceed === true && saveDataSuccessExecution === 'none' ||
|
executionId,
|
||||||
workflowDidSucceed === false && saveDataErrorExecution === 'none'
|
)) as IExecutionFlattedDb;
|
||||||
) {
|
const fullExecutionData = ResponseHelper.unflattenExecutionData(executionDb);
|
||||||
await Db.collections.Execution!.delete(executionId);
|
const runData = {
|
||||||
}
|
data: fullExecutionData.data,
|
||||||
} catch (err) {
|
finished: fullExecutionData.finished,
|
||||||
// We don't want errors here to crash n8n. Just log and proceed.
|
mode: fullExecutionData.mode,
|
||||||
console.log('Error removing saved execution from database. More details: ', err);
|
startedAt: fullExecutionData.startedAt,
|
||||||
}
|
stoppedAt: fullExecutionData.stoppedAt,
|
||||||
|
} as IRun;
|
||||||
|
|
||||||
resolve(runData);
|
this.activeExecutions.remove(executionId, runData);
|
||||||
});
|
// Normally also static data should be supplied here but as it only used for sending
|
||||||
|
// data to editor-UI is not needed.
|
||||||
|
hooks.executeHookFunctions('workflowExecuteAfter', [runData]);
|
||||||
|
try {
|
||||||
|
// Check if this execution data has to be removed from database
|
||||||
|
// based on workflow settings.
|
||||||
|
let saveDataErrorExecution = config.get('executions.saveDataOnError') as string;
|
||||||
|
let saveDataSuccessExecution = config.get('executions.saveDataOnSuccess') as string;
|
||||||
|
if (data.workflowData.settings !== undefined) {
|
||||||
|
saveDataErrorExecution =
|
||||||
|
(data.workflowData.settings.saveDataErrorExecution as string) ||
|
||||||
|
saveDataErrorExecution;
|
||||||
|
saveDataSuccessExecution =
|
||||||
|
(data.workflowData.settings.saveDataSuccessExecution as string) ||
|
||||||
|
saveDataSuccessExecution;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workflowDidSucceed = !runData.data.resultData.error;
|
||||||
|
if (
|
||||||
|
(workflowDidSucceed && saveDataSuccessExecution === 'none') ||
|
||||||
|
(!workflowDidSucceed && saveDataErrorExecution === 'none')
|
||||||
|
) {
|
||||||
|
await Db.collections.Execution!.delete(executionId);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line id-denylist
|
||||||
|
} catch (err) {
|
||||||
|
// We don't want errors here to crash n8n. Just log and proceed.
|
||||||
|
console.log('Error removing saved execution from database. More details: ', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(runData);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
this.activeExecutions.attachWorkflowExecution(executionId, workflowExecution);
|
this.activeExecutions.attachWorkflowExecution(executionId, workflowExecution);
|
||||||
return executionId;
|
return executionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run the workflow
|
* Run the workflow
|
||||||
*
|
*
|
||||||
|
@ -412,16 +529,22 @@ export class WorkflowRunner {
|
||||||
* @returns {Promise<string>}
|
* @returns {Promise<string>}
|
||||||
* @memberof WorkflowRunner
|
* @memberof WorkflowRunner
|
||||||
*/
|
*/
|
||||||
async runSubprocess(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean): Promise<string> {
|
async runSubprocess(
|
||||||
|
data: IWorkflowExecutionDataProcess,
|
||||||
|
loadStaticData?: boolean,
|
||||||
|
restartExecutionId?: string,
|
||||||
|
): Promise<string> {
|
||||||
let startedAt = new Date();
|
let startedAt = new Date();
|
||||||
const subprocess = fork(pathJoin(__dirname, 'WorkflowRunnerProcess.js'));
|
const subprocess = fork(pathJoin(__dirname, 'WorkflowRunnerProcess.js'));
|
||||||
|
|
||||||
if (loadStaticData === true && data.workflowData.id) {
|
if (loadStaticData === true && data.workflowData.id) {
|
||||||
data.workflowData.staticData = await WorkflowHelpers.getStaticDataById(data.workflowData.id as string);
|
data.workflowData.staticData = await WorkflowHelpers.getStaticDataById(
|
||||||
|
data.workflowData.id as string,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the active execution
|
// Register the active execution
|
||||||
const executionId = await this.activeExecutions.add(data, subprocess);
|
const executionId = await this.activeExecutions.add(data, subprocess, restartExecutionId);
|
||||||
|
|
||||||
// Check if workflow contains a "executeWorkflow" Node as in this
|
// Check if workflow contains a "executeWorkflow" Node as in this
|
||||||
// case we can not know which nodeTypes and credentialTypes will
|
// case we can not know which nodeTypes and credentialTypes will
|
||||||
|
@ -433,12 +556,11 @@ export class WorkflowRunner {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let nodeTypeData: ITransferNodeTypes;
|
let nodeTypeData: ITransferNodeTypes;
|
||||||
let credentialTypeData: ICredentialsTypeData;
|
let credentialTypeData: ICredentialsTypeData;
|
||||||
|
// eslint-disable-next-line prefer-destructuring
|
||||||
let credentialsOverwrites = this.credentialsOverwrites;
|
let credentialsOverwrites = this.credentialsOverwrites;
|
||||||
|
if (loadAllNodeTypes) {
|
||||||
if (loadAllNodeTypes === true) {
|
|
||||||
// Supply all nodeTypes and credentialTypes
|
// Supply all nodeTypes and credentialTypes
|
||||||
nodeTypeData = WorkflowHelpers.getAllNodeTypeData();
|
nodeTypeData = WorkflowHelpers.getAllNodeTypeData();
|
||||||
const credentialTypes = CredentialTypes();
|
const credentialTypes = CredentialTypes();
|
||||||
|
@ -446,7 +568,7 @@ export class WorkflowRunner {
|
||||||
} else {
|
} else {
|
||||||
// Supply only nodeTypes, credentialTypes and overwrites that the workflow needs
|
// Supply only nodeTypes, credentialTypes and overwrites that the workflow needs
|
||||||
nodeTypeData = WorkflowHelpers.getNodeTypeData(data.workflowData.nodes);
|
nodeTypeData = WorkflowHelpers.getNodeTypeData(data.workflowData.nodes);
|
||||||
credentialTypeData = WorkflowHelpers.getCredentialsData(data.credentials);
|
credentialTypeData = WorkflowHelpers.getCredentialsDataByNodes(data.workflowData.nodes);
|
||||||
|
|
||||||
credentialsOverwrites = {};
|
credentialsOverwrites = {};
|
||||||
for (const credentialName of Object.keys(credentialTypeData)) {
|
for (const credentialName of Object.keys(credentialTypeData)) {
|
||||||
|
@ -458,8 +580,10 @@ export class WorkflowRunner {
|
||||||
|
|
||||||
(data as unknown as IWorkflowExecutionDataProcessWithExecution).executionId = executionId;
|
(data as unknown as IWorkflowExecutionDataProcessWithExecution).executionId = executionId;
|
||||||
(data as unknown as IWorkflowExecutionDataProcessWithExecution).nodeTypeData = nodeTypeData;
|
(data as unknown as IWorkflowExecutionDataProcessWithExecution).nodeTypeData = nodeTypeData;
|
||||||
(data as unknown as IWorkflowExecutionDataProcessWithExecution).credentialsOverwrite = credentialsOverwrites;
|
(data as unknown as IWorkflowExecutionDataProcessWithExecution).credentialsOverwrite =
|
||||||
(data as unknown as IWorkflowExecutionDataProcessWithExecution).credentialsTypeData = credentialTypeData; // TODO: Still needs correct value
|
this.credentialsOverwrites;
|
||||||
|
(data as unknown as IWorkflowExecutionDataProcessWithExecution).credentialsTypeData =
|
||||||
|
credentialTypeData;
|
||||||
|
|
||||||
const workflowHooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId);
|
const workflowHooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId);
|
||||||
|
|
||||||
|
@ -475,7 +599,7 @@ export class WorkflowRunner {
|
||||||
let executionTimeout: NodeJS.Timeout;
|
let executionTimeout: NodeJS.Timeout;
|
||||||
let workflowTimeout = config.get('executions.timeout') as number; // initialize with default
|
let workflowTimeout = config.get('executions.timeout') as number; // initialize with default
|
||||||
if (data.workflowData.settings && data.workflowData.settings.executionTimeout) {
|
if (data.workflowData.settings && data.workflowData.settings.executionTimeout) {
|
||||||
workflowTimeout = data.workflowData.settings!.executionTimeout as number; // preference on workflow setting
|
workflowTimeout = data.workflowData.settings.executionTimeout as number; // preference on workflow setting
|
||||||
}
|
}
|
||||||
|
|
||||||
const processTimeoutFunction = (timeout: number) => {
|
const processTimeoutFunction = (timeout: number) => {
|
||||||
|
@ -484,11 +608,16 @@ export class WorkflowRunner {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (workflowTimeout > 0) {
|
if (workflowTimeout > 0) {
|
||||||
workflowTimeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number) * 1000; // as seconds
|
workflowTimeout =
|
||||||
|
Math.min(workflowTimeout, config.get('executions.maxTimeout') as number) * 1000; // as seconds
|
||||||
// Start timeout already now but give process at least 5 seconds to start.
|
// Start timeout already now but give process at least 5 seconds to start.
|
||||||
// Without it could would it be possible that the workflow executions times out before it even got started if
|
// Without it could would it be possible that the workflow executions times out before it even got started if
|
||||||
// the timeout time is very short as the process start time can be quite long.
|
// the timeout time is very short as the process start time can be quite long.
|
||||||
executionTimeout = setTimeout(processTimeoutFunction, Math.max(5000, workflowTimeout), workflowTimeout);
|
executionTimeout = setTimeout(
|
||||||
|
processTimeoutFunction,
|
||||||
|
Math.max(5000, workflowTimeout),
|
||||||
|
workflowTimeout,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a list of child spawned executions
|
// Create a list of child spawned executions
|
||||||
|
@ -498,7 +627,10 @@ export class WorkflowRunner {
|
||||||
|
|
||||||
// Listen to data from the subprocess
|
// Listen to data from the subprocess
|
||||||
subprocess.on('message', async (message: IProcessMessage) => {
|
subprocess.on('message', async (message: IProcessMessage) => {
|
||||||
Logger.debug(`Received child process message of type ${message.type} for execution ID ${executionId}.`, {executionId});
|
Logger.debug(
|
||||||
|
`Received child process message of type ${message.type} for execution ID ${executionId}.`,
|
||||||
|
{ executionId },
|
||||||
|
);
|
||||||
if (message.type === 'start') {
|
if (message.type === 'start') {
|
||||||
// Now that the execution actually started set the timeout again so that does not time out to early.
|
// Now that the execution actually started set the timeout again so that does not time out to early.
|
||||||
startedAt = new Date();
|
startedAt = new Date();
|
||||||
|
@ -506,18 +638,25 @@ export class WorkflowRunner {
|
||||||
clearTimeout(executionTimeout);
|
clearTimeout(executionTimeout);
|
||||||
executionTimeout = setTimeout(processTimeoutFunction, workflowTimeout, workflowTimeout);
|
executionTimeout = setTimeout(processTimeoutFunction, workflowTimeout, workflowTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (message.type === 'end') {
|
} else if (message.type === 'end') {
|
||||||
clearTimeout(executionTimeout);
|
clearTimeout(executionTimeout);
|
||||||
this.activeExecutions.remove(executionId!, message.data.runData);
|
this.activeExecutions.remove(executionId, message.data.runData);
|
||||||
|
|
||||||
} else if (message.type === 'sendMessageToUI') {
|
} else if (message.type === 'sendMessageToUI') {
|
||||||
WorkflowExecuteAdditionalData.sendMessageToUI.bind({ sessionId: data.sessionId })(message.data.source, message.data.message);
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
|
WorkflowExecuteAdditionalData.sendMessageToUI.bind({ sessionId: data.sessionId })(
|
||||||
|
message.data.source,
|
||||||
|
message.data.message,
|
||||||
|
);
|
||||||
} else if (message.type === 'processError') {
|
} else if (message.type === 'processError') {
|
||||||
clearTimeout(executionTimeout);
|
clearTimeout(executionTimeout);
|
||||||
const executionError = message.data.executionError as ExecutionError;
|
const executionError = message.data.executionError as ExecutionError;
|
||||||
await this.processError(executionError, startedAt, data.executionMode, executionId, workflowHooks);
|
await this.processError(
|
||||||
|
executionError,
|
||||||
|
startedAt,
|
||||||
|
data.executionMode,
|
||||||
|
executionId,
|
||||||
|
workflowHooks,
|
||||||
|
);
|
||||||
} else if (message.type === 'processHook') {
|
} else if (message.type === 'processHook') {
|
||||||
this.processHookMessage(workflowHooks, message.data as IProcessMessageDataHook);
|
this.processHookMessage(workflowHooks, message.data as IProcessMessageDataHook);
|
||||||
} else if (message.type === 'timeout') {
|
} else if (message.type === 'timeout') {
|
||||||
|
@ -529,43 +668,61 @@ export class WorkflowRunner {
|
||||||
} else if (message.type === 'startExecution') {
|
} else if (message.type === 'startExecution') {
|
||||||
const executionId = await this.activeExecutions.add(message.data.runData);
|
const executionId = await this.activeExecutions.add(message.data.runData);
|
||||||
childExecutionIds.push(executionId);
|
childExecutionIds.push(executionId);
|
||||||
subprocess.send({ type: 'executionId', data: {executionId} } as IProcessMessage);
|
subprocess.send({ type: 'executionId', data: { executionId } } as IProcessMessage);
|
||||||
} else if (message.type === 'finishExecution') {
|
} else if (message.type === 'finishExecution') {
|
||||||
const executionIdIndex = childExecutionIds.indexOf(message.data.executionId);
|
const executionIdIndex = childExecutionIds.indexOf(message.data.executionId);
|
||||||
if (executionIdIndex !== -1) {
|
if (executionIdIndex !== -1) {
|
||||||
childExecutionIds.splice(executionIdIndex, 1);
|
childExecutionIds.splice(executionIdIndex, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/await-thenable
|
||||||
await this.activeExecutions.remove(message.data.executionId, message.data.result);
|
await this.activeExecutions.remove(message.data.executionId, message.data.result);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Also get informed when the processes does exit especially when it did crash or timed out
|
// Also get informed when the processes does exit especially when it did crash or timed out
|
||||||
subprocess.on('exit', async (code, signal) => {
|
subprocess.on('exit', async (code, signal) => {
|
||||||
if (signal === 'SIGTERM'){
|
if (signal === 'SIGTERM') {
|
||||||
Logger.debug(`Subprocess for execution ID ${executionId} timed out.`, {executionId});
|
Logger.debug(`Subprocess for execution ID ${executionId} timed out.`, { executionId });
|
||||||
// Execution timed out and its process has been terminated
|
// Execution timed out and its process has been terminated
|
||||||
const timeoutError = new WorkflowOperationError('Workflow execution timed out!');
|
const timeoutError = new WorkflowOperationError('Workflow execution timed out!');
|
||||||
|
|
||||||
await this.processError(timeoutError, startedAt, data.executionMode, executionId, workflowHooks);
|
await this.processError(
|
||||||
|
timeoutError,
|
||||||
|
startedAt,
|
||||||
|
data.executionMode,
|
||||||
|
executionId,
|
||||||
|
workflowHooks,
|
||||||
|
);
|
||||||
} else if (code !== 0) {
|
} else if (code !== 0) {
|
||||||
Logger.debug(`Subprocess for execution ID ${executionId} finished with error code ${code}.`, {executionId});
|
Logger.debug(
|
||||||
|
`Subprocess for execution ID ${executionId} finished with error code ${code}.`,
|
||||||
|
{ executionId },
|
||||||
|
);
|
||||||
// Process did exit with error code, so something went wrong.
|
// Process did exit with error code, so something went wrong.
|
||||||
const executionError = new WorkflowOperationError('Workflow execution process did crash for an unknown reason!');
|
const executionError = new WorkflowOperationError(
|
||||||
|
'Workflow execution process did crash for an unknown reason!',
|
||||||
|
);
|
||||||
|
|
||||||
await this.processError(executionError, startedAt, data.executionMode, executionId, workflowHooks);
|
await this.processError(
|
||||||
|
executionError,
|
||||||
|
startedAt,
|
||||||
|
data.executionMode,
|
||||||
|
executionId,
|
||||||
|
workflowHooks,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for(const executionId of childExecutionIds) {
|
for (const executionId of childExecutionIds) {
|
||||||
// When the child process exits, if we still have
|
// When the child process exits, if we still have
|
||||||
// pending child executions, we mark them as finished
|
// pending child executions, we mark them as finished
|
||||||
// They will display as unknown to the user
|
// They will display as unknown to the user
|
||||||
// Instead of pending forever as executing when it
|
// Instead of pending forever as executing when it
|
||||||
// actually isn't anymore.
|
// actually isn't anymore.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/await-thenable, no-await-in-loop
|
||||||
await this.activeExecutions.remove(executionId);
|
await this.activeExecutions.remove(executionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
clearTimeout(executionTimeout);
|
clearTimeout(executionTimeout);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,11 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
import {
|
/* eslint-disable consistent-return */
|
||||||
CredentialsOverwrites,
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
CredentialTypes,
|
/* eslint-disable @typescript-eslint/no-shadow */
|
||||||
Db,
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
ExternalHooks,
|
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||||
IWorkflowExecuteProcess,
|
/* eslint-disable @typescript-eslint/unbound-method */
|
||||||
IWorkflowExecutionDataProcessWithExecution,
|
import { IProcessMessage, WorkflowExecute } from 'n8n-core';
|
||||||
NodeTypes,
|
|
||||||
WorkflowExecuteAdditionalData,
|
|
||||||
WorkflowHelpers,
|
|
||||||
} from './';
|
|
||||||
|
|
||||||
import {
|
|
||||||
IProcessMessage,
|
|
||||||
WorkflowExecute,
|
|
||||||
} from 'n8n-core';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ExecutionError,
|
ExecutionError,
|
||||||
|
@ -34,24 +25,41 @@ import {
|
||||||
WorkflowHooks,
|
WorkflowHooks,
|
||||||
WorkflowOperationError,
|
WorkflowOperationError,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getLogger,
|
CredentialsOverwrites,
|
||||||
} from '../src/Logger';
|
CredentialTypes,
|
||||||
|
Db,
|
||||||
|
ExternalHooks,
|
||||||
|
IWorkflowExecuteProcess,
|
||||||
|
IWorkflowExecutionDataProcessWithExecution,
|
||||||
|
NodeTypes,
|
||||||
|
WorkflowExecuteAdditionalData,
|
||||||
|
WorkflowHelpers,
|
||||||
|
} from '.';
|
||||||
|
|
||||||
|
import { getLogger } from './Logger';
|
||||||
|
|
||||||
import * as config from '../config';
|
import * as config from '../config';
|
||||||
|
|
||||||
export class WorkflowRunnerProcess {
|
export class WorkflowRunnerProcess {
|
||||||
data: IWorkflowExecutionDataProcessWithExecution | undefined;
|
data: IWorkflowExecutionDataProcessWithExecution | undefined;
|
||||||
|
|
||||||
logger: ILogger;
|
logger: ILogger;
|
||||||
|
|
||||||
startedAt = new Date();
|
startedAt = new Date();
|
||||||
|
|
||||||
workflow: Workflow | undefined;
|
workflow: Workflow | undefined;
|
||||||
|
|
||||||
workflowExecute: WorkflowExecute | undefined;
|
workflowExecute: WorkflowExecute | undefined;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
||||||
executionIdCallback: (executionId: string) => void | undefined;
|
executionIdCallback: (executionId: string) => void | undefined;
|
||||||
|
|
||||||
childExecutions: {
|
childExecutions: {
|
||||||
[key: string]: IWorkflowExecuteProcess,
|
[key: string]: IWorkflowExecuteProcess;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
static async stopProcess() {
|
static async stopProcess() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Attempt a graceful shutdown, giving executions 30 seconds to finish
|
// Attempt a graceful shutdown, giving executions 30 seconds to finish
|
||||||
|
@ -59,17 +67,20 @@ export class WorkflowRunnerProcess {
|
||||||
}, 30000);
|
}, 30000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async runWorkflow(inputData: IWorkflowExecutionDataProcessWithExecution): Promise<IRun> {
|
async runWorkflow(inputData: IWorkflowExecutionDataProcessWithExecution): Promise<IRun> {
|
||||||
process.on('SIGTERM', WorkflowRunnerProcess.stopProcess);
|
process.on('SIGTERM', WorkflowRunnerProcess.stopProcess);
|
||||||
process.on('SIGINT', WorkflowRunnerProcess.stopProcess);
|
process.on('SIGINT', WorkflowRunnerProcess.stopProcess);
|
||||||
|
|
||||||
const logger = this.logger = getLogger();
|
// eslint-disable-next-line no-multi-assign
|
||||||
|
const logger = (this.logger = getLogger());
|
||||||
LoggerProxy.init(logger);
|
LoggerProxy.init(logger);
|
||||||
|
|
||||||
this.data = inputData;
|
this.data = inputData;
|
||||||
|
|
||||||
logger.verbose('Initializing n8n sub-process', { pid: process.pid, workflowId: this.data.workflowData.id });
|
logger.verbose('Initializing n8n sub-process', {
|
||||||
|
pid: process.pid,
|
||||||
|
workflowId: this.data.workflowData.id,
|
||||||
|
});
|
||||||
|
|
||||||
let className: string;
|
let className: string;
|
||||||
let tempNode: INodeType;
|
let tempNode: INodeType;
|
||||||
|
@ -78,13 +89,16 @@ export class WorkflowRunnerProcess {
|
||||||
this.startedAt = new Date();
|
this.startedAt = new Date();
|
||||||
|
|
||||||
const nodeTypesData: INodeTypeData = {};
|
const nodeTypesData: INodeTypeData = {};
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const nodeTypeName of Object.keys(this.data.nodeTypeData)) {
|
for (const nodeTypeName of Object.keys(this.data.nodeTypeData)) {
|
||||||
className = this.data.nodeTypeData[nodeTypeName].className;
|
className = this.data.nodeTypeData[nodeTypeName].className;
|
||||||
|
|
||||||
filePath = this.data.nodeTypeData[nodeTypeName].sourcePath;
|
filePath = this.data.nodeTypeData[nodeTypeName].sourcePath;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, import/no-dynamic-require, global-require, @typescript-eslint/no-var-requires
|
||||||
const tempModule = require(filePath);
|
const tempModule = require(filePath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||||
tempNode = new tempModule[className]() as INodeType;
|
tempNode = new tempModule[className]() as INodeType;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Error loading node "${nodeTypeName}" from: "${filePath}"`);
|
throw new Error(`Error loading node "${nodeTypeName}" from: "${filePath}"`);
|
||||||
|
@ -111,46 +125,93 @@ export class WorkflowRunnerProcess {
|
||||||
const externalHooks = ExternalHooks();
|
const externalHooks = ExternalHooks();
|
||||||
await externalHooks.init();
|
await externalHooks.init();
|
||||||
|
|
||||||
// This code has been split into 3 ifs just to make it easier to understand
|
// Credentials should now be loaded from database.
|
||||||
|
// We check if any node uses credentials. If it does, then
|
||||||
|
// init database.
|
||||||
|
let shouldInitializaDb = false;
|
||||||
|
// eslint-disable-next-line array-callback-return
|
||||||
|
inputData.workflowData.nodes.map((node) => {
|
||||||
|
if (Object.keys(node.credentials === undefined ? {} : node.credentials).length > 0) {
|
||||||
|
shouldInitializaDb = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// This code has been split into 4 ifs just to make it easier to understand
|
||||||
// Can be made smaller but in the end it will make it impossible to read.
|
// Can be made smaller but in the end it will make it impossible to read.
|
||||||
if (inputData.workflowData.settings !== undefined && inputData.workflowData.settings.saveExecutionProgress === true) {
|
if (shouldInitializaDb) {
|
||||||
|
// initialize db as we need to load credentials
|
||||||
|
await Db.init();
|
||||||
|
} else if (
|
||||||
|
inputData.workflowData.settings !== undefined &&
|
||||||
|
inputData.workflowData.settings.saveExecutionProgress === true
|
||||||
|
) {
|
||||||
// Workflow settings specifying it should save
|
// Workflow settings specifying it should save
|
||||||
await Db.init();
|
await Db.init();
|
||||||
} else if (inputData.workflowData.settings !== undefined && inputData.workflowData.settings.saveExecutionProgress !== false && config.get('executions.saveExecutionProgress') as boolean) {
|
} else if (
|
||||||
|
inputData.workflowData.settings !== undefined &&
|
||||||
|
inputData.workflowData.settings.saveExecutionProgress !== false &&
|
||||||
|
(config.get('executions.saveExecutionProgress') as boolean)
|
||||||
|
) {
|
||||||
// Workflow settings not saying anything about saving but default settings says so
|
// Workflow settings not saying anything about saving but default settings says so
|
||||||
await Db.init();
|
await Db.init();
|
||||||
} else if (inputData.workflowData.settings === undefined && config.get('executions.saveExecutionProgress') as boolean) {
|
} else if (
|
||||||
|
inputData.workflowData.settings === undefined &&
|
||||||
|
(config.get('executions.saveExecutionProgress') as boolean)
|
||||||
|
) {
|
||||||
// Workflow settings not saying anything about saving but default settings says so
|
// Workflow settings not saying anything about saving but default settings says so
|
||||||
await Db.init();
|
await Db.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start timeout for the execution
|
// Start timeout for the execution
|
||||||
let workflowTimeout = config.get('executions.timeout') as number; // initialize with default
|
let workflowTimeout = config.get('executions.timeout') as number; // initialize with default
|
||||||
|
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
|
||||||
if (this.data.workflowData.settings && this.data.workflowData.settings.executionTimeout) {
|
if (this.data.workflowData.settings && this.data.workflowData.settings.executionTimeout) {
|
||||||
workflowTimeout = this.data.workflowData.settings!.executionTimeout as number; // preference on workflow setting
|
workflowTimeout = this.data.workflowData.settings.executionTimeout as number; // preference on workflow setting
|
||||||
}
|
}
|
||||||
|
|
||||||
if (workflowTimeout > 0) {
|
if (workflowTimeout > 0) {
|
||||||
workflowTimeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number);
|
workflowTimeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.workflow = new Workflow({ id: this.data.workflowData.id as string | undefined, name: this.data.workflowData.name, nodes: this.data.workflowData!.nodes, connections: this.data.workflowData!.connections, active: this.data.workflowData!.active, nodeTypes, staticData: this.data.workflowData!.staticData, settings: this.data.workflowData!.settings });
|
this.workflow = new Workflow({
|
||||||
const additionalData = await WorkflowExecuteAdditionalData.getBase(this.data.credentials, undefined, workflowTimeout <= 0 ? undefined : Date.now() + workflowTimeout * 1000);
|
id: this.data.workflowData.id as string | undefined,
|
||||||
|
name: this.data.workflowData.name,
|
||||||
|
nodes: this.data.workflowData.nodes,
|
||||||
|
connections: this.data.workflowData.connections,
|
||||||
|
active: this.data.workflowData.active,
|
||||||
|
nodeTypes,
|
||||||
|
staticData: this.data.workflowData.staticData,
|
||||||
|
settings: this.data.workflowData.settings,
|
||||||
|
});
|
||||||
|
const additionalData = await WorkflowExecuteAdditionalData.getBase(
|
||||||
|
undefined,
|
||||||
|
workflowTimeout <= 0 ? undefined : Date.now() + workflowTimeout * 1000,
|
||||||
|
);
|
||||||
additionalData.hooks = this.getProcessForwardHooks();
|
additionalData.hooks = this.getProcessForwardHooks();
|
||||||
|
additionalData.executionId = inputData.executionId;
|
||||||
|
|
||||||
additionalData.sendMessageToUI = async (source: string, message: any) => { // tslint:disable-line:no-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
additionalData.sendMessageToUI = async (source: string, message: any) => {
|
||||||
if (workflowRunner.data!.executionMode !== 'manual') {
|
if (workflowRunner.data!.executionMode !== 'manual') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
await sendToParentProcess('sendMessageToUI', { source, message });
|
await sendToParentProcess('sendMessageToUI', { source, message });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`There was a problem sending UI data to parent process: "${error.message}"`);
|
this.logger.error(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
|
||||||
|
`There was a problem sending UI data to parent process: "${error.message}"`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const executeWorkflowFunction = additionalData.executeWorkflow;
|
const executeWorkflowFunction = additionalData.executeWorkflow;
|
||||||
additionalData.executeWorkflow = async (workflowInfo: IExecuteWorkflowInfo, additionalData: IWorkflowExecuteAdditionalData, inputData?: INodeExecutionData[] | undefined): Promise<Array<INodeExecutionData[] | null> | IRun> => {
|
additionalData.executeWorkflow = async (
|
||||||
|
workflowInfo: IExecuteWorkflowInfo,
|
||||||
|
additionalData: IWorkflowExecuteAdditionalData,
|
||||||
|
inputData?: INodeExecutionData[] | undefined,
|
||||||
|
): Promise<Array<INodeExecutionData[] | null> | IRun> => {
|
||||||
const workflowData = await WorkflowExecuteAdditionalData.getWorkflowData(workflowInfo);
|
const workflowData = await WorkflowExecuteAdditionalData.getWorkflowData(workflowInfo);
|
||||||
const runData = await WorkflowExecuteAdditionalData.getRunData(workflowData, inputData);
|
const runData = await WorkflowExecuteAdditionalData.getRunData(workflowData, inputData);
|
||||||
await sendToParentProcess('startExecution', { runData });
|
await sendToParentProcess('startExecution', { runData });
|
||||||
|
@ -161,11 +222,18 @@ export class WorkflowRunnerProcess {
|
||||||
});
|
});
|
||||||
let result: IRun;
|
let result: IRun;
|
||||||
try {
|
try {
|
||||||
const executeWorkflowFunctionOutput = await executeWorkflowFunction(workflowInfo, additionalData, inputData, executionId, workflowData, runData) as {workflowExecute: WorkflowExecute, workflow: Workflow} as IWorkflowExecuteProcess;
|
const executeWorkflowFunctionOutput = (await executeWorkflowFunction(
|
||||||
const workflowExecute = executeWorkflowFunctionOutput.workflowExecute;
|
workflowInfo,
|
||||||
|
additionalData,
|
||||||
|
inputData,
|
||||||
|
executionId,
|
||||||
|
workflowData,
|
||||||
|
runData,
|
||||||
|
)) as { workflowExecute: WorkflowExecute; workflow: Workflow } as IWorkflowExecuteProcess;
|
||||||
|
const { workflowExecute } = executeWorkflowFunctionOutput;
|
||||||
this.childExecutions[executionId] = executeWorkflowFunctionOutput;
|
this.childExecutions[executionId] = executeWorkflowFunctionOutput;
|
||||||
const workflow = executeWorkflowFunctionOutput.workflow;
|
const { workflow } = executeWorkflowFunctionOutput;
|
||||||
result = await workflowExecute.processRunExecutionData(workflow) as IRun;
|
result = await workflowExecute.processRunExecutionData(workflow);
|
||||||
await externalHooks.run('workflow.postExecute', [result, workflowData]);
|
await externalHooks.run('workflow.postExecute', [result, workflowData]);
|
||||||
await sendToParentProcess('finishExecution', { executionId, result });
|
await sendToParentProcess('finishExecution', { executionId, result });
|
||||||
delete this.childExecutions[executionId];
|
delete this.childExecutions[executionId];
|
||||||
|
@ -183,22 +251,35 @@ export class WorkflowRunnerProcess {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.data.executionData !== undefined) {
|
if (this.data.executionData !== undefined) {
|
||||||
this.workflowExecute = new WorkflowExecute(additionalData, this.data.executionMode, this.data.executionData);
|
this.workflowExecute = new WorkflowExecute(
|
||||||
|
additionalData,
|
||||||
|
this.data.executionMode,
|
||||||
|
this.data.executionData,
|
||||||
|
);
|
||||||
return this.workflowExecute.processRunExecutionData(this.workflow);
|
return this.workflowExecute.processRunExecutionData(this.workflow);
|
||||||
} else if (this.data.runData === undefined || this.data.startNodes === undefined || this.data.startNodes.length === 0 || this.data.destinationNode === undefined) {
|
}
|
||||||
|
if (
|
||||||
|
this.data.runData === undefined ||
|
||||||
|
this.data.startNodes === undefined ||
|
||||||
|
this.data.startNodes.length === 0 ||
|
||||||
|
this.data.destinationNode === undefined
|
||||||
|
) {
|
||||||
// Execute all nodes
|
// Execute all nodes
|
||||||
|
|
||||||
// Can execute without webhook so go on
|
// Can execute without webhook so go on
|
||||||
this.workflowExecute = new WorkflowExecute(additionalData, this.data.executionMode);
|
this.workflowExecute = new WorkflowExecute(additionalData, this.data.executionMode);
|
||||||
return this.workflowExecute.run(this.workflow, undefined, this.data.destinationNode);
|
return this.workflowExecute.run(this.workflow, undefined, this.data.destinationNode);
|
||||||
} else {
|
|
||||||
// Execute only the nodes between start and destination nodes
|
|
||||||
this.workflowExecute = new WorkflowExecute(additionalData, this.data.executionMode);
|
|
||||||
return this.workflowExecute.runPartialWorkflow(this.workflow, this.data.runData, this.data.startNodes, this.data.destinationNode);
|
|
||||||
}
|
}
|
||||||
|
// Execute only the nodes between start and destination nodes
|
||||||
|
this.workflowExecute = new WorkflowExecute(additionalData, this.data.executionMode);
|
||||||
|
return this.workflowExecute.runPartialWorkflow(
|
||||||
|
this.workflow,
|
||||||
|
this.data.runData,
|
||||||
|
this.data.startNodes,
|
||||||
|
this.data.destinationNode,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends hook data to the parent process that it executes them
|
* Sends hook data to the parent process that it executes them
|
||||||
*
|
*
|
||||||
|
@ -206,18 +287,18 @@ export class WorkflowRunnerProcess {
|
||||||
* @param {any[]} parameters
|
* @param {any[]} parameters
|
||||||
* @memberof WorkflowRunnerProcess
|
* @memberof WorkflowRunnerProcess
|
||||||
*/
|
*/
|
||||||
async sendHookToParentProcess(hook: string, parameters: any[]) { // tslint:disable-line:no-any
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
|
||||||
|
async sendHookToParentProcess(hook: string, parameters: any[]) {
|
||||||
try {
|
try {
|
||||||
await sendToParentProcess('processHook', {
|
await sendToParentProcess('processHook', {
|
||||||
hook,
|
hook,
|
||||||
parameters,
|
parameters,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`There was a problem sending hook: "${hook}"`, { parameters, error});
|
this.logger.error(`There was a problem sending hook: "${hook}"`, { parameters, error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a wrapper for hooks which simply forwards the data to
|
* Create a wrapper for hooks which simply forwards the data to
|
||||||
* the parent process where they then can be executed with access
|
* the parent process where they then can be executed with access
|
||||||
|
@ -250,6 +331,7 @@ export class WorkflowRunnerProcess {
|
||||||
};
|
};
|
||||||
|
|
||||||
const preExecuteFunctions = WorkflowExecuteAdditionalData.hookFunctionsPreExecute();
|
const preExecuteFunctions = WorkflowExecuteAdditionalData.hookFunctionsPreExecute();
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const key of Object.keys(preExecuteFunctions)) {
|
for (const key of Object.keys(preExecuteFunctions)) {
|
||||||
if (hookFunctions[key] === undefined) {
|
if (hookFunctions[key] === undefined) {
|
||||||
hookFunctions[key] = [];
|
hookFunctions[key] = [];
|
||||||
|
@ -257,13 +339,16 @@ export class WorkflowRunnerProcess {
|
||||||
hookFunctions[key]!.push.apply(hookFunctions[key], preExecuteFunctions[key]);
|
hookFunctions[key]!.push.apply(hookFunctions[key], preExecuteFunctions[key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new WorkflowHooks(hookFunctions, this.data!.executionMode, this.data!.executionId, this.data!.workflowData, { sessionId: this.data!.sessionId, retryOf: this.data!.retryOf as string });
|
return new WorkflowHooks(
|
||||||
|
hookFunctions,
|
||||||
|
this.data!.executionMode,
|
||||||
|
this.data!.executionId,
|
||||||
|
this.data!.workflowData,
|
||||||
|
{ sessionId: this.data!.sessionId, retryOf: this.data!.retryOf as string },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends data to parent process
|
* Sends data to parent process
|
||||||
*
|
*
|
||||||
|
@ -271,25 +356,27 @@ export class WorkflowRunnerProcess {
|
||||||
* @param {*} data The data
|
* @param {*} data The data
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async function sendToParentProcess(type: string, data: any): Promise<void> { // tslint:disable-line:no-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
async function sendToParentProcess(type: string, data: any): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
process.send!({
|
process.send!(
|
||||||
type,
|
{
|
||||||
data,
|
type,
|
||||||
}, (error: Error) => {
|
data,
|
||||||
if (error) {
|
},
|
||||||
return reject(error);
|
(error: Error) => {
|
||||||
}
|
if (error) {
|
||||||
|
return reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const workflowRunner = new WorkflowRunnerProcess();
|
const workflowRunner = new WorkflowRunnerProcess();
|
||||||
|
|
||||||
|
|
||||||
// Listen to messages from parent process which send the data of
|
// Listen to messages from parent process which send the data of
|
||||||
// the worflow to process
|
// the worflow to process
|
||||||
process.on('message', async (message: IProcessMessage) => {
|
process.on('message', async (message: IProcessMessage) => {
|
||||||
|
@ -310,25 +397,42 @@ process.on('message', async (message: IProcessMessage) => {
|
||||||
let runData: IRun;
|
let runData: IRun;
|
||||||
|
|
||||||
if (workflowRunner.workflowExecute !== undefined) {
|
if (workflowRunner.workflowExecute !== undefined) {
|
||||||
|
|
||||||
const executionIds = Object.keys(workflowRunner.childExecutions);
|
const executionIds = Object.keys(workflowRunner.childExecutions);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const executionId of executionIds) {
|
for (const executionId of executionIds) {
|
||||||
const childWorkflowExecute = workflowRunner.childExecutions[executionId];
|
const childWorkflowExecute = workflowRunner.childExecutions[executionId];
|
||||||
runData = childWorkflowExecute.workflowExecute.getFullRunData(workflowRunner.childExecutions[executionId].startedAt);
|
runData = childWorkflowExecute.workflowExecute.getFullRunData(
|
||||||
const timeOutError = message.type === 'timeout' ? new WorkflowOperationError('Workflow execution timed out!') : new WorkflowOperationError('Workflow-Execution has been canceled!');
|
workflowRunner.childExecutions[executionId].startedAt,
|
||||||
|
);
|
||||||
|
const timeOutError =
|
||||||
|
message.type === 'timeout'
|
||||||
|
? new WorkflowOperationError('Workflow execution timed out!')
|
||||||
|
: new WorkflowOperationError('Workflow-Execution has been canceled!');
|
||||||
|
|
||||||
// If there is any data send it to parent process, if execution timedout add the error
|
// If there is any data send it to parent process, if execution timedout add the error
|
||||||
await childWorkflowExecute.workflowExecute.processSuccessExecution(workflowRunner.childExecutions[executionId].startedAt, childWorkflowExecute.workflow, timeOutError);
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await childWorkflowExecute.workflowExecute.processSuccessExecution(
|
||||||
|
workflowRunner.childExecutions[executionId].startedAt,
|
||||||
|
childWorkflowExecute.workflow,
|
||||||
|
timeOutError,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Workflow started already executing
|
// Workflow started already executing
|
||||||
runData = workflowRunner.workflowExecute.getFullRunData(workflowRunner.startedAt);
|
runData = workflowRunner.workflowExecute.getFullRunData(workflowRunner.startedAt);
|
||||||
|
|
||||||
const timeOutError = message.type === 'timeout' ? new WorkflowOperationError('Workflow execution timed out!') : new WorkflowOperationError('Workflow-Execution has been canceled!');
|
const timeOutError =
|
||||||
|
message.type === 'timeout'
|
||||||
|
? new WorkflowOperationError('Workflow execution timed out!')
|
||||||
|
: new WorkflowOperationError('Workflow-Execution has been canceled!');
|
||||||
|
|
||||||
// If there is any data send it to parent process, if execution timedout add the error
|
// If there is any data send it to parent process, if execution timedout add the error
|
||||||
await workflowRunner.workflowExecute.processSuccessExecution(workflowRunner.startedAt, workflowRunner.workflow!, timeOutError);
|
await workflowRunner.workflowExecute.processSuccessExecution(
|
||||||
|
workflowRunner.startedAt,
|
||||||
|
workflowRunner.workflow!,
|
||||||
|
timeOutError,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Workflow did not get started yet
|
// Workflow did not get started yet
|
||||||
runData = {
|
runData = {
|
||||||
|
@ -338,11 +442,14 @@ process.on('message', async (message: IProcessMessage) => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
finished: false,
|
finished: false,
|
||||||
mode: workflowRunner.data ? workflowRunner.data!.executionMode : 'own' as WorkflowExecuteMode,
|
mode: workflowRunner.data
|
||||||
|
? workflowRunner.data.executionMode
|
||||||
|
: ('own' as WorkflowExecuteMode),
|
||||||
startedAt: workflowRunner.startedAt,
|
startedAt: workflowRunner.startedAt,
|
||||||
stoppedAt: new Date(),
|
stoppedAt: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
workflowRunner.sendHookToParentProcess('workflowExecuteAfter', [runData]);
|
workflowRunner.sendHookToParentProcess('workflowExecuteAfter', [runData]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -353,16 +460,16 @@ process.on('message', async (message: IProcessMessage) => {
|
||||||
// Stop process
|
// Stop process
|
||||||
process.exit();
|
process.exit();
|
||||||
} else if (message.type === 'executionId') {
|
} else if (message.type === 'executionId') {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
workflowRunner.executionIdCallback(message.data.executionId);
|
workflowRunner.executionIdCallback(message.data.executionId);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
||||||
// Catch all uncaught errors and forward them to parent process
|
// Catch all uncaught errors and forward them to parent process
|
||||||
const executionError = {
|
const executionError = {
|
||||||
...error,
|
...error,
|
||||||
name: error!.name || 'Error',
|
name: error.name || 'Error',
|
||||||
message: error!.message,
|
message: error.message,
|
||||||
stack: error!.stack,
|
stack: error.stack,
|
||||||
} as ExecutionError;
|
} as ExecutionError;
|
||||||
|
|
||||||
await sendToParentProcess('processError', {
|
await sendToParentProcess('processError', {
|
||||||
|
|
|
@ -1,15 +1,6 @@
|
||||||
import {
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
ICredentialNodeAccess,
|
/* eslint-disable import/no-cycle */
|
||||||
} from 'n8n-workflow';
|
import { ICredentialNodeAccess } from 'n8n-workflow';
|
||||||
|
|
||||||
import {
|
|
||||||
getTimestampSyntax,
|
|
||||||
resolveDataType
|
|
||||||
} from '../utils';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ICredentialsDb,
|
|
||||||
} from '../..';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BeforeUpdate,
|
BeforeUpdate,
|
||||||
|
@ -20,10 +11,12 @@ import {
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
import { getTimestampSyntax, resolveDataType } from '../utils';
|
||||||
|
|
||||||
|
import { ICredentialsDb } from '../..';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class CredentialsEntity implements ICredentialsDb {
|
export class CredentialsEntity implements ICredentialsDb {
|
||||||
|
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id: number;
|
id: number;
|
||||||
|
|
||||||
|
@ -47,7 +40,11 @@ export class CredentialsEntity implements ICredentialsDb {
|
||||||
@CreateDateColumn({ precision: 3, default: () => getTimestampSyntax() })
|
@CreateDateColumn({ precision: 3, default: () => getTimestampSyntax() })
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
||||||
@UpdateDateColumn({ precision: 3, default: () => getTimestampSyntax(), onUpdate: getTimestampSyntax() })
|
@UpdateDateColumn({
|
||||||
|
precision: 3,
|
||||||
|
default: () => getTimestampSyntax(),
|
||||||
|
onUpdate: getTimestampSyntax(),
|
||||||
|
})
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|
||||||
@BeforeUpdate()
|
@BeforeUpdate()
|
||||||
|
|
|
@ -1,27 +1,13 @@
|
||||||
import {
|
/* eslint-disable import/no-cycle */
|
||||||
WorkflowExecuteMode,
|
import { WorkflowExecuteMode } from 'n8n-workflow';
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
import {
|
import { Column, ColumnOptions, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
IExecutionFlattedDb,
|
import { IExecutionFlattedDb, IWorkflowDb } from '../..';
|
||||||
IWorkflowDb,
|
|
||||||
} from '../../';
|
|
||||||
|
|
||||||
import {
|
import { resolveDataType } from '../utils';
|
||||||
resolveDataType
|
|
||||||
} from '../utils';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Column,
|
|
||||||
ColumnOptions,
|
|
||||||
Entity,
|
|
||||||
Index,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
} from 'typeorm';
|
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class ExecutionEntity implements IExecutionFlattedDb {
|
export class ExecutionEntity implements IExecutionFlattedDb {
|
||||||
|
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id: number;
|
id: number;
|
||||||
|
|
||||||
|
@ -53,4 +39,8 @@ export class ExecutionEntity implements IExecutionFlattedDb {
|
||||||
@Index()
|
@Index()
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
workflowId: string;
|
workflowId: string;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column({ type: resolveDataType('datetime') as ColumnOptions['type'], nullable: true })
|
||||||
|
waitTill: Date;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,15 @@
|
||||||
import { BeforeUpdate, Column, CreateDateColumn, Entity, Index, ManyToMany, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
/* eslint-disable import/no-cycle */
|
||||||
|
import {
|
||||||
|
BeforeUpdate,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
Index,
|
||||||
|
ManyToMany,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
import { IsDate, IsOptional, IsString, Length } from 'class-validator';
|
import { IsDate, IsOptional, IsString, Length } from 'class-validator';
|
||||||
|
|
||||||
import { ITagDb } from '../../Interfaces';
|
import { ITagDb } from '../../Interfaces';
|
||||||
|
@ -7,7 +18,6 @@ import { getTimestampSyntax } from '../utils';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class TagEntity implements ITagDb {
|
export class TagEntity implements ITagDb {
|
||||||
|
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id: number;
|
id: number;
|
||||||
|
|
||||||
|
@ -22,12 +32,16 @@ export class TagEntity implements ITagDb {
|
||||||
@IsDate()
|
@IsDate()
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
||||||
@UpdateDateColumn({ precision: 3, default: () => getTimestampSyntax(), onUpdate: getTimestampSyntax() })
|
@UpdateDateColumn({
|
||||||
|
precision: 3,
|
||||||
|
default: () => getTimestampSyntax(),
|
||||||
|
onUpdate: getTimestampSyntax(),
|
||||||
|
})
|
||||||
@IsOptional() // ignored by validation because set at DB level
|
@IsOptional() // ignored by validation because set at DB level
|
||||||
@IsDate()
|
@IsDate()
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|
||||||
@ManyToMany(() => WorkflowEntity, workflow => workflow.tags)
|
@ManyToMany(() => WorkflowEntity, (workflow) => workflow.tags)
|
||||||
workflows: WorkflowEntity[];
|
workflows: WorkflowEntity[];
|
||||||
|
|
||||||
@BeforeUpdate()
|
@BeforeUpdate()
|
||||||
|
|
|
@ -1,18 +1,11 @@
|
||||||
import {
|
import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
|
||||||
Column,
|
|
||||||
Entity,
|
|
||||||
Index,
|
|
||||||
PrimaryColumn,
|
|
||||||
} from 'typeorm';
|
|
||||||
|
|
||||||
import {
|
// eslint-disable-next-line import/no-cycle
|
||||||
IWebhookDb,
|
import { IWebhookDb } from '../../Interfaces';
|
||||||
} from '../../Interfaces';
|
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
@Index(['webhookId', 'method', 'pathLength'])
|
@Index(['webhookId', 'method', 'pathLength'])
|
||||||
export class WebhookEntity implements IWebhookDb {
|
export class WebhookEntity implements IWebhookDb {
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
workflowId: number;
|
workflowId: number;
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,8 @@
|
||||||
import {
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
Length,
|
/* eslint-disable import/no-cycle */
|
||||||
} from 'class-validator';
|
import { Length } from 'class-validator';
|
||||||
|
|
||||||
import {
|
import { IConnections, IDataObject, INode, IWorkflowSettings } from 'n8n-workflow';
|
||||||
IConnections,
|
|
||||||
IDataObject,
|
|
||||||
INode,
|
|
||||||
IWorkflowSettings,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BeforeUpdate,
|
BeforeUpdate,
|
||||||
|
@ -22,22 +17,14 @@ import {
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
import {
|
import { IWorkflowDb } from '../..';
|
||||||
IWorkflowDb,
|
|
||||||
} from '../../';
|
|
||||||
|
|
||||||
import {
|
import { getTimestampSyntax, resolveDataType } from '../utils';
|
||||||
getTimestampSyntax,
|
|
||||||
resolveDataType
|
|
||||||
} from '../utils';
|
|
||||||
|
|
||||||
import {
|
import { TagEntity } from './TagEntity';
|
||||||
TagEntity,
|
|
||||||
} from './TagEntity';
|
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class WorkflowEntity implements IWorkflowDb {
|
export class WorkflowEntity implements IWorkflowDb {
|
||||||
|
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id: number;
|
id: number;
|
||||||
|
|
||||||
|
@ -58,7 +45,11 @@ export class WorkflowEntity implements IWorkflowDb {
|
||||||
@CreateDateColumn({ precision: 3, default: () => getTimestampSyntax() })
|
@CreateDateColumn({ precision: 3, default: () => getTimestampSyntax() })
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
||||||
@UpdateDateColumn({ precision: 3, default: () => getTimestampSyntax(), onUpdate: getTimestampSyntax() })
|
@UpdateDateColumn({
|
||||||
|
precision: 3,
|
||||||
|
default: () => getTimestampSyntax(),
|
||||||
|
onUpdate: getTimestampSyntax(),
|
||||||
|
})
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
|
@ -73,16 +64,16 @@ export class WorkflowEntity implements IWorkflowDb {
|
||||||
})
|
})
|
||||||
staticData?: IDataObject;
|
staticData?: IDataObject;
|
||||||
|
|
||||||
@ManyToMany(() => TagEntity, tag => tag.workflows)
|
@ManyToMany(() => TagEntity, (tag) => tag.workflows)
|
||||||
@JoinTable({
|
@JoinTable({
|
||||||
name: "workflows_tags", // table name for the junction table of this relation
|
name: 'workflows_tags', // table name for the junction table of this relation
|
||||||
joinColumn: {
|
joinColumn: {
|
||||||
name: "workflowId",
|
name: 'workflowId',
|
||||||
referencedColumnName: "id",
|
referencedColumnName: 'id',
|
||||||
},
|
},
|
||||||
inverseJoinColumn: {
|
inverseJoinColumn: {
|
||||||
name: "tagId",
|
name: 'tagId',
|
||||||
referencedColumnName: "id",
|
referencedColumnName: 'id',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
tags: TagEntity[];
|
tags: TagEntity[];
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
/* eslint-disable import/no-cycle */
|
||||||
import { CredentialsEntity } from './CredentialsEntity';
|
import { CredentialsEntity } from './CredentialsEntity';
|
||||||
import { ExecutionEntity } from './ExecutionEntity';
|
import { ExecutionEntity } from './ExecutionEntity';
|
||||||
import { WorkflowEntity } from './WorkflowEntity';
|
import { WorkflowEntity } from './WorkflowEntity';
|
||||||
|
|
|
@ -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 { CreateTagEntity1617268711084 } from './1617268711084-CreateTagEntity';
|
||||||
import { UniqueWorkflowNames1620826335440 } from './1620826335440-UniqueWorkflowNames';
|
import { UniqueWorkflowNames1620826335440 } from './1620826335440-UniqueWorkflowNames';
|
||||||
import { CertifyCorrectCollation1623936588000 } from './1623936588000-CertifyCorrectCollation';
|
import { CertifyCorrectCollation1623936588000 } from './1623936588000-CertifyCorrectCollation';
|
||||||
|
import { AddWaitColumnId1626183952959 } from './1626183952959-AddWaitColumn';
|
||||||
|
|
||||||
export const mysqlMigrations = [
|
export const mysqlMigrations = [
|
||||||
InitialMigration1588157391238,
|
InitialMigration1588157391238,
|
||||||
|
@ -20,4 +21,5 @@ export const mysqlMigrations = [
|
||||||
CreateTagEntity1617268711084,
|
CreateTagEntity1617268711084,
|
||||||
UniqueWorkflowNames1620826335440,
|
UniqueWorkflowNames1620826335440,
|
||||||
CertifyCorrectCollation1623936588000,
|
CertifyCorrectCollation1623936588000,
|
||||||
|
AddWaitColumnId1626183952959,
|
||||||
];
|
];
|
||||||
|
|
|
@ -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 { MakeStoppedAtNullable1607431743768 } from './1607431743768-MakeStoppedAtNullable';
|
||||||
import { CreateTagEntity1617270242566 } from './1617270242566-CreateTagEntity';
|
import { CreateTagEntity1617270242566 } from './1617270242566-CreateTagEntity';
|
||||||
import { UniqueWorkflowNames1620824779533 } from './1620824779533-UniqueWorkflowNames';
|
import { UniqueWorkflowNames1620824779533 } from './1620824779533-UniqueWorkflowNames';
|
||||||
|
import { AddwaitTill1626176912946 } from './1626176912946-AddwaitTill';
|
||||||
|
|
||||||
export const postgresMigrations = [
|
export const postgresMigrations = [
|
||||||
InitialMigration1587669153312,
|
InitialMigration1587669153312,
|
||||||
|
@ -14,4 +15,5 @@ export const postgresMigrations = [
|
||||||
MakeStoppedAtNullable1607431743768,
|
MakeStoppedAtNullable1607431743768,
|
||||||
CreateTagEntity1617270242566,
|
CreateTagEntity1617270242566,
|
||||||
UniqueWorkflowNames1620824779533,
|
UniqueWorkflowNames1620824779533,
|
||||||
|
AddwaitTill1626176912946,
|
||||||
];
|
];
|
||||||
|
|
|
@ -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 { MakeStoppedAtNullable1607431743769 } from './1607431743769-MakeStoppedAtNullable';
|
||||||
import { CreateTagEntity1617213344594 } from './1617213344594-CreateTagEntity';
|
import { CreateTagEntity1617213344594 } from './1617213344594-CreateTagEntity';
|
||||||
import { UniqueWorkflowNames1620821879465 } from './1620821879465-UniqueWorkflowNames';
|
import { UniqueWorkflowNames1620821879465 } from './1620821879465-UniqueWorkflowNames';
|
||||||
|
import { AddWaitColumn1621707690587 } from './1621707690587-AddWaitColumn';
|
||||||
|
|
||||||
export const sqliteMigrations = [
|
export const sqliteMigrations = [
|
||||||
InitialMigration1588102412422,
|
InitialMigration1588102412422,
|
||||||
|
@ -14,4 +15,5 @@ export const sqliteMigrations = [
|
||||||
MakeStoppedAtNullable1607431743769,
|
MakeStoppedAtNullable1607431743769,
|
||||||
CreateTagEntity1617213344594,
|
CreateTagEntity1617213344594,
|
||||||
UniqueWorkflowNames1620821879465,
|
UniqueWorkflowNames1620821879465,
|
||||||
|
AddWaitColumn1621707690587,
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import {
|
/* eslint-disable import/no-cycle */
|
||||||
DatabaseType,
|
import { DatabaseType } from '../index';
|
||||||
} from '../index';
|
import { getConfigValueSync } from '../GenericHelpers';
|
||||||
import { getConfigValueSync } from '../../src/GenericHelpers';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the data type for the used database type
|
* Resolves the data type for the used database type
|
||||||
|
@ -10,6 +9,7 @@ import { getConfigValueSync } from '../../src/GenericHelpers';
|
||||||
* @param {string} dataType
|
* @param {string} dataType
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
export function resolveDataType(dataType: string) {
|
export function resolveDataType(dataType: string) {
|
||||||
const dbType = getConfigValueSync('database.type') as DatabaseType;
|
const dbType = getConfigValueSync('database.type') as DatabaseType;
|
||||||
|
|
||||||
|
@ -27,16 +27,16 @@ export function resolveDataType(dataType: string) {
|
||||||
return typeMap[dbType][dataType] ?? dataType;
|
return typeMap[dbType][dataType] ?? dataType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
export function getTimestampSyntax() {
|
export function getTimestampSyntax() {
|
||||||
const dbType = getConfigValueSync('database.type') as DatabaseType;
|
const dbType = getConfigValueSync('database.type') as DatabaseType;
|
||||||
|
|
||||||
const map: { [key in DatabaseType]: string } = {
|
const map: { [key in DatabaseType]: string } = {
|
||||||
sqlite: "STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')",
|
sqlite: "STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')",
|
||||||
postgresdb: "CURRENT_TIMESTAMP(3)",
|
postgresdb: 'CURRENT_TIMESTAMP(3)',
|
||||||
mysqldb: "CURRENT_TIMESTAMP(3)",
|
mysqldb: 'CURRENT_TIMESTAMP(3)',
|
||||||
mariadb: "CURRENT_TIMESTAMP(3)",
|
mariadb: 'CURRENT_TIMESTAMP(3)',
|
||||||
};
|
};
|
||||||
|
|
||||||
return map[dbType];
|
return map[dbType];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
/* eslint-disable import/first */
|
||||||
|
/* eslint-disable import/no-cycle */
|
||||||
export * from './CredentialsHelper';
|
export * from './CredentialsHelper';
|
||||||
export * from './CredentialTypes';
|
export * from './CredentialTypes';
|
||||||
export * from './CredentialsOverwrites';
|
export * from './CredentialsOverwrites';
|
||||||
|
@ -5,6 +7,8 @@ export * from './ExternalHooks';
|
||||||
export * from './Interfaces';
|
export * from './Interfaces';
|
||||||
export * from './LoadNodesAndCredentials';
|
export * from './LoadNodesAndCredentials';
|
||||||
export * from './NodeTypes';
|
export * from './NodeTypes';
|
||||||
|
export * from './WaitTracker';
|
||||||
|
export * from './WaitingWebhooks';
|
||||||
export * from './WorkflowCredentials';
|
export * from './WorkflowCredentials';
|
||||||
export * from './WorkflowRunner';
|
export * from './WorkflowRunner';
|
||||||
|
|
||||||
|
@ -20,6 +24,7 @@ import * as WebhookHelpers from './WebhookHelpers';
|
||||||
import * as WebhookServer from './WebhookServer';
|
import * as WebhookServer from './WebhookServer';
|
||||||
import * as WorkflowExecuteAdditionalData from './WorkflowExecuteAdditionalData';
|
import * as WorkflowExecuteAdditionalData from './WorkflowExecuteAdditionalData';
|
||||||
import * as WorkflowHelpers from './WorkflowHelpers';
|
import * as WorkflowHelpers from './WorkflowHelpers';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ActiveExecutions,
|
ActiveExecutions,
|
||||||
ActiveWorkflowRunner,
|
ActiveWorkflowRunner,
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
describe('Placeholder', () => {
|
describe('Placeholder', () => {
|
||||||
|
|
||||||
test('example', () => {
|
test('example', () => {
|
||||||
expect(1 + 1).toEqual(2);
|
expect(1 + 1).toEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-core",
|
"name": "n8n-core",
|
||||||
"version": "0.78.0",
|
"version": "0.81.0",
|
||||||
"description": "Core functionality of n8n",
|
"description": "Core functionality of n8n",
|
||||||
"license": "SEE LICENSE IN LICENSE.md",
|
"license": "SEE LICENSE IN LICENSE.md",
|
||||||
"homepage": "https://n8n.io",
|
"homepage": "https://n8n.io",
|
||||||
|
@ -17,8 +17,9 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"dev": "npm run watch",
|
"dev": "npm run watch",
|
||||||
"tslint": "tslint -p tsconfig.json -c tslint.json",
|
"format": "cd ../.. && node_modules/prettier/bin-prettier.js packages/core/**/**.ts --write",
|
||||||
"tslintfix": "tslint --fix -p tsconfig.json -c tslint.json",
|
"lint": "cd ../.. && node_modules/eslint/bin/eslint.js packages/core",
|
||||||
|
"lintfix": "cd ../.. && node_modules/eslint/bin/eslint.js packages/core --fix",
|
||||||
"watch": "tsc --watch",
|
"watch": "tsc --watch",
|
||||||
"test": "jest"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
|
@ -38,16 +39,16 @@
|
||||||
"source-map-support": "^0.5.9",
|
"source-map-support": "^0.5.9",
|
||||||
"ts-jest": "^26.3.0",
|
"ts-jest": "^26.3.0",
|
||||||
"tslint": "^6.1.2",
|
"tslint": "^6.1.2",
|
||||||
"typescript": "~3.9.7"
|
"typescript": "~4.3.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"client-oauth2": "^4.2.5",
|
"client-oauth2": "^4.2.5",
|
||||||
"cron": "^1.7.2",
|
"cron": "^1.7.2",
|
||||||
"crypto-js": "4.0.0",
|
"crypto-js": "~4.1.1",
|
||||||
"file-type": "^14.6.2",
|
"file-type": "^14.6.2",
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
"mime-types": "^2.1.27",
|
"mime-types": "^2.1.27",
|
||||||
"n8n-workflow": "~0.64.0",
|
"n8n-workflow": "~0.66.0",
|
||||||
"oauth-1.0a": "^2.2.6",
|
"oauth-1.0a": "^2.2.6",
|
||||||
"p-cancelable": "^2.0.0",
|
"p-cancelable": "^2.0.0",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
|
|
|
@ -6,10 +6,8 @@ import {
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import {
|
// eslint-disable-next-line import/no-cycle
|
||||||
NodeExecuteFunctions,
|
import { NodeExecuteFunctions } from '.';
|
||||||
} from './';
|
|
||||||
|
|
||||||
|
|
||||||
export class ActiveWebhooks {
|
export class ActiveWebhooks {
|
||||||
private workflowWebhooks: {
|
private workflowWebhooks: {
|
||||||
|
@ -22,7 +20,6 @@ export class ActiveWebhooks {
|
||||||
|
|
||||||
testWebhooks = false;
|
testWebhooks = false;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a new webhook
|
* Adds a new webhook
|
||||||
*
|
*
|
||||||
|
@ -31,19 +28,31 @@ export class ActiveWebhooks {
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
* @memberof ActiveWebhooks
|
* @memberof ActiveWebhooks
|
||||||
*/
|
*/
|
||||||
async add(workflow: Workflow, webhookData: IWebhookData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): Promise<void> {
|
async add(
|
||||||
|
workflow: Workflow,
|
||||||
|
webhookData: IWebhookData,
|
||||||
|
mode: WorkflowExecuteMode,
|
||||||
|
activation: WorkflowActivateMode,
|
||||||
|
): Promise<void> {
|
||||||
if (workflow.id === undefined) {
|
if (workflow.id === undefined) {
|
||||||
throw new Error('Webhooks can only be added for saved workflows as an id is needed!');
|
throw new Error('Webhooks can only be added for saved workflows as an id is needed!');
|
||||||
}
|
}
|
||||||
if (webhookData.path.endsWith('/')) {
|
if (webhookData.path.endsWith('/')) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
webhookData.path = webhookData.path.slice(0, -1);
|
webhookData.path = webhookData.path.slice(0, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const webhookKey = this.getWebhookKey(webhookData.httpMethod, webhookData.path, webhookData.webhookId);
|
const webhookKey = this.getWebhookKey(
|
||||||
|
webhookData.httpMethod,
|
||||||
|
webhookData.path,
|
||||||
|
webhookData.webhookId,
|
||||||
|
);
|
||||||
|
|
||||||
//check that there is not a webhook already registed with that path/method
|
// check that there is not a webhook already registed with that path/method
|
||||||
if (this.webhookUrls[webhookKey] && !webhookData.webhookId) {
|
if (this.webhookUrls[webhookKey] && !webhookData.webhookId) {
|
||||||
throw new Error(`Test-Webhook can not be activated because another one with the same method "${webhookData.httpMethod}" and path "${webhookData.path}" is already active!`);
|
throw new Error(
|
||||||
|
`Test-Webhook can not be activated because another one with the same method "${webhookData.httpMethod}" and path "${webhookData.path}" is already active!`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.workflowWebhooks[webhookData.workflowId] === undefined) {
|
if (this.workflowWebhooks[webhookData.workflowId] === undefined) {
|
||||||
|
@ -58,18 +67,33 @@ export class ActiveWebhooks {
|
||||||
this.webhookUrls[webhookKey].push(webhookData);
|
this.webhookUrls[webhookKey].push(webhookData);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const webhookExists = await workflow.runWebhookMethod('checkExists', webhookData, NodeExecuteFunctions, mode, activation, this.testWebhooks);
|
const webhookExists = await workflow.runWebhookMethod(
|
||||||
|
'checkExists',
|
||||||
|
webhookData,
|
||||||
|
NodeExecuteFunctions,
|
||||||
|
mode,
|
||||||
|
activation,
|
||||||
|
this.testWebhooks,
|
||||||
|
);
|
||||||
if (webhookExists !== true) {
|
if (webhookExists !== true) {
|
||||||
// If webhook does not exist yet create it
|
// If webhook does not exist yet create it
|
||||||
await workflow.runWebhookMethod('create', webhookData, NodeExecuteFunctions, mode, activation, this.testWebhooks);
|
await workflow.runWebhookMethod(
|
||||||
|
'create',
|
||||||
|
webhookData,
|
||||||
|
NodeExecuteFunctions,
|
||||||
|
mode,
|
||||||
|
activation,
|
||||||
|
this.testWebhooks,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If there was a problem unregister the webhook again
|
// If there was a problem unregister the webhook again
|
||||||
if (this.webhookUrls[webhookKey].length <= 1) {
|
if (this.webhookUrls[webhookKey].length <= 1) {
|
||||||
delete this.webhookUrls[webhookKey];
|
delete this.webhookUrls[webhookKey];
|
||||||
} else {
|
} else {
|
||||||
this.webhookUrls[webhookKey] = this.webhookUrls[webhookKey].filter(webhook => webhook.path !== webhookData.path);
|
this.webhookUrls[webhookKey] = this.webhookUrls[webhookKey].filter(
|
||||||
|
(webhook) => webhook.path !== webhookData.path,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
|
@ -77,7 +101,6 @@ export class ActiveWebhooks {
|
||||||
this.workflowWebhooks[webhookData.workflowId].push(webhookData);
|
this.workflowWebhooks[webhookData.workflowId].push(webhookData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns webhookData if a webhook with matches is currently registered
|
* Returns webhookData if a webhook with matches is currently registered
|
||||||
*
|
*
|
||||||
|
@ -98,9 +121,9 @@ export class ActiveWebhooks {
|
||||||
const pathElementsSet = new Set(path.split('/'));
|
const pathElementsSet = new Set(path.split('/'));
|
||||||
// check if static elements match in path
|
// check if static elements match in path
|
||||||
// if more results have been returned choose the one with the most static-route matches
|
// if more results have been returned choose the one with the most static-route matches
|
||||||
this.webhookUrls[webhookKey].forEach(dynamicWebhook => {
|
this.webhookUrls[webhookKey].forEach((dynamicWebhook) => {
|
||||||
const staticElements = dynamicWebhook.path.split('/').filter(ele => !ele.startsWith(':'));
|
const staticElements = dynamicWebhook.path.split('/').filter((ele) => !ele.startsWith(':'));
|
||||||
const allStaticExist = staticElements.every(staticEle => pathElementsSet.has(staticEle));
|
const allStaticExist = staticElements.every((staticEle) => pathElementsSet.has(staticEle));
|
||||||
|
|
||||||
if (allStaticExist && staticElements.length > maxMatches) {
|
if (allStaticExist && staticElements.length > maxMatches) {
|
||||||
maxMatches = staticElements.length;
|
maxMatches = staticElements.length;
|
||||||
|
@ -120,13 +143,14 @@ export class ActiveWebhooks {
|
||||||
* @param path
|
* @param path
|
||||||
*/
|
*/
|
||||||
getWebhookMethods(path: string): string[] {
|
getWebhookMethods(path: string): string[] {
|
||||||
const methods : string[] = [];
|
const methods: string[] = [];
|
||||||
|
|
||||||
Object.keys(this.webhookUrls)
|
Object.keys(this.webhookUrls)
|
||||||
.filter(key => key.includes(path))
|
.filter((key) => key.includes(path))
|
||||||
.map(key => {
|
// eslint-disable-next-line array-callback-return
|
||||||
methods.push(key.split('|')[0]);
|
.map((key) => {
|
||||||
});
|
methods.push(key.split('|')[0]);
|
||||||
|
});
|
||||||
|
|
||||||
return methods;
|
return methods;
|
||||||
}
|
}
|
||||||
|
@ -141,7 +165,6 @@ export class ActiveWebhooks {
|
||||||
return Object.keys(this.workflowWebhooks);
|
return Object.keys(this.workflowWebhooks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns key to uniquely identify a webhook
|
* Returns key to uniquely identify a webhook
|
||||||
*
|
*
|
||||||
|
@ -155,6 +178,7 @@ export class ActiveWebhooks {
|
||||||
if (webhookId) {
|
if (webhookId) {
|
||||||
if (path.startsWith(webhookId)) {
|
if (path.startsWith(webhookId)) {
|
||||||
const cutFromIndex = path.indexOf('/') + 1;
|
const cutFromIndex = path.indexOf('/') + 1;
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
path = path.slice(cutFromIndex);
|
path = path.slice(cutFromIndex);
|
||||||
}
|
}
|
||||||
return `${httpMethod}|${webhookId}|${path.split('/').length}`;
|
return `${httpMethod}|${webhookId}|${path.split('/').length}`;
|
||||||
|
@ -162,7 +186,6 @@ export class ActiveWebhooks {
|
||||||
return `${httpMethod}|${path}`;
|
return `${httpMethod}|${path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes all webhooks of a workflow
|
* Removes all webhooks of a workflow
|
||||||
*
|
*
|
||||||
|
@ -171,6 +194,7 @@ export class ActiveWebhooks {
|
||||||
* @memberof ActiveWebhooks
|
* @memberof ActiveWebhooks
|
||||||
*/
|
*/
|
||||||
async removeWorkflow(workflow: Workflow): Promise<boolean> {
|
async removeWorkflow(workflow: Workflow): Promise<boolean> {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const workflowId = workflow.id!.toString();
|
const workflowId = workflow.id!.toString();
|
||||||
|
|
||||||
if (this.workflowWebhooks[workflowId] === undefined) {
|
if (this.workflowWebhooks[workflowId] === undefined) {
|
||||||
|
@ -183,10 +207,21 @@ export class ActiveWebhooks {
|
||||||
const mode = 'internal';
|
const mode = 'internal';
|
||||||
|
|
||||||
// Go through all the registered webhooks of the workflow and remove them
|
// Go through all the registered webhooks of the workflow and remove them
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const webhookData of webhooks) {
|
for (const webhookData of webhooks) {
|
||||||
await workflow.runWebhookMethod('delete', webhookData, NodeExecuteFunctions, mode, 'update', this.testWebhooks);
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await workflow.runWebhookMethod(
|
||||||
|
'delete',
|
||||||
|
webhookData,
|
||||||
|
NodeExecuteFunctions,
|
||||||
|
mode,
|
||||||
|
'update',
|
||||||
|
this.testWebhooks,
|
||||||
|
);
|
||||||
|
|
||||||
delete this.webhookUrls[this.getWebhookKey(webhookData.httpMethod, webhookData.path, webhookData.webhookId)];
|
delete this.webhookUrls[
|
||||||
|
this.getWebhookKey(webhookData.httpMethod, webhookData.path, webhookData.webhookId)
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove also the workflow-webhook entry
|
// Remove also the workflow-webhook entry
|
||||||
|
@ -195,18 +230,16 @@ export class ActiveWebhooks {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes all the webhooks of the given workflows
|
* Removes all the webhooks of the given workflows
|
||||||
*/
|
*/
|
||||||
async removeAll(workflows: Workflow[]): Promise<void> {
|
async removeAll(workflows: Workflow[]): Promise<void> {
|
||||||
const removePromises = [];
|
const removePromises = [];
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const workflow of workflows) {
|
for (const workflow of workflows) {
|
||||||
removePromises.push(this.removeWorkflow(workflow));
|
removePromises.push(this.removeWorkflow(workflow));
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(removePromises);
|
await Promise.all(removePromises);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
/* eslint-disable no-continue */
|
||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
|
/* eslint-disable no-restricted-syntax */
|
||||||
import { CronJob } from 'cron';
|
import { CronJob } from 'cron';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -13,18 +16,14 @@ import {
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import {
|
// eslint-disable-next-line import/no-cycle
|
||||||
ITriggerTime,
|
import { ITriggerTime, IWorkflowData } from '.';
|
||||||
IWorkflowData,
|
|
||||||
} from './';
|
|
||||||
|
|
||||||
|
|
||||||
export class ActiveWorkflows {
|
export class ActiveWorkflows {
|
||||||
private workflowData: {
|
private workflowData: {
|
||||||
[key: string]: IWorkflowData;
|
[key: string]: IWorkflowData;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns if the workflow is active
|
* Returns if the workflow is active
|
||||||
*
|
*
|
||||||
|
@ -33,10 +32,10 @@ export class ActiveWorkflows {
|
||||||
* @memberof ActiveWorkflows
|
* @memberof ActiveWorkflows
|
||||||
*/
|
*/
|
||||||
isActive(id: string): boolean {
|
isActive(id: string): boolean {
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
return this.workflowData.hasOwnProperty(id);
|
return this.workflowData.hasOwnProperty(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the ids of the currently active workflows
|
* Returns the ids of the currently active workflows
|
||||||
*
|
*
|
||||||
|
@ -47,7 +46,6 @@ export class ActiveWorkflows {
|
||||||
return Object.keys(this.workflowData);
|
return Object.keys(this.workflowData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the Workflow data for the workflow with
|
* Returns the Workflow data for the workflow with
|
||||||
* the given id if it is currently active
|
* the given id if it is currently active
|
||||||
|
@ -60,7 +58,6 @@ export class ActiveWorkflows {
|
||||||
return this.workflowData[id];
|
return this.workflowData[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes a workflow active
|
* Makes a workflow active
|
||||||
*
|
*
|
||||||
|
@ -70,16 +67,31 @@ export class ActiveWorkflows {
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
* @memberof ActiveWorkflows
|
* @memberof ActiveWorkflows
|
||||||
*/
|
*/
|
||||||
async add(id: string, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode, getTriggerFunctions: IGetExecuteTriggerFunctions, getPollFunctions: IGetExecutePollFunctions): Promise<void> {
|
async add(
|
||||||
|
id: string,
|
||||||
|
workflow: Workflow,
|
||||||
|
additionalData: IWorkflowExecuteAdditionalData,
|
||||||
|
mode: WorkflowExecuteMode,
|
||||||
|
activation: WorkflowActivateMode,
|
||||||
|
getTriggerFunctions: IGetExecuteTriggerFunctions,
|
||||||
|
getPollFunctions: IGetExecutePollFunctions,
|
||||||
|
): Promise<void> {
|
||||||
this.workflowData[id] = {};
|
this.workflowData[id] = {};
|
||||||
const triggerNodes = workflow.getTriggerNodes();
|
const triggerNodes = workflow.getTriggerNodes();
|
||||||
|
|
||||||
let triggerResponse: ITriggerResponse | undefined;
|
let triggerResponse: ITriggerResponse | undefined;
|
||||||
this.workflowData[id].triggerResponses = [];
|
this.workflowData[id].triggerResponses = [];
|
||||||
for (const triggerNode of triggerNodes) {
|
for (const triggerNode of triggerNodes) {
|
||||||
triggerResponse = await workflow.runTrigger(triggerNode, getTriggerFunctions, additionalData, mode, activation);
|
triggerResponse = await workflow.runTrigger(
|
||||||
|
triggerNode,
|
||||||
|
getTriggerFunctions,
|
||||||
|
additionalData,
|
||||||
|
mode,
|
||||||
|
activation,
|
||||||
|
);
|
||||||
if (triggerResponse !== undefined) {
|
if (triggerResponse !== undefined) {
|
||||||
// If a response was given save it
|
// If a response was given save it
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
this.workflowData[id].triggerResponses!.push(triggerResponse);
|
this.workflowData[id].triggerResponses!.push(triggerResponse);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,12 +100,21 @@ export class ActiveWorkflows {
|
||||||
if (pollNodes.length) {
|
if (pollNodes.length) {
|
||||||
this.workflowData[id].pollResponses = [];
|
this.workflowData[id].pollResponses = [];
|
||||||
for (const pollNode of pollNodes) {
|
for (const pollNode of pollNodes) {
|
||||||
this.workflowData[id].pollResponses!.push(await this.activatePolling(pollNode, workflow, additionalData, getPollFunctions, mode, activation));
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
this.workflowData[id].pollResponses!.push(
|
||||||
|
await this.activatePolling(
|
||||||
|
pollNode,
|
||||||
|
workflow,
|
||||||
|
additionalData,
|
||||||
|
getPollFunctions,
|
||||||
|
mode,
|
||||||
|
activation,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activates polling for the given node
|
* Activates polling for the given node
|
||||||
*
|
*
|
||||||
|
@ -104,7 +125,14 @@ export class ActiveWorkflows {
|
||||||
* @returns {Promise<IPollResponse>}
|
* @returns {Promise<IPollResponse>}
|
||||||
* @memberof ActiveWorkflows
|
* @memberof ActiveWorkflows
|
||||||
*/
|
*/
|
||||||
async activatePolling(node: INode, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, getPollFunctions: IGetExecutePollFunctions, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): Promise<IPollResponse> {
|
async activatePolling(
|
||||||
|
node: INode,
|
||||||
|
workflow: Workflow,
|
||||||
|
additionalData: IWorkflowExecuteAdditionalData,
|
||||||
|
getPollFunctions: IGetExecutePollFunctions,
|
||||||
|
mode: WorkflowExecuteMode,
|
||||||
|
activation: WorkflowActivateMode,
|
||||||
|
): Promise<IPollResponse> {
|
||||||
const pollFunctions = getPollFunctions(workflow, node, additionalData, mode, activation);
|
const pollFunctions = getPollFunctions(workflow, node, additionalData, mode, activation);
|
||||||
|
|
||||||
const pollTimes = pollFunctions.getNodeParameter('pollTimes') as unknown as {
|
const pollTimes = pollFunctions.getNodeParameter('pollTimes') as unknown as {
|
||||||
|
@ -113,12 +141,12 @@ export class ActiveWorkflows {
|
||||||
|
|
||||||
// Define the order the cron-time-parameter appear
|
// Define the order the cron-time-parameter appear
|
||||||
const parameterOrder = [
|
const parameterOrder = [
|
||||||
'second', // 0 - 59
|
'second', // 0 - 59
|
||||||
'minute', // 0 - 59
|
'minute', // 0 - 59
|
||||||
'hour', // 0 - 23
|
'hour', // 0 - 23
|
||||||
'dayOfMonth', // 1 - 31
|
'dayOfMonth', // 1 - 31
|
||||||
'month', // 0 - 11(Jan - Dec)
|
'month', // 0 - 11(Jan - Dec)
|
||||||
'weekday', // 0 - 6(Sun - Sat)
|
'weekday', // 0 - 6(Sun - Sat)
|
||||||
];
|
];
|
||||||
|
|
||||||
// Get all the trigger times
|
// Get all the trigger times
|
||||||
|
@ -165,10 +193,15 @@ export class ActiveWorkflows {
|
||||||
|
|
||||||
// The trigger function to execute when the cron-time got reached
|
// The trigger function to execute when the cron-time got reached
|
||||||
const executeTrigger = async () => {
|
const executeTrigger = async () => {
|
||||||
Logger.info(`Polling trigger initiated for workflow "${workflow.name}"`, {workflowName: workflow.name, workflowId: workflow.id});
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
|
Logger.info(`Polling trigger initiated for workflow "${workflow.name}"`, {
|
||||||
|
workflowName: workflow.name,
|
||||||
|
workflowId: workflow.id,
|
||||||
|
});
|
||||||
const pollResponse = await workflow.runPoll(node, pollFunctions);
|
const pollResponse = await workflow.runPoll(node, pollFunctions);
|
||||||
|
|
||||||
if (pollResponse !== null) {
|
if (pollResponse !== null) {
|
||||||
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
pollFunctions.__emit(pollResponse);
|
pollFunctions.__emit(pollResponse);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -180,6 +213,7 @@ export class ActiveWorkflows {
|
||||||
|
|
||||||
// Start the cron-jobs
|
// Start the cron-jobs
|
||||||
const cronJobs: CronJob[] = [];
|
const cronJobs: CronJob[] = [];
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
for (const cronTime of cronTimes) {
|
for (const cronTime of cronTimes) {
|
||||||
const cronTimeParts = cronTime.split(' ');
|
const cronTimeParts = cronTime.split(' ');
|
||||||
if (cronTimeParts.length > 0 && cronTimeParts[0].includes('*')) {
|
if (cronTimeParts.length > 0 && cronTimeParts[0].includes('*')) {
|
||||||
|
@ -201,7 +235,6 @@ export class ActiveWorkflows {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes a workflow inactive
|
* Makes a workflow inactive
|
||||||
*
|
*
|
||||||
|
@ -212,7 +245,9 @@ export class ActiveWorkflows {
|
||||||
async remove(id: string): Promise<void> {
|
async remove(id: string): Promise<void> {
|
||||||
if (!this.isActive(id)) {
|
if (!this.isActive(id)) {
|
||||||
// Workflow is currently not registered
|
// Workflow is currently not registered
|
||||||
throw new Error(`The workflow with the id "${id}" is currently not active and can so not be removed`);
|
throw new Error(
|
||||||
|
`The workflow with the id "${id}" is currently not active and can so not be removed`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const workflowData = this.workflowData[id];
|
const workflowData = this.workflowData[id];
|
||||||
|
@ -235,5 +270,4 @@ export class ActiveWorkflows {
|
||||||
|
|
||||||
delete this.workflowData[id];
|
delete this.workflowData[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,4 +5,6 @@ export const EXTENSIONS_SUBDIRECTORY = 'custom';
|
||||||
export const USER_FOLDER_ENV_OVERWRITE = 'N8N_USER_FOLDER';
|
export const USER_FOLDER_ENV_OVERWRITE = 'N8N_USER_FOLDER';
|
||||||
export const USER_SETTINGS_FILE_NAME = 'config';
|
export const USER_SETTINGS_FILE_NAME = 'config';
|
||||||
export const USER_SETTINGS_SUBFOLDER = '.n8n';
|
export const USER_SETTINGS_SUBFOLDER = '.n8n';
|
||||||
|
export const PLACEHOLDER_EMPTY_EXECUTION_ID = '__UNKOWN__';
|
||||||
export const TUNNEL_SUBDOMAIN_ENV = 'N8N_TUNNEL_SUBDOMAIN';
|
export const TUNNEL_SUBDOMAIN_ENV = 'N8N_TUNNEL_SUBDOMAIN';
|
||||||
|
export const WAIT_TIME_UNLIMITED = '3000-01-01T00:00:00.000Z';
|
||||||
|
|
|
@ -7,16 +7,13 @@ import {
|
||||||
|
|
||||||
import { AES, enc } from 'crypto-js';
|
import { AES, enc } from 'crypto-js';
|
||||||
|
|
||||||
|
|
||||||
export class Credentials extends ICredentials {
|
export class Credentials extends ICredentials {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns if the given nodeType has access to data
|
* Returns if the given nodeType has access to data
|
||||||
*/
|
*/
|
||||||
hasNodeAccess(nodeType: string): boolean {
|
hasNodeAccess(nodeType: string): boolean {
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const accessData of this.nodesAccess) {
|
for (const accessData of this.nodesAccess) {
|
||||||
|
|
||||||
if (accessData.nodeType === nodeType) {
|
if (accessData.nodeType === nodeType) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -25,7 +22,6 @@ export class Credentials extends ICredentials {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets new credential object
|
* Sets new credential object
|
||||||
*/
|
*/
|
||||||
|
@ -33,7 +29,6 @@ export class Credentials extends ICredentials {
|
||||||
this.data = AES.encrypt(JSON.stringify(data), encryptionKey).toString();
|
this.data = AES.encrypt(JSON.stringify(data), encryptionKey).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets new credentials for given key
|
* Sets new credentials for given key
|
||||||
*/
|
*/
|
||||||
|
@ -50,13 +45,14 @@ export class Credentials extends ICredentials {
|
||||||
return this.setData(fullData, encryptionKey);
|
return this.setData(fullData, encryptionKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the decrypted credential object
|
* Returns the decrypted credential object
|
||||||
*/
|
*/
|
||||||
getData(encryptionKey: string, nodeType?: string): ICredentialDataDecryptedObject {
|
getData(encryptionKey: string, nodeType?: string): ICredentialDataDecryptedObject {
|
||||||
if (nodeType && !this.hasNodeAccess(nodeType)) {
|
if (nodeType && !this.hasNodeAccess(nodeType)) {
|
||||||
throw new Error(`The node of type "${nodeType}" does not have access to credentials "${this.name}" of type "${this.type}".`);
|
throw new Error(
|
||||||
|
`The node of type "${nodeType}" does not have access to credentials "${this.name}" of type "${this.type}".`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.data === undefined) {
|
if (this.data === undefined) {
|
||||||
|
@ -66,13 +62,15 @@ export class Credentials extends ICredentials {
|
||||||
const decryptedData = AES.decrypt(this.data, encryptionKey);
|
const decryptedData = AES.decrypt(this.data, encryptionKey);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
return JSON.parse(decryptedData.toString(enc.Utf8));
|
return JSON.parse(decryptedData.toString(enc.Utf8));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error('Credentials could not be decrypted. The reason is that probably a different "encryptionKey" got used to encrypt the data than now to decrypt it.');
|
throw new Error(
|
||||||
|
'Credentials could not be decrypted. The likely reason is that a different "encryptionKey" was used to encrypt the data.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the decrypted credentials for given key
|
* Returns the decrypted credentials for given key
|
||||||
*/
|
*/
|
||||||
|
@ -80,9 +78,10 @@ export class Credentials extends ICredentials {
|
||||||
const fullData = this.getData(encryptionKey, nodeType);
|
const fullData = this.getData(encryptionKey, nodeType);
|
||||||
|
|
||||||
if (fullData === null) {
|
if (fullData === null) {
|
||||||
throw new Error(`No data got set.`);
|
throw new Error(`No data was set.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
if (!fullData.hasOwnProperty(key)) {
|
if (!fullData.hasOwnProperty(key)) {
|
||||||
throw new Error(`No data for key "${key}" exists.`);
|
throw new Error(`No data for key "${key}" exists.`);
|
||||||
}
|
}
|
||||||
|
@ -90,13 +89,12 @@ export class Credentials extends ICredentials {
|
||||||
return fullData[key];
|
return fullData[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the encrypted credentials to be saved
|
* Returns the encrypted credentials to be saved
|
||||||
*/
|
*/
|
||||||
getDataToSave(): ICredentialsEncrypted {
|
getDataToSave(): ICredentialsEncrypted {
|
||||||
if (this.data === undefined) {
|
if (this.data === undefined) {
|
||||||
throw new Error(`No credentials got set to save.`);
|
throw new Error(`No credentials were set to save.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -5,10 +5,10 @@ export interface IDeferredPromise<T> {
|
||||||
resolve: (result: T) => void;
|
resolve: (result: T) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createDeferredPromise<T>(): Promise<IDeferredPromise<T>> {
|
export async function createDeferredPromise<T>(): Promise<IDeferredPromise<T>> {
|
||||||
return new Promise<IDeferredPromise<T>>(resolveCreate => {
|
return new Promise<IDeferredPromise<T>>((resolveCreate) => {
|
||||||
const promise = new Promise<T>((resolve, reject) => {
|
const promise = new Promise<T>((resolve, reject) => {
|
||||||
resolveCreate({ promise: () => promise, resolve, reject });
|
resolveCreate({ promise: async () => promise, resolve, reject });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import {
|
import {
|
||||||
IAllExecuteFunctions,
|
IAllExecuteFunctions,
|
||||||
IBinaryData,
|
IBinaryData,
|
||||||
|
@ -16,69 +17,116 @@ import {
|
||||||
ITriggerResponse,
|
ITriggerResponse,
|
||||||
IWebhookFunctions as IWebhookFunctionsBase,
|
IWebhookFunctions as IWebhookFunctionsBase,
|
||||||
IWorkflowSettings as IWorkflowSettingsWorkflow,
|
IWorkflowSettings as IWorkflowSettingsWorkflow,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import { OptionsWithUri, OptionsWithUrl } from 'request';
|
import { OptionsWithUri, OptionsWithUrl } from 'request';
|
||||||
import * as requestPromise from 'request-promise-native';
|
import * as requestPromise from 'request-promise-native';
|
||||||
|
|
||||||
interface Constructable<T> {
|
interface Constructable<T> {
|
||||||
new(): T;
|
new (): T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IProcessMessage {
|
export interface IProcessMessage {
|
||||||
data?: any; // tslint:disable-line:no-any
|
data?: any;
|
||||||
type: string;
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface IExecuteFunctions extends IExecuteFunctionsBase {
|
export interface IExecuteFunctions extends IExecuteFunctionsBase {
|
||||||
helpers: {
|
helpers: {
|
||||||
prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise<IBinaryData>;
|
prepareBinaryData(
|
||||||
request: requestPromise.RequestPromiseAPI,
|
binaryData: Buffer,
|
||||||
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise<any>, // tslint:disable-line:no-any
|
filePath?: string,
|
||||||
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
|
mimeType?: string,
|
||||||
|
): Promise<IBinaryData>;
|
||||||
|
getBinaryDataBuffer(itemIndex: number, propertyName: string): Promise<Buffer>;
|
||||||
|
request: requestPromise.RequestPromiseAPI;
|
||||||
|
requestOAuth2(
|
||||||
|
this: IAllExecuteFunctions,
|
||||||
|
credentialsType: string,
|
||||||
|
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
|
||||||
|
oAuth2Options?: IOAuth2Options,
|
||||||
|
): Promise<any>; // tslint:disable-line:no-any
|
||||||
|
requestOAuth1(
|
||||||
|
this: IAllExecuteFunctions,
|
||||||
|
credentialsType: string,
|
||||||
|
requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions,
|
||||||
|
): Promise<any>; // tslint:disable-line:no-any
|
||||||
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
|
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface IExecuteSingleFunctions extends IExecuteSingleFunctionsBase {
|
export interface IExecuteSingleFunctions extends IExecuteSingleFunctionsBase {
|
||||||
helpers: {
|
helpers: {
|
||||||
prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise<IBinaryData>;
|
prepareBinaryData(
|
||||||
request: requestPromise.RequestPromiseAPI,
|
binaryData: Buffer,
|
||||||
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise<any>, // tslint:disable-line:no-any
|
filePath?: string,
|
||||||
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
|
mimeType?: string,
|
||||||
|
): Promise<IBinaryData>;
|
||||||
|
request: requestPromise.RequestPromiseAPI;
|
||||||
|
requestOAuth2(
|
||||||
|
this: IAllExecuteFunctions,
|
||||||
|
credentialsType: string,
|
||||||
|
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
|
||||||
|
oAuth2Options?: IOAuth2Options,
|
||||||
|
): Promise<any>; // tslint:disable-line:no-any
|
||||||
|
requestOAuth1(
|
||||||
|
this: IAllExecuteFunctions,
|
||||||
|
credentialsType: string,
|
||||||
|
requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions,
|
||||||
|
): Promise<any>; // tslint:disable-line:no-any
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface IPollFunctions extends IPollFunctionsBase {
|
export interface IPollFunctions extends IPollFunctionsBase {
|
||||||
helpers: {
|
helpers: {
|
||||||
prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise<IBinaryData>;
|
prepareBinaryData(
|
||||||
request: requestPromise.RequestPromiseAPI,
|
binaryData: Buffer,
|
||||||
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise<any>, // tslint:disable-line:no-any
|
filePath?: string,
|
||||||
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
|
mimeType?: string,
|
||||||
|
): Promise<IBinaryData>;
|
||||||
|
request: requestPromise.RequestPromiseAPI;
|
||||||
|
requestOAuth2(
|
||||||
|
this: IAllExecuteFunctions,
|
||||||
|
credentialsType: string,
|
||||||
|
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
|
||||||
|
oAuth2Options?: IOAuth2Options,
|
||||||
|
): Promise<any>; // tslint:disable-line:no-any
|
||||||
|
requestOAuth1(
|
||||||
|
this: IAllExecuteFunctions,
|
||||||
|
credentialsType: string,
|
||||||
|
requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions,
|
||||||
|
): Promise<any>; // tslint:disable-line:no-any
|
||||||
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
|
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface IResponseError extends Error {
|
export interface IResponseError extends Error {
|
||||||
statusCode?: number;
|
statusCode?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface ITriggerFunctions extends ITriggerFunctionsBase {
|
export interface ITriggerFunctions extends ITriggerFunctionsBase {
|
||||||
helpers: {
|
helpers: {
|
||||||
prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise<IBinaryData>;
|
prepareBinaryData(
|
||||||
request: requestPromise.RequestPromiseAPI,
|
binaryData: Buffer,
|
||||||
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise<any>, // tslint:disable-line:no-any
|
filePath?: string,
|
||||||
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
|
mimeType?: string,
|
||||||
|
): Promise<IBinaryData>;
|
||||||
|
request: requestPromise.RequestPromiseAPI;
|
||||||
|
requestOAuth2(
|
||||||
|
this: IAllExecuteFunctions,
|
||||||
|
credentialsType: string,
|
||||||
|
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
|
||||||
|
oAuth2Options?: IOAuth2Options,
|
||||||
|
): Promise<any>; // tslint:disable-line:no-any
|
||||||
|
requestOAuth1(
|
||||||
|
this: IAllExecuteFunctions,
|
||||||
|
credentialsType: string,
|
||||||
|
requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions,
|
||||||
|
): Promise<any>; // tslint:disable-line:no-any
|
||||||
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
|
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface ITriggerTime {
|
export interface ITriggerTime {
|
||||||
mode: string;
|
mode: string;
|
||||||
hour: number;
|
hour: number;
|
||||||
|
@ -88,7 +136,6 @@ export interface ITriggerTime {
|
||||||
[key: string]: string | number;
|
[key: string]: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface IUserSettings {
|
export interface IUserSettings {
|
||||||
encryptionKey?: string;
|
encryptionKey?: string;
|
||||||
tunnelSubdomain?: string;
|
tunnelSubdomain?: string;
|
||||||
|
@ -96,28 +143,57 @@ export interface IUserSettings {
|
||||||
|
|
||||||
export interface ILoadOptionsFunctions extends ILoadOptionsFunctionsBase {
|
export interface ILoadOptionsFunctions extends ILoadOptionsFunctionsBase {
|
||||||
helpers: {
|
helpers: {
|
||||||
request?: requestPromise.RequestPromiseAPI,
|
request?: requestPromise.RequestPromiseAPI;
|
||||||
requestOAuth2?: (this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options) => Promise<any>, // tslint:disable-line:no-any
|
requestOAuth2?: (
|
||||||
requestOAuth1?(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
|
this: IAllExecuteFunctions,
|
||||||
|
credentialsType: string,
|
||||||
|
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
|
||||||
|
oAuth2Options?: IOAuth2Options,
|
||||||
|
) => Promise<any>; // tslint:disable-line:no-any
|
||||||
|
requestOAuth1?(
|
||||||
|
this: IAllExecuteFunctions,
|
||||||
|
credentialsType: string,
|
||||||
|
requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions,
|
||||||
|
): Promise<any>; // tslint:disable-line:no-any
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface IHookFunctions extends IHookFunctionsBase {
|
export interface IHookFunctions extends IHookFunctionsBase {
|
||||||
helpers: {
|
helpers: {
|
||||||
request: requestPromise.RequestPromiseAPI,
|
request: requestPromise.RequestPromiseAPI;
|
||||||
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise<any>, // tslint:disable-line:no-any
|
requestOAuth2(
|
||||||
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
|
this: IAllExecuteFunctions,
|
||||||
|
credentialsType: string,
|
||||||
|
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
|
||||||
|
oAuth2Options?: IOAuth2Options,
|
||||||
|
): Promise<any>; // tslint:disable-line:no-any
|
||||||
|
requestOAuth1(
|
||||||
|
this: IAllExecuteFunctions,
|
||||||
|
credentialsType: string,
|
||||||
|
requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions,
|
||||||
|
): Promise<any>; // tslint:disable-line:no-any
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface IWebhookFunctions extends IWebhookFunctionsBase {
|
export interface IWebhookFunctions extends IWebhookFunctionsBase {
|
||||||
helpers: {
|
helpers: {
|
||||||
prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise<IBinaryData>;
|
prepareBinaryData(
|
||||||
request: requestPromise.RequestPromiseAPI,
|
binaryData: Buffer,
|
||||||
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise<any>, // tslint:disable-line:no-any
|
filePath?: string,
|
||||||
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
|
mimeType?: string,
|
||||||
|
): Promise<IBinaryData>;
|
||||||
|
request: requestPromise.RequestPromiseAPI;
|
||||||
|
requestOAuth2(
|
||||||
|
this: IAllExecuteFunctions,
|
||||||
|
credentialsType: string,
|
||||||
|
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
|
||||||
|
oAuth2Options?: IOAuth2Options,
|
||||||
|
): Promise<any>; // tslint:disable-line:no-any
|
||||||
|
requestOAuth1(
|
||||||
|
this: IAllExecuteFunctions,
|
||||||
|
credentialsType: string,
|
||||||
|
requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions,
|
||||||
|
): Promise<any>; // tslint:disable-line:no-any
|
||||||
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
|
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -128,19 +204,16 @@ export interface IWorkflowSettings extends IWorkflowSettingsWorkflow {
|
||||||
saveManualRuns?: boolean;
|
saveManualRuns?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// New node definition in file
|
// New node definition in file
|
||||||
export interface INodeDefinitionFile {
|
export interface INodeDefinitionFile {
|
||||||
[key: string]: Constructable<INodeType | ICredentialType>;
|
[key: string]: Constructable<INodeType | ICredentialType>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Is identical to TaskDataConnections but does not allow null value to be used as input for nodes
|
// Is identical to TaskDataConnections but does not allow null value to be used as input for nodes
|
||||||
export interface INodeInputDataConnections {
|
export interface INodeInputDataConnections {
|
||||||
[key: string]: INodeExecutionData[][];
|
[key: string]: INodeExecutionData[][];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface IWorkflowData {
|
export interface IWorkflowData {
|
||||||
pollResponses?: IPollResponse[];
|
pollResponses?: IPollResponse[];
|
||||||
triggerResponses?: ITriggerResponse[];
|
triggerResponses?: ITriggerResponse[];
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
import {
|
import {
|
||||||
INode,
|
INode,
|
||||||
INodeCredentials,
|
INodeCredentials,
|
||||||
|
@ -8,21 +9,24 @@ import {
|
||||||
Workflow,
|
Workflow,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import {
|
// eslint-disable-next-line import/no-cycle
|
||||||
NodeExecuteFunctions,
|
import { NodeExecuteFunctions } from '.';
|
||||||
} from './';
|
|
||||||
|
|
||||||
|
|
||||||
const TEMP_NODE_NAME = 'Temp-Node';
|
const TEMP_NODE_NAME = 'Temp-Node';
|
||||||
const TEMP_WORKFLOW_NAME = 'Temp-Workflow';
|
const TEMP_WORKFLOW_NAME = 'Temp-Workflow';
|
||||||
|
|
||||||
|
|
||||||
export class LoadNodeParameterOptions {
|
export class LoadNodeParameterOptions {
|
||||||
path: string;
|
path: string;
|
||||||
|
|
||||||
workflow: Workflow;
|
workflow: Workflow;
|
||||||
|
|
||||||
|
constructor(
|
||||||
constructor(nodeTypeName: string, nodeTypes: INodeTypes, path: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials) {
|
nodeTypeName: string,
|
||||||
|
nodeTypes: INodeTypes,
|
||||||
|
path: string,
|
||||||
|
currentNodeParameters: INodeParameters,
|
||||||
|
credentials?: INodeCredentials,
|
||||||
|
) {
|
||||||
this.path = path;
|
this.path = path;
|
||||||
const nodeType = nodeTypes.getByName(nodeTypeName);
|
const nodeType = nodeTypes.getByName(nodeTypeName);
|
||||||
|
|
||||||
|
@ -35,10 +39,7 @@ export class LoadNodeParameterOptions {
|
||||||
name: TEMP_NODE_NAME,
|
name: TEMP_NODE_NAME,
|
||||||
type: nodeTypeName,
|
type: nodeTypeName,
|
||||||
typeVersion: 1,
|
typeVersion: 1,
|
||||||
position: [
|
position: [0, 0],
|
||||||
0,
|
|
||||||
0,
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (credentials) {
|
if (credentials) {
|
||||||
|
@ -46,22 +47,25 @@ export class LoadNodeParameterOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
const workflowData = {
|
const workflowData = {
|
||||||
nodes: [
|
nodes: [nodeData],
|
||||||
nodeData,
|
|
||||||
],
|
|
||||||
connections: {},
|
connections: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
this.workflow = new Workflow({ nodes: workflowData.nodes, connections: workflowData.connections, active: false, nodeTypes });
|
this.workflow = new Workflow({
|
||||||
|
nodes: workflowData.nodes,
|
||||||
|
connections: workflowData.connections,
|
||||||
|
active: false,
|
||||||
|
nodeTypes,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns data of a fake workflow
|
* Returns data of a fake workflow
|
||||||
*
|
*
|
||||||
* @returns
|
* @returns
|
||||||
* @memberof LoadNodeParameterOptions
|
* @memberof LoadNodeParameterOptions
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
getWorkflowData() {
|
getWorkflowData() {
|
||||||
return {
|
return {
|
||||||
name: TEMP_WORKFLOW_NAME,
|
name: TEMP_WORKFLOW_NAME,
|
||||||
|
@ -73,7 +77,6 @@ export class LoadNodeParameterOptions {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the available options
|
* Returns the available options
|
||||||
*
|
*
|
||||||
|
@ -82,18 +85,31 @@ export class LoadNodeParameterOptions {
|
||||||
* @returns {Promise<INodePropertyOptions[]>}
|
* @returns {Promise<INodePropertyOptions[]>}
|
||||||
* @memberof LoadNodeParameterOptions
|
* @memberof LoadNodeParameterOptions
|
||||||
*/
|
*/
|
||||||
getOptions(methodName: string, additionalData: IWorkflowExecuteAdditionalData): Promise<INodePropertyOptions[]> {
|
async getOptions(
|
||||||
|
methodName: string,
|
||||||
|
additionalData: IWorkflowExecuteAdditionalData,
|
||||||
|
): Promise<INodePropertyOptions[]> {
|
||||||
const node = this.workflow.getNode(TEMP_NODE_NAME);
|
const node = this.workflow.getNode(TEMP_NODE_NAME);
|
||||||
|
|
||||||
const nodeType = this.workflow.nodeTypes.getByName(node!.type);
|
const nodeType = this.workflow.nodeTypes.getByName(node!.type);
|
||||||
|
|
||||||
if (nodeType!.methods === undefined || nodeType!.methods.loadOptions === undefined || nodeType!.methods.loadOptions[methodName] === undefined) {
|
if (
|
||||||
throw new Error(`The node-type "${node!.type}" does not have the method "${methodName}" defined!`);
|
nodeType!.methods === undefined ||
|
||||||
|
nodeType!.methods.loadOptions === undefined ||
|
||||||
|
nodeType!.methods.loadOptions[methodName] === undefined
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`The node-type "${node!.type}" does not have the method "${methodName}" defined!`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const thisArgs = NodeExecuteFunctions.getLoadOptionsFunctions(this.workflow, node!, this.path, additionalData);
|
const thisArgs = NodeExecuteFunctions.getLoadOptionsFunctions(
|
||||||
|
this.workflow,
|
||||||
|
node!,
|
||||||
|
this.path,
|
||||||
|
additionalData,
|
||||||
|
);
|
||||||
|
|
||||||
return nodeType!.methods.loadOptions[methodName].call(thisArgs);
|
return nodeType!.methods.loadOptions[methodName].call(thisArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,3 +1,11 @@
|
||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { randomBytes } from 'crypto';
|
||||||
|
// eslint-disable-next-line import/no-cycle
|
||||||
import {
|
import {
|
||||||
ENCRYPTION_KEY_ENV_OVERWRITE,
|
ENCRYPTION_KEY_ENV_OVERWRITE,
|
||||||
EXTENSIONS_SUBDIRECTORY,
|
EXTENSIONS_SUBDIRECTORY,
|
||||||
|
@ -7,20 +15,15 @@ import {
|
||||||
USER_SETTINGS_SUBFOLDER,
|
USER_SETTINGS_SUBFOLDER,
|
||||||
} from '.';
|
} from '.';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
import { randomBytes } from 'crypto';
|
|
||||||
const { promisify } = require('util');
|
const { promisify } = require('util');
|
||||||
|
|
||||||
const fsAccess = promisify(fs.access);
|
const fsAccess = promisify(fs.access);
|
||||||
const fsReadFile = promisify(fs.readFile);
|
const fsReadFile = promisify(fs.readFile);
|
||||||
const fsMkdir = promisify(fs.mkdir);
|
const fsMkdir = promisify(fs.mkdir);
|
||||||
const fsWriteFile = promisify(fs.writeFile);
|
const fsWriteFile = promisify(fs.writeFile);
|
||||||
|
|
||||||
|
let settingsCache: IUserSettings | undefined;
|
||||||
|
|
||||||
let settingsCache: IUserSettings | undefined = undefined;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the user settings if they do not exist yet
|
* Creates the user settings if they do not exist yet
|
||||||
|
@ -49,12 +52,12 @@ export async function prepareUserSettings(): Promise<IUserSettings> {
|
||||||
userSettings.encryptionKey = randomBytes(24).toString('base64');
|
userSettings.encryptionKey = randomBytes(24).toString('base64');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`UserSettings got generated and saved to: ${settingsPath}`);
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(`UserSettings were generated and saved to: ${settingsPath}`);
|
||||||
|
|
||||||
return writeUserSettings(userSettings, settingsPath);
|
return writeUserSettings(userSettings, settingsPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the encryption key which is used to encrypt
|
* Returns the encryption key which is used to encrypt
|
||||||
* the credentials.
|
* the credentials.
|
||||||
|
@ -62,6 +65,7 @@ export async function prepareUserSettings(): Promise<IUserSettings> {
|
||||||
* @export
|
* @export
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
export async function getEncryptionKey() {
|
export async function getEncryptionKey() {
|
||||||
if (process.env[ENCRYPTION_KEY_ENV_OVERWRITE] !== undefined) {
|
if (process.env[ENCRYPTION_KEY_ENV_OVERWRITE] !== undefined) {
|
||||||
return process.env[ENCRYPTION_KEY_ENV_OVERWRITE];
|
return process.env[ENCRYPTION_KEY_ENV_OVERWRITE];
|
||||||
|
@ -80,7 +84,6 @@ export async function getEncryptionKey() {
|
||||||
return userSettings.encryptionKey;
|
return userSettings.encryptionKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds/Overwrite the given settings in the currently
|
* Adds/Overwrite the given settings in the currently
|
||||||
* saved user settings
|
* saved user settings
|
||||||
|
@ -90,7 +93,10 @@ export async function getEncryptionKey() {
|
||||||
* @param {string} [settingsPath] Optional settings file path
|
* @param {string} [settingsPath] Optional settings file path
|
||||||
* @returns {Promise<IUserSettings>}
|
* @returns {Promise<IUserSettings>}
|
||||||
*/
|
*/
|
||||||
export async function addToUserSettings(addSettings: IUserSettings, settingsPath?: string): Promise<IUserSettings> {
|
export async function addToUserSettings(
|
||||||
|
addSettings: IUserSettings,
|
||||||
|
settingsPath?: string,
|
||||||
|
): Promise<IUserSettings> {
|
||||||
if (settingsPath === undefined) {
|
if (settingsPath === undefined) {
|
||||||
settingsPath = getUserSettingsPath();
|
settingsPath = getUserSettingsPath();
|
||||||
}
|
}
|
||||||
|
@ -107,7 +113,6 @@ export async function addToUserSettings(addSettings: IUserSettings, settingsPath
|
||||||
return writeUserSettings(userSettings, settingsPath);
|
return writeUserSettings(userSettings, settingsPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes a user settings file
|
* Writes a user settings file
|
||||||
*
|
*
|
||||||
|
@ -116,7 +121,10 @@ export async function addToUserSettings(addSettings: IUserSettings, settingsPath
|
||||||
* @param {string} [settingsPath] Optional settings file path
|
* @param {string} [settingsPath] Optional settings file path
|
||||||
* @returns {Promise<IUserSettings>}
|
* @returns {Promise<IUserSettings>}
|
||||||
*/
|
*/
|
||||||
export async function writeUserSettings(userSettings: IUserSettings, settingsPath?: string): Promise<IUserSettings> {
|
export async function writeUserSettings(
|
||||||
|
userSettings: IUserSettings,
|
||||||
|
settingsPath?: string,
|
||||||
|
): Promise<IUserSettings> {
|
||||||
if (settingsPath === undefined) {
|
if (settingsPath === undefined) {
|
||||||
settingsPath = getUserSettingsPath();
|
settingsPath = getUserSettingsPath();
|
||||||
}
|
}
|
||||||
|
@ -139,14 +147,16 @@ export async function writeUserSettings(userSettings: IUserSettings, settingsPat
|
||||||
return userSettings;
|
return userSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the content of the user settings
|
* Returns the content of the user settings
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
* @returns {UserSettings}
|
* @returns {UserSettings}
|
||||||
*/
|
*/
|
||||||
export async function getUserSettings(settingsPath?: string, ignoreCache?: boolean): Promise<IUserSettings | undefined> {
|
export async function getUserSettings(
|
||||||
|
settingsPath?: string,
|
||||||
|
ignoreCache?: boolean,
|
||||||
|
): Promise<IUserSettings | undefined> {
|
||||||
if (settingsCache !== undefined && ignoreCache !== true) {
|
if (settingsCache !== undefined && ignoreCache !== true) {
|
||||||
return settingsCache;
|
return settingsCache;
|
||||||
}
|
}
|
||||||
|
@ -167,13 +177,14 @@ export async function getUserSettings(settingsPath?: string, ignoreCache?: boole
|
||||||
try {
|
try {
|
||||||
settingsCache = JSON.parse(settingsFile);
|
settingsCache = JSON.parse(settingsFile);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Error parsing n8n-config file "${settingsPath}". It does not seem to be valid JSON.`);
|
throw new Error(
|
||||||
|
`Error parsing n8n-config file "${settingsPath}". It does not seem to be valid JSON.`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return settingsCache as IUserSettings;
|
return settingsCache as IUserSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the path to the user settings
|
* Returns the path to the user settings
|
||||||
*
|
*
|
||||||
|
@ -186,8 +197,6 @@ export function getUserSettingsPath(): string {
|
||||||
return path.join(n8nFolder, USER_SETTINGS_FILE_NAME);
|
return path.join(n8nFolder, USER_SETTINGS_FILE_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retruns the path to the n8n folder in which all n8n
|
* Retruns the path to the n8n folder in which all n8n
|
||||||
* related data gets saved
|
* related data gets saved
|
||||||
|
@ -206,7 +215,6 @@ export function getUserN8nFolderPath(): string {
|
||||||
return path.join(userFolder, USER_SETTINGS_SUBFOLDER);
|
return path.join(userFolder, USER_SETTINGS_SUBFOLDER);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the path to the n8n user folder with the custom
|
* Returns the path to the n8n user folder with the custom
|
||||||
* extensions like nodes and credentials
|
* extensions like nodes and credentials
|
||||||
|
@ -218,7 +226,6 @@ export function getUserN8nFolderCustomExtensionPath(): string {
|
||||||
return path.join(getUserN8nFolderPath(), EXTENSIONS_SUBDIRECTORY);
|
return path.join(getUserN8nFolderPath(), EXTENSIONS_SUBDIRECTORY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the home folder path of the user if
|
* Returns the home folder path of the user if
|
||||||
* none can be found it falls back to the current
|
* none can be found it falls back to the current
|
||||||
|
|
|
@ -1,3 +1,14 @@
|
||||||
|
/* eslint-disable @typescript-eslint/prefer-optional-chain */
|
||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
|
/* eslint-disable no-labels */
|
||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
|
/* eslint-disable no-continue */
|
||||||
|
/* eslint-disable no-prototype-builtins */
|
||||||
|
/* eslint-disable no-restricted-syntax */
|
||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
|
||||||
import * as PCancelable from 'p-cancelable';
|
import * as PCancelable from 'p-cancelable';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -20,24 +31,27 @@ import {
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
WorkflowOperationError,
|
WorkflowOperationError,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import {
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
NodeExecuteFunctions,
|
|
||||||
} from './';
|
|
||||||
|
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
|
// eslint-disable-next-line import/no-cycle
|
||||||
|
import { NodeExecuteFunctions } from '.';
|
||||||
|
|
||||||
export class WorkflowExecute {
|
export class WorkflowExecute {
|
||||||
runExecutionData: IRunExecutionData;
|
runExecutionData: IRunExecutionData;
|
||||||
|
|
||||||
private additionalData: IWorkflowExecuteAdditionalData;
|
private additionalData: IWorkflowExecuteAdditionalData;
|
||||||
|
|
||||||
private mode: WorkflowExecuteMode;
|
private mode: WorkflowExecuteMode;
|
||||||
|
|
||||||
|
constructor(
|
||||||
constructor(additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, runExecutionData?: IRunExecutionData) {
|
additionalData: IWorkflowExecuteAdditionalData,
|
||||||
|
mode: WorkflowExecuteMode,
|
||||||
|
runExecutionData?: IRunExecutionData,
|
||||||
|
) {
|
||||||
this.additionalData = additionalData;
|
this.additionalData = additionalData;
|
||||||
this.mode = mode;
|
this.mode = mode;
|
||||||
this.runExecutionData = runExecutionData || {
|
this.runExecutionData = runExecutionData || {
|
||||||
startData: {
|
startData: {},
|
||||||
},
|
|
||||||
resultData: {
|
resultData: {
|
||||||
runData: {},
|
runData: {},
|
||||||
},
|
},
|
||||||
|
@ -49,8 +63,6 @@ export class WorkflowExecute {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes the given workflow.
|
* Executes the given workflow.
|
||||||
*
|
*
|
||||||
|
@ -60,7 +72,8 @@ export class WorkflowExecute {
|
||||||
* @returns {(Promise<string>)}
|
* @returns {(Promise<string>)}
|
||||||
* @memberof WorkflowExecute
|
* @memberof WorkflowExecute
|
||||||
*/
|
*/
|
||||||
run(workflow: Workflow, startNode?: INode, destinationNode?: string): PCancelable<IRun> {
|
// @ts-ignore
|
||||||
|
async run(workflow: Workflow, startNode?: INode, destinationNode?: string): PCancelable<IRun> {
|
||||||
// Get the nodes to start workflow execution from
|
// Get the nodes to start workflow execution from
|
||||||
startNode = startNode || workflow.getStartNode(destinationNode);
|
startNode = startNode || workflow.getStartNode(destinationNode);
|
||||||
|
|
||||||
|
@ -69,7 +82,7 @@ export class WorkflowExecute {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a destination node is given we only run the direct parent nodes and no others
|
// If a destination node is given we only run the direct parent nodes and no others
|
||||||
let runNodeFilter: string[] | undefined = undefined;
|
let runNodeFilter: string[] | undefined;
|
||||||
if (destinationNode) {
|
if (destinationNode) {
|
||||||
runNodeFilter = workflow.getParentNodes(destinationNode);
|
runNodeFilter = workflow.getParentNodes(destinationNode);
|
||||||
runNodeFilter.push(destinationNode);
|
runNodeFilter.push(destinationNode);
|
||||||
|
@ -109,8 +122,6 @@ export class WorkflowExecute {
|
||||||
return this.processRunExecutionData(workflow);
|
return this.processRunExecutionData(workflow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes the given workflow but only
|
* Executes the given workflow but only
|
||||||
*
|
*
|
||||||
|
@ -122,7 +133,13 @@ export class WorkflowExecute {
|
||||||
* @memberof WorkflowExecute
|
* @memberof WorkflowExecute
|
||||||
*/
|
*/
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
async runPartialWorkflow(workflow: Workflow, runData: IRunData, startNodes: string[], destinationNode: string): PCancelable<IRun> {
|
async runPartialWorkflow(
|
||||||
|
workflow: Workflow,
|
||||||
|
runData: IRunData,
|
||||||
|
startNodes: string[],
|
||||||
|
destinationNode: string,
|
||||||
|
// @ts-ignore
|
||||||
|
): PCancelable<IRun> {
|
||||||
let incomingNodeConnections: INodeConnections | undefined;
|
let incomingNodeConnections: INodeConnections | undefined;
|
||||||
let connection: IConnection;
|
let connection: IConnection;
|
||||||
|
|
||||||
|
@ -150,7 +167,8 @@ export class WorkflowExecute {
|
||||||
for (let inputIndex = 0; inputIndex < connections.length; inputIndex++) {
|
for (let inputIndex = 0; inputIndex < connections.length; inputIndex++) {
|
||||||
connection = connections[inputIndex];
|
connection = connections[inputIndex];
|
||||||
incomingData.push(
|
incomingData.push(
|
||||||
runData[connection.node!][runIndex].data![connection.type][connection.index]!,
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
runData[connection.node][runIndex].data![connection.type][connection.index]!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -183,11 +201,12 @@ export class WorkflowExecute {
|
||||||
waitingExecution[destinationNode][runIndex][connection.type] = [];
|
waitingExecution[destinationNode][runIndex][connection.type] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (runData[connection.node] !== undefined) {
|
||||||
if (runData[connection.node!] !== undefined) {
|
|
||||||
// Input data exists so add as waiting
|
// Input data exists so add as waiting
|
||||||
// incomingDataDestination.push(runData[connection.node!][runIndex].data![connection.type][connection.index]);
|
// incomingDataDestination.push(runData[connection.node!][runIndex].data![connection.type][connection.index]);
|
||||||
waitingExecution[destinationNode][runIndex][connection.type].push(runData[connection.node!][runIndex].data![connection.type][connection.index]);
|
waitingExecution[destinationNode][runIndex][connection.type].push(
|
||||||
|
runData[connection.node][runIndex].data![connection.type][connection.index],
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
waitingExecution[destinationNode][runIndex][connection.type].push(null);
|
waitingExecution[destinationNode][runIndex][connection.type].push(null);
|
||||||
}
|
}
|
||||||
|
@ -197,7 +216,8 @@ export class WorkflowExecute {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only run the parent nodes and no others
|
// Only run the parent nodes and no others
|
||||||
let runNodeFilter: string[] | undefined = undefined;
|
let runNodeFilter: string[] | undefined;
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
runNodeFilter = workflow.getParentNodes(destinationNode);
|
runNodeFilter = workflow.getParentNodes(destinationNode);
|
||||||
runNodeFilter.push(destinationNode);
|
runNodeFilter.push(destinationNode);
|
||||||
|
|
||||||
|
@ -219,8 +239,6 @@ export class WorkflowExecute {
|
||||||
return this.processRunExecutionData(workflow);
|
return this.processRunExecutionData(workflow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes the hook with the given name
|
* Executes the hook with the given name
|
||||||
*
|
*
|
||||||
|
@ -229,22 +247,31 @@ export class WorkflowExecute {
|
||||||
* @returns {Promise<IRun>}
|
* @returns {Promise<IRun>}
|
||||||
* @memberof WorkflowExecute
|
* @memberof WorkflowExecute
|
||||||
*/
|
*/
|
||||||
async executeHook(hookName: string, parameters: any[]): Promise<void> { // tslint:disable-line:no-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
async executeHook(hookName: string, parameters: any[]): Promise<void> {
|
||||||
|
// tslint:disable-line:no-any
|
||||||
if (this.additionalData.hooks === undefined) {
|
if (this.additionalData.hooks === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
return this.additionalData.hooks.executeHookFunctions(hookName, parameters);
|
return this.additionalData.hooks.executeHookFunctions(hookName, parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks the incoming connection does not receive any data
|
* Checks the incoming connection does not receive any data
|
||||||
*/
|
*/
|
||||||
incomingConnectionIsEmpty(runData: IRunData, inputConnections: IConnection[], runIndex: number): boolean {
|
incomingConnectionIsEmpty(
|
||||||
|
runData: IRunData,
|
||||||
|
inputConnections: IConnection[],
|
||||||
|
runIndex: number,
|
||||||
|
): boolean {
|
||||||
// for (const inputConnection of workflow.connectionsByDestinationNode[nodeToAdd].main[0]) {
|
// for (const inputConnection of workflow.connectionsByDestinationNode[nodeToAdd].main[0]) {
|
||||||
for (const inputConnection of inputConnections) {
|
for (const inputConnection of inputConnections) {
|
||||||
const nodeIncomingData = get(runData, `[${inputConnection.node}][${runIndex}].data.main[${inputConnection.index}]`);
|
const nodeIncomingData = get(
|
||||||
|
runData,
|
||||||
|
`[${inputConnection.node}][${runIndex}].data.main[${inputConnection.index}]`,
|
||||||
|
);
|
||||||
if (nodeIncomingData !== undefined && (nodeIncomingData as object[]).length !== 0) {
|
if (nodeIncomingData !== undefined && (nodeIncomingData as object[]).length !== 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -252,79 +279,117 @@ export class WorkflowExecute {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addNodeToBeExecuted(
|
||||||
addNodeToBeExecuted(workflow: Workflow, connectionData: IConnection, outputIndex: number, parentNodeName: string, nodeSuccessData: INodeExecutionData[][], runIndex: number): void {
|
workflow: Workflow,
|
||||||
|
connectionData: IConnection,
|
||||||
|
outputIndex: number,
|
||||||
|
parentNodeName: string,
|
||||||
|
nodeSuccessData: INodeExecutionData[][],
|
||||||
|
runIndex: number,
|
||||||
|
): void {
|
||||||
let stillDataMissing = false;
|
let stillDataMissing = false;
|
||||||
|
|
||||||
// Check if node has multiple inputs as then we have to wait for all input data
|
// Check if node has multiple inputs as then we have to wait for all input data
|
||||||
// to be present before we can add it to the node-execution-stack
|
// to be present before we can add it to the node-execution-stack
|
||||||
if (workflow.connectionsByDestinationNode[connectionData.node]['main'].length > 1) {
|
if (workflow.connectionsByDestinationNode[connectionData.node].main.length > 1) {
|
||||||
// Node has multiple inputs
|
// Node has multiple inputs
|
||||||
let nodeWasWaiting = true;
|
let nodeWasWaiting = true;
|
||||||
|
|
||||||
// Check if there is already data for the node
|
// Check if there is already data for the node
|
||||||
if (this.runExecutionData.executionData!.waitingExecution[connectionData.node] === undefined) {
|
if (
|
||||||
|
this.runExecutionData.executionData!.waitingExecution[connectionData.node] === undefined
|
||||||
|
) {
|
||||||
// Node does not have data yet so create a new empty one
|
// Node does not have data yet so create a new empty one
|
||||||
this.runExecutionData.executionData!.waitingExecution[connectionData.node] = {};
|
this.runExecutionData.executionData!.waitingExecution[connectionData.node] = {};
|
||||||
nodeWasWaiting = false;
|
nodeWasWaiting = false;
|
||||||
}
|
}
|
||||||
if (this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] === undefined) {
|
if (
|
||||||
|
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] ===
|
||||||
|
undefined
|
||||||
|
) {
|
||||||
// Node does not have data for runIndex yet so create also empty one and init it
|
// Node does not have data for runIndex yet so create also empty one and init it
|
||||||
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] = {
|
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] = {
|
||||||
main: [],
|
main: [],
|
||||||
};
|
};
|
||||||
for (let i = 0; i < workflow.connectionsByDestinationNode[connectionData.node]['main'].length; i++) {
|
for (
|
||||||
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main.push(null);
|
let i = 0;
|
||||||
|
i < workflow.connectionsByDestinationNode[connectionData.node].main.length;
|
||||||
|
i++
|
||||||
|
) {
|
||||||
|
this.runExecutionData.executionData!.waitingExecution[connectionData.node][
|
||||||
|
runIndex
|
||||||
|
].main.push(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the new data
|
// Add the new data
|
||||||
if (nodeSuccessData === null) {
|
if (nodeSuccessData === null) {
|
||||||
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main[connectionData.index] = null;
|
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main[
|
||||||
|
connectionData.index
|
||||||
|
] = null;
|
||||||
} else {
|
} else {
|
||||||
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main[connectionData.index] = nodeSuccessData[outputIndex];
|
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main[
|
||||||
|
connectionData.index
|
||||||
|
] = nodeSuccessData[outputIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if all data exists now
|
// Check if all data exists now
|
||||||
let thisExecutionData: INodeExecutionData[] | null;
|
let thisExecutionData: INodeExecutionData[] | null;
|
||||||
let allDataFound = true;
|
let allDataFound = true;
|
||||||
for (let i = 0; i < this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main.length; i++) {
|
for (
|
||||||
thisExecutionData = this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main[i];
|
let i = 0;
|
||||||
|
i <
|
||||||
|
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main
|
||||||
|
.length;
|
||||||
|
i++
|
||||||
|
) {
|
||||||
|
thisExecutionData =
|
||||||
|
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main[
|
||||||
|
i
|
||||||
|
];
|
||||||
if (thisExecutionData === null) {
|
if (thisExecutionData === null) {
|
||||||
allDataFound = false;
|
allDataFound = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allDataFound === true) {
|
if (allDataFound) {
|
||||||
// All data exists for node to be executed
|
// All data exists for node to be executed
|
||||||
// So add it to the execution stack
|
// So add it to the execution stack
|
||||||
this.runExecutionData.executionData!.nodeExecutionStack.push({
|
this.runExecutionData.executionData!.nodeExecutionStack.push({
|
||||||
node: workflow.nodes[connectionData.node],
|
node: workflow.nodes[connectionData.node],
|
||||||
data: this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex],
|
data: this.runExecutionData.executionData!.waitingExecution[connectionData.node][
|
||||||
|
runIndex
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove the data from waiting
|
// Remove the data from waiting
|
||||||
delete this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex];
|
delete this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex];
|
||||||
|
|
||||||
if (Object.keys(this.runExecutionData.executionData!.waitingExecution[connectionData.node]).length === 0) {
|
if (
|
||||||
|
Object.keys(this.runExecutionData.executionData!.waitingExecution[connectionData.node])
|
||||||
|
.length === 0
|
||||||
|
) {
|
||||||
// No more data left for the node so also delete that one
|
// No more data left for the node so also delete that one
|
||||||
delete this.runExecutionData.executionData!.waitingExecution[connectionData.node];
|
delete this.runExecutionData.executionData!.waitingExecution[connectionData.node];
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
} else {
|
|
||||||
stillDataMissing = true;
|
|
||||||
}
|
}
|
||||||
|
stillDataMissing = true;
|
||||||
|
|
||||||
if (nodeWasWaiting === false) {
|
if (!nodeWasWaiting) {
|
||||||
|
|
||||||
// Get a list of all the output nodes that we can check for siblings easier
|
// Get a list of all the output nodes that we can check for siblings easier
|
||||||
const checkOutputNodes = [];
|
const checkOutputNodes = [];
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-for-in-array
|
||||||
for (const outputIndexParent in workflow.connectionsBySourceNode[parentNodeName].main) {
|
for (const outputIndexParent in workflow.connectionsBySourceNode[parentNodeName].main) {
|
||||||
if (!workflow.connectionsBySourceNode[parentNodeName].main.hasOwnProperty(outputIndexParent)) {
|
if (
|
||||||
|
!workflow.connectionsBySourceNode[parentNodeName].main.hasOwnProperty(outputIndexParent)
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (const connectionDataCheck of workflow.connectionsBySourceNode[parentNodeName].main[outputIndexParent]) {
|
for (const connectionDataCheck of workflow.connectionsBySourceNode[parentNodeName].main[
|
||||||
|
outputIndexParent
|
||||||
|
]) {
|
||||||
checkOutputNodes.push(connectionDataCheck.node);
|
checkOutputNodes.push(connectionDataCheck.node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -333,14 +398,22 @@ export class WorkflowExecute {
|
||||||
// checked. So we have to go through all the inputs and check if they
|
// checked. So we have to go through all the inputs and check if they
|
||||||
// are already on the list to be processed.
|
// are already on the list to be processed.
|
||||||
// If that is not the case add it.
|
// If that is not the case add it.
|
||||||
for (let inputIndex = 0; inputIndex < workflow.connectionsByDestinationNode[connectionData.node]['main'].length; inputIndex++) {
|
for (
|
||||||
for (const inputData of workflow.connectionsByDestinationNode[connectionData.node]['main'][inputIndex]) {
|
let inputIndex = 0;
|
||||||
|
inputIndex < workflow.connectionsByDestinationNode[connectionData.node].main.length;
|
||||||
|
inputIndex++
|
||||||
|
) {
|
||||||
|
for (const inputData of workflow.connectionsByDestinationNode[connectionData.node].main[
|
||||||
|
inputIndex
|
||||||
|
]) {
|
||||||
if (inputData.node === parentNodeName) {
|
if (inputData.node === parentNodeName) {
|
||||||
// Is the node we come from so its data will be available for sure
|
// Is the node we come from so its data will be available for sure
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const executionStackNodes = this.runExecutionData.executionData!.nodeExecutionStack.map((stackData) => stackData.node.name);
|
const executionStackNodes = this.runExecutionData.executionData!.nodeExecutionStack.map(
|
||||||
|
(stackData) => stackData.node.name,
|
||||||
|
);
|
||||||
|
|
||||||
// Check if that node is also an output connection of the
|
// Check if that node is also an output connection of the
|
||||||
// previously processed one
|
// previously processed one
|
||||||
|
@ -349,7 +422,13 @@ export class WorkflowExecute {
|
||||||
// will then process this node next. So nothing to do
|
// will then process this node next. So nothing to do
|
||||||
// unless the incoming data of the node is empty
|
// unless the incoming data of the node is empty
|
||||||
// because then it would not be executed
|
// because then it would not be executed
|
||||||
if (!this.incomingConnectionIsEmpty(this.runExecutionData.resultData.runData, workflow.connectionsByDestinationNode[inputData.node].main[0], runIndex)) {
|
if (
|
||||||
|
!this.incomingConnectionIsEmpty(
|
||||||
|
this.runExecutionData.resultData.runData,
|
||||||
|
workflow.connectionsByDestinationNode[inputData.node].main[0],
|
||||||
|
runIndex,
|
||||||
|
)
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -402,7 +481,10 @@ export class WorkflowExecute {
|
||||||
nodeToAdd = parentNode;
|
nodeToAdd = parentNode;
|
||||||
}
|
}
|
||||||
const parentNodesNodeToAdd = workflow.getParentNodes(nodeToAdd as string);
|
const parentNodesNodeToAdd = workflow.getParentNodes(nodeToAdd as string);
|
||||||
if (parentNodesNodeToAdd.includes(parentNodeName) && nodeSuccessData[outputIndex].length === 0) {
|
if (
|
||||||
|
parentNodesNodeToAdd.includes(parentNodeName) &&
|
||||||
|
nodeSuccessData[outputIndex].length === 0
|
||||||
|
) {
|
||||||
// We do not add the node if there is no input data and the node that should be connected
|
// We do not add the node if there is no input data and the node that should be connected
|
||||||
// is a child of the parent node. Because else it would run a node even though it should be
|
// is a child of the parent node. Because else it would run a node even though it should be
|
||||||
// specifically not run, as it did not receive any data.
|
// specifically not run, as it did not receive any data.
|
||||||
|
@ -419,30 +501,32 @@ export class WorkflowExecute {
|
||||||
if (workflow.connectionsByDestinationNode[nodeToAdd] === undefined) {
|
if (workflow.connectionsByDestinationNode[nodeToAdd] === undefined) {
|
||||||
// Add empty item if the node does not have any input connections
|
// Add empty item if the node does not have any input connections
|
||||||
addEmptyItem = true;
|
addEmptyItem = true;
|
||||||
} else {
|
} else if (
|
||||||
if (this.incomingConnectionIsEmpty(this.runExecutionData.resultData.runData, workflow.connectionsByDestinationNode[nodeToAdd].main[0], runIndex)) {
|
this.incomingConnectionIsEmpty(
|
||||||
// Add empty item also if the input data is empty
|
this.runExecutionData.resultData.runData,
|
||||||
addEmptyItem = true;
|
workflow.connectionsByDestinationNode[nodeToAdd].main[0],
|
||||||
}
|
runIndex,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// Add empty item also if the input data is empty
|
||||||
|
addEmptyItem = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (addEmptyItem === true) {
|
if (addEmptyItem) {
|
||||||
// Add only node if it does not have any inputs because else it will
|
// Add only node if it does not have any inputs because else it will
|
||||||
// be added by its input node later anyway.
|
// be added by its input node later anyway.
|
||||||
this.runExecutionData.executionData!.nodeExecutionStack.push(
|
this.runExecutionData.executionData!.nodeExecutionStack.push({
|
||||||
{
|
node: workflow.getNode(nodeToAdd) as INode,
|
||||||
node: workflow.getNode(nodeToAdd) as INode,
|
data: {
|
||||||
data: {
|
main: [
|
||||||
main: [
|
[
|
||||||
[
|
{
|
||||||
{
|
json: {},
|
||||||
json: {},
|
},
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -462,9 +546,11 @@ export class WorkflowExecute {
|
||||||
connectionDataArray[connectionData.index] = nodeSuccessData[outputIndex];
|
connectionDataArray[connectionData.index] = nodeSuccessData[outputIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stillDataMissing === true) {
|
if (stillDataMissing) {
|
||||||
// Additional data is needed to run node so add it to waiting
|
// Additional data is needed to run node so add it to waiting
|
||||||
if (!this.runExecutionData.executionData!.waitingExecution.hasOwnProperty(connectionData.node)) {
|
if (
|
||||||
|
!this.runExecutionData.executionData!.waitingExecution.hasOwnProperty(connectionData.node)
|
||||||
|
) {
|
||||||
this.runExecutionData.executionData!.waitingExecution[connectionData.node] = {};
|
this.runExecutionData.executionData!.waitingExecution[connectionData.node] = {};
|
||||||
}
|
}
|
||||||
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] = {
|
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] = {
|
||||||
|
@ -481,7 +567,6 @@ export class WorkflowExecute {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs the given execution data.
|
* Runs the given execution data.
|
||||||
*
|
*
|
||||||
|
@ -489,14 +574,17 @@ export class WorkflowExecute {
|
||||||
* @returns {Promise<string>}
|
* @returns {Promise<string>}
|
||||||
* @memberof WorkflowExecute
|
* @memberof WorkflowExecute
|
||||||
*/
|
*/
|
||||||
processRunExecutionData(workflow: Workflow): PCancelable<IRun> {
|
// @ts-ignore
|
||||||
|
async processRunExecutionData(workflow: Workflow): PCancelable<IRun> {
|
||||||
Logger.verbose('Workflow execution started', { workflowId: workflow.id });
|
Logger.verbose('Workflow execution started', { workflowId: workflow.id });
|
||||||
|
|
||||||
const startedAt = new Date();
|
const startedAt = new Date();
|
||||||
|
|
||||||
const workflowIssues = workflow.checkReadyForExecution();
|
const workflowIssues = workflow.checkReadyForExecution();
|
||||||
if (workflowIssues !== null) {
|
if (workflowIssues !== null) {
|
||||||
throw new Error('The workflow has issues and can for that reason not be executed. Please fix them first.');
|
throw new Error(
|
||||||
|
'The workflow has issues and can for that reason not be executed. Please fix them first.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Variables which hold temporary data for each node-execution
|
// Variables which hold temporary data for each node-execution
|
||||||
|
@ -512,10 +600,17 @@ export class WorkflowExecute {
|
||||||
this.runExecutionData.startData = {};
|
this.runExecutionData.startData = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.runExecutionData.waitTill) {
|
||||||
|
const lastNodeExecuted = this.runExecutionData.resultData.lastNodeExecuted as string;
|
||||||
|
this.runExecutionData.executionData!.nodeExecutionStack[0].node.disabled = true;
|
||||||
|
this.runExecutionData.waitTill = undefined;
|
||||||
|
this.runExecutionData.resultData.runData[lastNodeExecuted].pop();
|
||||||
|
}
|
||||||
|
|
||||||
let currentExecutionTry = '';
|
let currentExecutionTry = '';
|
||||||
let lastExecutionTry = '';
|
let lastExecutionTry = '';
|
||||||
|
|
||||||
return new PCancelable((resolve, reject, onCancel) => {
|
return new PCancelable(async (resolve, reject, onCancel) => {
|
||||||
let gotCancel = false;
|
let gotCancel = false;
|
||||||
|
|
||||||
onCancel.shouldReject = false;
|
onCancel.shouldReject = false;
|
||||||
|
@ -527,7 +622,6 @@ export class WorkflowExecute {
|
||||||
try {
|
try {
|
||||||
await this.executeHook('workflowExecuteBefore', [workflow]);
|
await this.executeHook('workflowExecuteBefore', [workflow]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
||||||
// Set the error that it can be saved correctly
|
// Set the error that it can be saved correctly
|
||||||
executionError = {
|
executionError = {
|
||||||
...error,
|
...error,
|
||||||
|
@ -536,16 +630,17 @@ export class WorkflowExecute {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set the incoming data of the node that it can be saved correctly
|
// Set the incoming data of the node that it can be saved correctly
|
||||||
executionData = this.runExecutionData.executionData!.nodeExecutionStack[0] as IExecuteData;
|
// eslint-disable-next-line prefer-destructuring
|
||||||
|
executionData = this.runExecutionData.executionData!.nodeExecutionStack[0];
|
||||||
this.runExecutionData.resultData = {
|
this.runExecutionData.resultData = {
|
||||||
runData: {
|
runData: {
|
||||||
[executionData.node.name]: [
|
[executionData.node.name]: [
|
||||||
{
|
{
|
||||||
startTime,
|
startTime,
|
||||||
executionTime: (new Date().getTime()) - startTime,
|
executionTime: new Date().getTime() - startTime,
|
||||||
data: ({
|
data: {
|
||||||
'main': executionData.data.main,
|
main: executionData.data.main,
|
||||||
} as ITaskDataConnections),
|
} as ITaskDataConnections,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -556,24 +651,31 @@ export class WorkflowExecute {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
executionLoop:
|
executionLoop: while (
|
||||||
while (this.runExecutionData.executionData!.nodeExecutionStack.length !== 0) {
|
this.runExecutionData.executionData!.nodeExecutionStack.length !== 0
|
||||||
|
) {
|
||||||
if (this.additionalData.executionTimeoutTimestamp !== undefined && Date.now() >= this.additionalData.executionTimeoutTimestamp) {
|
if (
|
||||||
|
this.additionalData.executionTimeoutTimestamp !== undefined &&
|
||||||
|
Date.now() >= this.additionalData.executionTimeoutTimestamp
|
||||||
|
) {
|
||||||
gotCancel = true;
|
gotCancel = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (gotCancel === true) {
|
if (gotCancel) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeSuccessData = null;
|
nodeSuccessData = null;
|
||||||
executionError = undefined;
|
executionError = undefined;
|
||||||
executionData = this.runExecutionData.executionData!.nodeExecutionStack.shift() as IExecuteData;
|
executionData =
|
||||||
|
this.runExecutionData.executionData!.nodeExecutionStack.shift() as IExecuteData;
|
||||||
executionNode = executionData.node;
|
executionNode = executionData.node;
|
||||||
|
|
||||||
Logger.debug(`Start processing node "${executionNode.name}"`, { node: executionNode.name, workflowId: workflow.id });
|
Logger.debug(`Start processing node "${executionNode.name}"`, {
|
||||||
|
node: executionNode.name,
|
||||||
|
workflowId: workflow.id,
|
||||||
|
});
|
||||||
await this.executeHook('nodeExecuteBefore', [executionNode.name]);
|
await this.executeHook('nodeExecuteBefore', [executionNode.name]);
|
||||||
|
|
||||||
// Get the index of the current run
|
// Get the index of the current run
|
||||||
|
@ -588,7 +690,10 @@ export class WorkflowExecute {
|
||||||
throw new Error('Did stop execution because execution seems to be in endless loop.');
|
throw new Error('Did stop execution because execution seems to be in endless loop.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.runExecutionData.startData!.runNodeFilter !== undefined && this.runExecutionData.startData!.runNodeFilter!.indexOf(executionNode.name) === -1) {
|
if (
|
||||||
|
this.runExecutionData.startData!.runNodeFilter !== undefined &&
|
||||||
|
this.runExecutionData.startData!.runNodeFilter.indexOf(executionNode.name) === -1
|
||||||
|
) {
|
||||||
// If filter is set and node is not on filter skip it, that avoids the problem that it executes
|
// If filter is set and node is not on filter skip it, that avoids the problem that it executes
|
||||||
// leafs that are parallel to a selected destinationNode. Normally it would execute them because
|
// leafs that are parallel to a selected destinationNode. Normally it would execute them because
|
||||||
// they have the same parent and it executes all child nodes.
|
// they have the same parent and it executes all child nodes.
|
||||||
|
@ -602,17 +707,24 @@ export class WorkflowExecute {
|
||||||
let inputConnections: IConnection[][];
|
let inputConnections: IConnection[][];
|
||||||
let connectionIndex: number;
|
let connectionIndex: number;
|
||||||
|
|
||||||
inputConnections = workflow.connectionsByDestinationNode[executionNode.name]['main'];
|
// eslint-disable-next-line prefer-const
|
||||||
|
inputConnections = workflow.connectionsByDestinationNode[executionNode.name].main;
|
||||||
|
|
||||||
for (connectionIndex = 0; connectionIndex < inputConnections.length; connectionIndex++) {
|
for (
|
||||||
if (workflow.getHighestNode(executionNode.name, 'main', connectionIndex).length === 0) {
|
connectionIndex = 0;
|
||||||
|
connectionIndex < inputConnections.length;
|
||||||
|
connectionIndex++
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
workflow.getHighestNode(executionNode.name, 'main', connectionIndex).length === 0
|
||||||
|
) {
|
||||||
// If there is no valid incoming node (if all are disabled)
|
// If there is no valid incoming node (if all are disabled)
|
||||||
// then ignore that it has inputs and simply execute it as it is without
|
// then ignore that it has inputs and simply execute it as it is without
|
||||||
// any data
|
// any data
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!executionData.data!.hasOwnProperty('main')) {
|
if (!executionData.data.hasOwnProperty('main')) {
|
||||||
// ExecutionData does not even have the connection set up so can
|
// ExecutionData does not even have the connection set up so can
|
||||||
// not have that data, so add it again to be executed later
|
// not have that data, so add it again to be executed later
|
||||||
this.runExecutionData.executionData!.nodeExecutionStack.push(executionData);
|
this.runExecutionData.executionData!.nodeExecutionStack.push(executionData);
|
||||||
|
@ -623,7 +735,10 @@ export class WorkflowExecute {
|
||||||
// Check if it has the data for all the inputs
|
// Check if it has the data for all the inputs
|
||||||
// The most nodes just have one but merge node for example has two and data
|
// The most nodes just have one but merge node for example has two and data
|
||||||
// of both inputs has to be available to be able to process the node.
|
// of both inputs has to be available to be able to process the node.
|
||||||
if (executionData.data!.main!.length < connectionIndex || executionData.data!.main![connectionIndex] === null) {
|
if (
|
||||||
|
executionData.data.main!.length < connectionIndex ||
|
||||||
|
executionData.data.main![connectionIndex] === null
|
||||||
|
) {
|
||||||
// Does not have the data of the connections so add back to stack
|
// Does not have the data of the connections so add back to stack
|
||||||
this.runExecutionData.executionData!.nodeExecutionStack.push(executionData);
|
this.runExecutionData.executionData!.nodeExecutionStack.push(executionData);
|
||||||
lastExecutionTry = currentExecutionTry;
|
lastExecutionTry = currentExecutionTry;
|
||||||
|
@ -647,22 +762,25 @@ export class WorkflowExecute {
|
||||||
let waitBetweenTries = 0;
|
let waitBetweenTries = 0;
|
||||||
if (executionData.node.retryOnFail === true) {
|
if (executionData.node.retryOnFail === true) {
|
||||||
// TODO: Remove the hardcoded default-values here and also in NodeSettings.vue
|
// TODO: Remove the hardcoded default-values here and also in NodeSettings.vue
|
||||||
waitBetweenTries = Math.min(5000, Math.max(0, executionData.node.waitBetweenTries || 1000));
|
waitBetweenTries = Math.min(
|
||||||
|
5000,
|
||||||
|
Math.max(0, executionData.node.waitBetweenTries || 1000),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let tryIndex = 0; tryIndex < maxTries; tryIndex++) {
|
for (let tryIndex = 0; tryIndex < maxTries; tryIndex++) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (gotCancel === true) {
|
if (gotCancel) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (tryIndex !== 0) {
|
if (tryIndex !== 0) {
|
||||||
// Reset executionError from previous error try
|
// Reset executionError from previous error try
|
||||||
executionError = undefined;
|
executionError = undefined;
|
||||||
if (waitBetweenTries !== 0) {
|
if (waitBetweenTries !== 0) {
|
||||||
// TODO: Improve that in the future and check if other nodes can
|
// TODO: Improve that in the future and check if other nodes can
|
||||||
// be executed in the meantime
|
// be executed in the meantime
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resolve(undefined);
|
resolve(undefined);
|
||||||
|
@ -671,9 +789,23 @@ export class WorkflowExecute {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.debug(`Running node "${executionNode.name}" started`, { node: executionNode.name, workflowId: workflow.id });
|
Logger.debug(`Running node "${executionNode.name}" started`, {
|
||||||
nodeSuccessData = await workflow.runNode(executionData.node, executionData.data, this.runExecutionData, runIndex, this.additionalData, NodeExecuteFunctions, this.mode);
|
node: executionNode.name,
|
||||||
Logger.debug(`Running node "${executionNode.name}" finished successfully`, { node: executionNode.name, workflowId: workflow.id });
|
workflowId: workflow.id,
|
||||||
|
});
|
||||||
|
nodeSuccessData = await workflow.runNode(
|
||||||
|
executionData.node,
|
||||||
|
executionData.data,
|
||||||
|
this.runExecutionData,
|
||||||
|
runIndex,
|
||||||
|
this.additionalData,
|
||||||
|
NodeExecuteFunctions,
|
||||||
|
this.mode,
|
||||||
|
);
|
||||||
|
Logger.debug(`Running node "${executionNode.name}" finished successfully`, {
|
||||||
|
node: executionNode.name,
|
||||||
|
workflowId: workflow.id,
|
||||||
|
});
|
||||||
|
|
||||||
if (nodeSuccessData === undefined) {
|
if (nodeSuccessData === undefined) {
|
||||||
// Node did not get executed
|
// Node did not get executed
|
||||||
|
@ -693,7 +825,7 @@ export class WorkflowExecute {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nodeSuccessData === null) {
|
if (nodeSuccessData === null && !this.runExecutionData.waitTill!) {
|
||||||
// If null gets returned it means that the node did succeed
|
// If null gets returned it means that the node did succeed
|
||||||
// but did not have any data. So the branch should end
|
// but did not have any data. So the branch should end
|
||||||
// (meaning the nodes afterwards should not be processed)
|
// (meaning the nodes afterwards should not be processed)
|
||||||
|
@ -702,7 +834,6 @@ export class WorkflowExecute {
|
||||||
|
|
||||||
break;
|
break;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
||||||
this.runExecutionData.resultData.lastNodeExecuted = executionData.node.name;
|
this.runExecutionData.resultData.lastNodeExecuted = executionData.node.name;
|
||||||
|
|
||||||
executionError = {
|
executionError = {
|
||||||
|
@ -711,7 +842,10 @@ export class WorkflowExecute {
|
||||||
stack: error.stack,
|
stack: error.stack,
|
||||||
};
|
};
|
||||||
|
|
||||||
Logger.debug(`Running node "${executionNode.name}" finished with error`, { node: executionNode.name, workflowId: workflow.id });
|
Logger.debug(`Running node "${executionNode.name}" finished with error`, {
|
||||||
|
node: executionNode.name,
|
||||||
|
workflowId: workflow.id,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -723,7 +857,7 @@ export class WorkflowExecute {
|
||||||
}
|
}
|
||||||
taskData = {
|
taskData = {
|
||||||
startTime,
|
startTime,
|
||||||
executionTime: (new Date().getTime()) - startTime,
|
executionTime: new Date().getTime() - startTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (executionError !== undefined) {
|
if (executionError !== undefined) {
|
||||||
|
@ -735,7 +869,7 @@ export class WorkflowExecute {
|
||||||
// Simply get the input data of the node if it has any and pass it through
|
// Simply get the input data of the node if it has any and pass it through
|
||||||
// to the next node
|
// to the next node
|
||||||
if (executionData.data.main[0] !== null) {
|
if (executionData.data.main[0] !== null) {
|
||||||
nodeSuccessData = [executionData.data.main[0] as INodeExecutionData[]];
|
nodeSuccessData = [executionData.data.main[0]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -745,50 +879,97 @@ export class WorkflowExecute {
|
||||||
// Add the execution data again so that it can get restarted
|
// Add the execution data again so that it can get restarted
|
||||||
this.runExecutionData.executionData!.nodeExecutionStack.unshift(executionData);
|
this.runExecutionData.executionData!.nodeExecutionStack.unshift(executionData);
|
||||||
|
|
||||||
await this.executeHook('nodeExecuteAfter', [executionNode.name, taskData, this.runExecutionData]);
|
await this.executeHook('nodeExecuteAfter', [
|
||||||
|
executionNode.name,
|
||||||
|
taskData,
|
||||||
|
this.runExecutionData,
|
||||||
|
]);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Node executed successfully. So add data and go on.
|
// Node executed successfully. So add data and go on.
|
||||||
taskData.data = ({
|
taskData.data = {
|
||||||
'main': nodeSuccessData,
|
main: nodeSuccessData,
|
||||||
} as ITaskDataConnections);
|
} as ITaskDataConnections;
|
||||||
|
|
||||||
this.runExecutionData.resultData.runData[executionNode.name].push(taskData);
|
this.runExecutionData.resultData.runData[executionNode.name].push(taskData);
|
||||||
|
|
||||||
if (this.runExecutionData.startData && this.runExecutionData.startData.destinationNode && this.runExecutionData.startData.destinationNode === executionNode.name) {
|
if (
|
||||||
|
this.runExecutionData.startData &&
|
||||||
|
this.runExecutionData.startData.destinationNode &&
|
||||||
|
this.runExecutionData.startData.destinationNode === executionNode.name
|
||||||
|
) {
|
||||||
// Before stopping, make sure we are executing hooks so
|
// Before stopping, make sure we are executing hooks so
|
||||||
// That frontend is notified for example for manual executions.
|
// That frontend is notified for example for manual executions.
|
||||||
await this.executeHook('nodeExecuteAfter', [executionNode.name, taskData, this.runExecutionData]);
|
await this.executeHook('nodeExecuteAfter', [
|
||||||
|
executionNode.name,
|
||||||
|
taskData,
|
||||||
|
this.runExecutionData,
|
||||||
|
]);
|
||||||
|
|
||||||
// If destination node is defined and got executed stop execution
|
// If destination node is defined and got executed stop execution
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.runExecutionData.waitTill!) {
|
||||||
|
await this.executeHook('nodeExecuteAfter', [
|
||||||
|
executionNode.name,
|
||||||
|
taskData,
|
||||||
|
this.runExecutionData,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Add the node back to the stack that the workflow can start to execute again from that node
|
||||||
|
this.runExecutionData.executionData!.nodeExecutionStack.unshift(executionData);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Add the nodes to which the current node has an output connection to that they can
|
// Add the nodes to which the current node has an output connection to that they can
|
||||||
// be executed next
|
// be executed next
|
||||||
if (workflow.connectionsBySourceNode.hasOwnProperty(executionNode.name)) {
|
if (workflow.connectionsBySourceNode.hasOwnProperty(executionNode.name)) {
|
||||||
if (workflow.connectionsBySourceNode[executionNode.name].hasOwnProperty('main')) {
|
if (workflow.connectionsBySourceNode[executionNode.name].hasOwnProperty('main')) {
|
||||||
let outputIndex: string, connectionData: IConnection;
|
let outputIndex: string;
|
||||||
|
let connectionData: IConnection;
|
||||||
// Iterate over all the outputs
|
// Iterate over all the outputs
|
||||||
|
|
||||||
// Add the nodes to be executed
|
// Add the nodes to be executed
|
||||||
for (outputIndex in workflow.connectionsBySourceNode[executionNode.name]['main']) {
|
// eslint-disable-next-line @typescript-eslint/no-for-in-array
|
||||||
if (!workflow.connectionsBySourceNode[executionNode.name]['main'].hasOwnProperty(outputIndex)) {
|
for (outputIndex in workflow.connectionsBySourceNode[executionNode.name].main) {
|
||||||
|
if (
|
||||||
|
!workflow.connectionsBySourceNode[executionNode.name].main.hasOwnProperty(
|
||||||
|
outputIndex,
|
||||||
|
)
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterate over all the different connections of this output
|
// Iterate over all the different connections of this output
|
||||||
for (connectionData of workflow.connectionsBySourceNode[executionNode.name]['main'][outputIndex]) {
|
for (connectionData of workflow.connectionsBySourceNode[executionNode.name].main[
|
||||||
|
outputIndex
|
||||||
|
]) {
|
||||||
if (!workflow.nodes.hasOwnProperty(connectionData.node)) {
|
if (!workflow.nodes.hasOwnProperty(connectionData.node)) {
|
||||||
return Promise.reject(new Error(`The node "${executionNode.name}" connects to not found node "${connectionData.node}"`));
|
return Promise.reject(
|
||||||
|
new Error(
|
||||||
|
`The node "${executionNode.name}" connects to not found node "${connectionData.node}"`,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nodeSuccessData![outputIndex] && (nodeSuccessData![outputIndex].length !== 0 || connectionData.index > 0)) {
|
if (
|
||||||
|
nodeSuccessData![outputIndex] &&
|
||||||
|
(nodeSuccessData![outputIndex].length !== 0 || connectionData.index > 0)
|
||||||
|
) {
|
||||||
// Add the node only if it did execute or if connected to second "optional" input
|
// Add the node only if it did execute or if connected to second "optional" input
|
||||||
this.addNodeToBeExecuted(workflow, connectionData, parseInt(outputIndex, 10), executionNode.name, nodeSuccessData!, runIndex);
|
this.addNodeToBeExecuted(
|
||||||
|
workflow,
|
||||||
|
connectionData,
|
||||||
|
parseInt(outputIndex, 10),
|
||||||
|
executionNode.name,
|
||||||
|
nodeSuccessData!,
|
||||||
|
runIndex,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -799,56 +980,80 @@ export class WorkflowExecute {
|
||||||
// Execute hooks now to make sure that all hooks are executed properly
|
// Execute hooks now to make sure that all hooks are executed properly
|
||||||
// Await is needed to make sure that we don't fall into concurrency problems
|
// Await is needed to make sure that we don't fall into concurrency problems
|
||||||
// When saving node execution data
|
// When saving node execution data
|
||||||
await this.executeHook('nodeExecuteAfter', [executionNode.name, taskData, this.runExecutionData]);
|
await this.executeHook('nodeExecuteAfter', [
|
||||||
|
executionNode.name,
|
||||||
|
taskData,
|
||||||
|
this.runExecutionData,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
})()
|
})()
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
if (gotCancel && executionError === undefined) {
|
if (gotCancel && executionError === undefined) {
|
||||||
return this.processSuccessExecution(startedAt, workflow, new WorkflowOperationError('Workflow has been canceled or timed out!'));
|
return this.processSuccessExecution(
|
||||||
}
|
startedAt,
|
||||||
return this.processSuccessExecution(startedAt, workflow, executionError);
|
workflow,
|
||||||
})
|
new WorkflowOperationError('Workflow has been canceled or timed out!'),
|
||||||
.catch(async (error) => {
|
);
|
||||||
const fullRunData = this.getFullRunData(startedAt);
|
}
|
||||||
|
return this.processSuccessExecution(startedAt, workflow, executionError);
|
||||||
|
})
|
||||||
|
.catch(async (error) => {
|
||||||
|
const fullRunData = this.getFullRunData(startedAt);
|
||||||
|
|
||||||
fullRunData.data.resultData.error = {
|
fullRunData.data.resultData.error = {
|
||||||
...error,
|
...error,
|
||||||
message: error.message,
|
message: error.message,
|
||||||
stack: error.stack,
|
stack: error.stack,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if static data changed
|
// Check if static data changed
|
||||||
let newStaticData: IDataObject | undefined;
|
let newStaticData: IDataObject | undefined;
|
||||||
if (workflow.staticData.__dataChanged === true) {
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
// Static data of workflow changed
|
if (workflow.staticData.__dataChanged === true) {
|
||||||
newStaticData = workflow.staticData;
|
// Static data of workflow changed
|
||||||
}
|
newStaticData = workflow.staticData;
|
||||||
await this.executeHook('workflowExecuteAfter', [fullRunData, newStaticData]).catch(error => {
|
}
|
||||||
console.error('There was a problem running hook "workflowExecuteAfter"', error);
|
await this.executeHook('workflowExecuteAfter', [fullRunData, newStaticData]).catch(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
|
(error) => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('There was a problem running hook "workflowExecuteAfter"', error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return fullRunData;
|
||||||
});
|
});
|
||||||
|
|
||||||
return fullRunData;
|
|
||||||
});
|
|
||||||
|
|
||||||
return returnPromise.then(resolve);
|
return returnPromise.then(resolve);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async processSuccessExecution(
|
||||||
// @ts-ignore
|
startedAt: Date,
|
||||||
async processSuccessExecution(startedAt: Date, workflow: Workflow, executionError?: ExecutionError): PCancelable<IRun> {
|
workflow: Workflow,
|
||||||
|
executionError?: ExecutionError,
|
||||||
|
// @ts-ignore
|
||||||
|
): PCancelable<IRun> {
|
||||||
const fullRunData = this.getFullRunData(startedAt);
|
const fullRunData = this.getFullRunData(startedAt);
|
||||||
|
|
||||||
if (executionError !== undefined) {
|
if (executionError !== undefined) {
|
||||||
Logger.verbose(`Workflow execution finished with error`, { error: executionError, workflowId: workflow.id });
|
Logger.verbose(`Workflow execution finished with error`, {
|
||||||
|
error: executionError,
|
||||||
|
workflowId: workflow.id,
|
||||||
|
});
|
||||||
fullRunData.data.resultData.error = {
|
fullRunData.data.resultData.error = {
|
||||||
...executionError,
|
...executionError,
|
||||||
message: executionError.message,
|
message: executionError.message,
|
||||||
stack: executionError.stack,
|
stack: executionError.stack,
|
||||||
} as ExecutionError;
|
} as ExecutionError;
|
||||||
|
} else if (this.runExecutionData.waitTill!) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
|
Logger.verbose(`Workflow execution will wait until ${this.runExecutionData.waitTill}`, {
|
||||||
|
workflowId: workflow.id,
|
||||||
|
});
|
||||||
|
fullRunData.waitTill = this.runExecutionData.waitTill;
|
||||||
} else {
|
} else {
|
||||||
Logger.verbose(`Workflow execution finished successfully`, { workflowId: workflow.id });
|
Logger.verbose(`Workflow execution finished successfully`, { workflowId: workflow.id });
|
||||||
fullRunData.finished = true;
|
fullRunData.finished = true;
|
||||||
|
@ -856,6 +1061,7 @@ export class WorkflowExecute {
|
||||||
|
|
||||||
// Check if static data changed
|
// Check if static data changed
|
||||||
let newStaticData: IDataObject | undefined;
|
let newStaticData: IDataObject | undefined;
|
||||||
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
if (workflow.staticData.__dataChanged === true) {
|
if (workflow.staticData.__dataChanged === true) {
|
||||||
// Static data of workflow changed
|
// Static data of workflow changed
|
||||||
newStaticData = workflow.staticData;
|
newStaticData = workflow.staticData;
|
||||||
|
@ -876,5 +1082,4 @@ export class WorkflowExecute {
|
||||||
|
|
||||||
return fullRunData;
|
return fullRunData;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
try {
|
/* eslint-disable import/no-cycle */
|
||||||
require('source-map-support').install();
|
import * as NodeExecuteFunctions from './NodeExecuteFunctions';
|
||||||
} catch (error) {
|
import * as UserSettings from './UserSettings';
|
||||||
|
|
||||||
}
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, import/no-extraneous-dependencies, global-require, @typescript-eslint/no-var-requires
|
||||||
|
require('source-map-support').install();
|
||||||
|
// eslint-disable-next-line no-empty
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
export * from './ActiveWorkflows';
|
export * from './ActiveWorkflows';
|
||||||
export * from './ActiveWebhooks';
|
export * from './ActiveWebhooks';
|
||||||
|
@ -13,10 +17,4 @@ export * from './Interfaces';
|
||||||
export * from './LoadNodeParameterOptions';
|
export * from './LoadNodeParameterOptions';
|
||||||
export * from './NodeExecuteFunctions';
|
export * from './NodeExecuteFunctions';
|
||||||
export * from './WorkflowExecute';
|
export * from './WorkflowExecute';
|
||||||
|
export { NodeExecuteFunctions, UserSettings };
|
||||||
import * as NodeExecuteFunctions from './NodeExecuteFunctions';
|
|
||||||
import * as UserSettings from './UserSettings';
|
|
||||||
export {
|
|
||||||
NodeExecuteFunctions,
|
|
||||||
UserSettings,
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,88 +1,83 @@
|
||||||
|
|
||||||
import { Credentials } from '../src';
|
import { Credentials } from '../src';
|
||||||
|
|
||||||
describe('Credentials', () => {
|
describe('Credentials', () => {
|
||||||
|
describe('without nodeType set', () => {
|
||||||
|
test('should be able to set and read key data without initial data set', () => {
|
||||||
|
const credentials = new Credentials('testName', 'testType', []);
|
||||||
|
|
||||||
describe('without nodeType set', () => {
|
const key = 'key1';
|
||||||
|
const password = 'password';
|
||||||
|
// const nodeType = 'base.noOp';
|
||||||
|
const newData = 1234;
|
||||||
|
|
||||||
test('should be able to set and read key data without initial data set', () => {
|
credentials.setDataKey(key, newData, password);
|
||||||
|
|
||||||
const credentials = new Credentials('testName', 'testType', []);
|
|
||||||
|
|
||||||
const key = 'key1';
|
|
||||||
const password = 'password';
|
|
||||||
// const nodeType = 'base.noOp';
|
|
||||||
const newData = 1234;
|
|
||||||
|
|
||||||
credentials.setDataKey(key, newData, password);
|
|
||||||
|
|
||||||
expect(credentials.getDataKey(key, password)).toEqual(newData);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should be able to set and read key data with initial data set', () => {
|
|
||||||
|
|
||||||
const key = 'key2';
|
|
||||||
const password = 'password';
|
|
||||||
|
|
||||||
// Saved under "key1"
|
|
||||||
const initialData = 4321;
|
|
||||||
const initialDataEncoded = 'U2FsdGVkX1+0baznXt+Ag/ub8A2kHLyoLxn/rR9h4XQ=';
|
|
||||||
|
|
||||||
const credentials = new Credentials('testName', 'testType', [], initialDataEncoded);
|
|
||||||
|
|
||||||
const newData = 1234;
|
|
||||||
|
|
||||||
// Set and read new data
|
|
||||||
credentials.setDataKey(key, newData, password);
|
|
||||||
expect(credentials.getDataKey(key, password)).toEqual(newData);
|
|
||||||
|
|
||||||
// Read the data which got provided encrypted on init
|
|
||||||
expect(credentials.getDataKey('key1', password)).toEqual(initialData);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
expect(credentials.getDataKey(key, password)).toEqual(newData);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with nodeType set', () => {
|
test('should be able to set and read key data with initial data set', () => {
|
||||||
|
const key = 'key2';
|
||||||
|
const password = 'password';
|
||||||
|
|
||||||
test('should be able to set and read key data without initial data set', () => {
|
// Saved under "key1"
|
||||||
|
const initialData = 4321;
|
||||||
|
const initialDataEncoded = 'U2FsdGVkX1+0baznXt+Ag/ub8A2kHLyoLxn/rR9h4XQ=';
|
||||||
|
|
||||||
const nodeAccess = [
|
const credentials = new Credentials('testName', 'testType', [], initialDataEncoded);
|
||||||
{
|
|
||||||
nodeType: 'base.noOp',
|
|
||||||
user: 'userName',
|
|
||||||
date: new Date(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const credentials = new Credentials('testName', 'testType', nodeAccess);
|
const newData = 1234;
|
||||||
|
|
||||||
const key = 'key1';
|
// Set and read new data
|
||||||
const password = 'password';
|
credentials.setDataKey(key, newData, password);
|
||||||
const nodeType = 'base.noOp';
|
expect(credentials.getDataKey(key, password)).toEqual(newData);
|
||||||
const newData = 1234;
|
|
||||||
|
|
||||||
credentials.setDataKey(key, newData, password);
|
// Read the data which got provided encrypted on init
|
||||||
|
expect(credentials.getDataKey('key1', password)).toEqual(initialData);
|
||||||
// Should be able to read with nodeType which has access
|
|
||||||
expect(credentials.getDataKey(key, password, nodeType)).toEqual(newData);
|
|
||||||
|
|
||||||
// Should not be able to read with nodeType which does NOT have access
|
|
||||||
// expect(credentials.getDataKey(key, password, 'base.otherNode')).toThrowError(Error);
|
|
||||||
try {
|
|
||||||
credentials.getDataKey(key, password, 'base.otherNode');
|
|
||||||
expect(true).toBe(false);
|
|
||||||
} catch (e) {
|
|
||||||
expect(e.message).toBe('The node of type "base.otherNode" does not have access to credentials "testName" of type "testType".');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the data which will be saved in database
|
|
||||||
const dbData = credentials.getDataToSave();
|
|
||||||
expect(dbData.name).toEqual('testName');
|
|
||||||
expect(dbData.type).toEqual('testType');
|
|
||||||
expect(dbData.nodesAccess).toEqual(nodeAccess);
|
|
||||||
// Compare only the first 6 characters as the rest seems to change with each execution
|
|
||||||
expect(dbData.data!.slice(0, 6)).toEqual('U2FsdGVkX1+wpQWkj+YTzaPSNTFATjnlmFKIsUTZdhk='.slice(0, 6));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with nodeType set', () => {
|
||||||
|
test('should be able to set and read key data without initial data set', () => {
|
||||||
|
const nodeAccess = [
|
||||||
|
{
|
||||||
|
nodeType: 'base.noOp',
|
||||||
|
user: 'userName',
|
||||||
|
date: new Date(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const credentials = new Credentials('testName', 'testType', nodeAccess);
|
||||||
|
|
||||||
|
const key = 'key1';
|
||||||
|
const password = 'password';
|
||||||
|
const nodeType = 'base.noOp';
|
||||||
|
const newData = 1234;
|
||||||
|
|
||||||
|
credentials.setDataKey(key, newData, password);
|
||||||
|
|
||||||
|
// Should be able to read with nodeType which has access
|
||||||
|
expect(credentials.getDataKey(key, password, nodeType)).toEqual(newData);
|
||||||
|
|
||||||
|
// Should not be able to read with nodeType which does NOT have access
|
||||||
|
// expect(credentials.getDataKey(key, password, 'base.otherNode')).toThrowError(Error);
|
||||||
|
try {
|
||||||
|
credentials.getDataKey(key, password, 'base.otherNode');
|
||||||
|
expect(true).toBe(false);
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.message).toBe(
|
||||||
|
'The node of type "base.otherNode" does not have access to credentials "testName" of type "testType".',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the data which will be saved in database
|
||||||
|
const dbData = credentials.getDataToSave();
|
||||||
|
expect(dbData.name).toEqual('testName');
|
||||||
|
expect(dbData.type).toEqual('testType');
|
||||||
|
expect(dbData.nodesAccess).toEqual(nodeAccess);
|
||||||
|
// Compare only the first 6 characters as the rest seems to change with each execution
|
||||||
|
expect(dbData.data!.slice(0, 6)).toEqual(
|
||||||
|
'U2FsdGVkX1+wpQWkj+YTzaPSNTFATjnlmFKIsUTZdhk='.slice(0, 6),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -18,28 +18,27 @@ import {
|
||||||
WorkflowHooks,
|
WorkflowHooks,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import {
|
import { Credentials, IDeferredPromise, IExecuteFunctions } from '../src';
|
||||||
Credentials,
|
|
||||||
IDeferredPromise,
|
|
||||||
IExecuteFunctions,
|
|
||||||
} from '../src';
|
|
||||||
|
|
||||||
|
|
||||||
export class CredentialsHelper extends ICredentialsHelper {
|
export class CredentialsHelper extends ICredentialsHelper {
|
||||||
getDecrypted(name: string, type: string): ICredentialDataDecryptedObject {
|
getDecrypted(name: string, type: string): Promise<ICredentialDataDecryptedObject> {
|
||||||
return {};
|
return new Promise((res) => res({}));
|
||||||
}
|
}
|
||||||
|
|
||||||
getCredentials(name: string, type: string): Credentials {
|
getCredentials(name: string, type: string): Promise<Credentials> {
|
||||||
return new Credentials('', '', [], '');
|
return new Promise((res) => {
|
||||||
|
res(new Credentials('', '', [], ''));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateCredentials(name: string, type: string, data: ICredentialDataDecryptedObject): Promise<void> {}
|
async updateCredentials(
|
||||||
|
name: string,
|
||||||
|
type: string,
|
||||||
|
data: ICredentialDataDecryptedObject,
|
||||||
|
): Promise<void> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class NodeTypesClass implements INodeTypes {
|
class NodeTypesClass implements INodeTypes {
|
||||||
|
|
||||||
nodeTypes: INodeTypeData = {
|
nodeTypes: INodeTypeData = {
|
||||||
'n8n-nodes-base.if': {
|
'n8n-nodes-base.if': {
|
||||||
sourcePath: '',
|
sourcePath: '',
|
||||||
|
@ -159,9 +158,7 @@ class NodeTypesClass implements INodeTypes {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
hide: {
|
hide: {
|
||||||
operation: [
|
operation: ['isEmpty'],
|
||||||
'isEmpty',
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
default: 0,
|
default: 0,
|
||||||
|
@ -227,10 +224,7 @@ class NodeTypesClass implements INodeTypes {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
hide: {
|
hide: {
|
||||||
operation: [
|
operation: ['isEmpty', 'regex'],
|
||||||
'isEmpty',
|
|
||||||
'regex',
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
default: '',
|
default: '',
|
||||||
|
@ -242,9 +236,7 @@ class NodeTypesClass implements INodeTypes {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
show: {
|
show: {
|
||||||
operation: [
|
operation: ['regex'],
|
||||||
'regex',
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
default: '',
|
default: '',
|
||||||
|
@ -272,7 +264,8 @@ class NodeTypesClass implements INodeTypes {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
default: 'all',
|
default: 'all',
|
||||||
description: 'If multiple rules got set this settings decides if it is true as soon as ANY condition matches or only if ALL get meet.',
|
description:
|
||||||
|
'If multiple rules got set this settings decides if it is true as soon as ANY condition matches or only if ALL get meet.',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -289,19 +282,30 @@ class NodeTypesClass implements INodeTypes {
|
||||||
const compareOperationFunctions: {
|
const compareOperationFunctions: {
|
||||||
[key: string]: (value1: NodeParameterValue, value2: NodeParameterValue) => boolean;
|
[key: string]: (value1: NodeParameterValue, value2: NodeParameterValue) => boolean;
|
||||||
} = {
|
} = {
|
||||||
contains: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || '').toString().includes((value2 || '').toString()),
|
contains: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
notContains: (value1: NodeParameterValue, value2: NodeParameterValue) => !(value1 || '').toString().includes((value2 || '').toString()),
|
(value1 || '').toString().includes((value2 || '').toString()),
|
||||||
endsWith: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 as string).endsWith(value2 as string),
|
notContains: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
|
!(value1 || '').toString().includes((value2 || '').toString()),
|
||||||
|
endsWith: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
|
(value1 as string).endsWith(value2 as string),
|
||||||
equal: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 === value2,
|
equal: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 === value2,
|
||||||
notEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 !== value2,
|
notEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 !== value2,
|
||||||
larger: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) > (value2 || 0),
|
larger: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
largerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) >= (value2 || 0),
|
(value1 || 0) > (value2 || 0),
|
||||||
smaller: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) < (value2 || 0),
|
largerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
smallerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) <= (value2 || 0),
|
(value1 || 0) >= (value2 || 0),
|
||||||
startsWith: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 as string).startsWith(value2 as string),
|
smaller: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
isEmpty: (value1: NodeParameterValue) => [undefined, null, ''].includes(value1 as string),
|
(value1 || 0) < (value2 || 0),
|
||||||
|
smallerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
|
(value1 || 0) <= (value2 || 0),
|
||||||
|
startsWith: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
|
(value1 as string).startsWith(value2 as string),
|
||||||
|
isEmpty: (value1: NodeParameterValue) =>
|
||||||
|
[undefined, null, ''].includes(value1 as string),
|
||||||
regex: (value1: NodeParameterValue, value2: NodeParameterValue) => {
|
regex: (value1: NodeParameterValue, value2: NodeParameterValue) => {
|
||||||
const regexMatch = (value2 || '').toString().match(new RegExp('^/(.*?)/([gimusy]*)$'));
|
const regexMatch = (value2 || '')
|
||||||
|
.toString()
|
||||||
|
.match(new RegExp('^/(.*?)/([gimusy]*)$'));
|
||||||
|
|
||||||
let regex: RegExp;
|
let regex: RegExp;
|
||||||
if (!regexMatch) {
|
if (!regexMatch) {
|
||||||
|
@ -317,18 +321,13 @@ class NodeTypesClass implements INodeTypes {
|
||||||
};
|
};
|
||||||
|
|
||||||
// The different dataTypes to check the values in
|
// The different dataTypes to check the values in
|
||||||
const dataTypes = [
|
const dataTypes = ['boolean', 'number', 'string'];
|
||||||
'boolean',
|
|
||||||
'number',
|
|
||||||
'string',
|
|
||||||
];
|
|
||||||
|
|
||||||
// Itterate over all items to check which ones should be output as via output "true" and
|
// Itterate over all items to check which ones should be output as via output "true" and
|
||||||
// which ones via output "false"
|
// which ones via output "false"
|
||||||
let dataType: string;
|
let dataType: string;
|
||||||
let compareOperationResult: boolean;
|
let compareOperationResult: boolean;
|
||||||
itemLoop:
|
itemLoop: for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
|
||||||
item = items[itemIndex];
|
item = items[itemIndex];
|
||||||
|
|
||||||
let compareData: INodeParameters;
|
let compareData: INodeParameters;
|
||||||
|
@ -338,9 +337,16 @@ class NodeTypesClass implements INodeTypes {
|
||||||
// Check all the values of the different dataTypes
|
// Check all the values of the different dataTypes
|
||||||
for (dataType of dataTypes) {
|
for (dataType of dataTypes) {
|
||||||
// Check all the values of the current dataType
|
// Check all the values of the current dataType
|
||||||
for (compareData of this.getNodeParameter(`conditions.${dataType}`, itemIndex, []) as INodeParameters[]) {
|
for (compareData of this.getNodeParameter(
|
||||||
|
`conditions.${dataType}`,
|
||||||
|
itemIndex,
|
||||||
|
[],
|
||||||
|
) as INodeParameters[]) {
|
||||||
// Check if the values passes
|
// Check if the values passes
|
||||||
compareOperationResult = compareOperationFunctions[compareData.operation as string](compareData.value1 as NodeParameterValue, compareData.value2 as NodeParameterValue);
|
compareOperationResult = compareOperationFunctions[compareData.operation as string](
|
||||||
|
compareData.value1 as NodeParameterValue,
|
||||||
|
compareData.value2 as NodeParameterValue,
|
||||||
|
);
|
||||||
|
|
||||||
if (compareOperationResult === true && combineOperation === 'any') {
|
if (compareOperationResult === true && combineOperation === 'any') {
|
||||||
// If it passes and the operation is "any" we do not have to check any
|
// If it passes and the operation is "any" we do not have to check any
|
||||||
|
@ -395,21 +401,25 @@ class NodeTypesClass implements INodeTypes {
|
||||||
{
|
{
|
||||||
name: 'Append',
|
name: 'Append',
|
||||||
value: 'append',
|
value: 'append',
|
||||||
description: 'Combines data of both inputs. The output will contain items of input 1 and input 2.',
|
description:
|
||||||
|
'Combines data of both inputs. The output will contain items of input 1 and input 2.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Pass-through',
|
name: 'Pass-through',
|
||||||
value: 'passThrough',
|
value: 'passThrough',
|
||||||
description: 'Passes through data of one input. The output will conain only items of the defined input.',
|
description:
|
||||||
|
'Passes through data of one input. The output will conain only items of the defined input.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Wait',
|
name: 'Wait',
|
||||||
value: 'wait',
|
value: 'wait',
|
||||||
description: 'Waits till data of both inputs is available and will then output a single empty item.',
|
description:
|
||||||
|
'Waits till data of both inputs is available and will then output a single empty item.',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
default: 'append',
|
default: 'append',
|
||||||
description: 'How data should be merged. If it should simply<br />be appended or merged depending on a property.',
|
description:
|
||||||
|
'How data should be merged. If it should simply<br />be appended or merged depending on a property.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Output Data',
|
displayName: 'Output Data',
|
||||||
|
@ -417,9 +427,7 @@ class NodeTypesClass implements INodeTypes {
|
||||||
type: 'options',
|
type: 'options',
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
show: {
|
show: {
|
||||||
mode: [
|
mode: ['passThrough'],
|
||||||
'passThrough',
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
options: [
|
options: [
|
||||||
|
@ -510,7 +518,8 @@ class NodeTypesClass implements INodeTypes {
|
||||||
name: 'keepOnlySet',
|
name: 'keepOnlySet',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
description: 'If only the values set on this node should be<br />kept and all others removed.',
|
description:
|
||||||
|
'If only the values set on this node should be<br />kept and all others removed.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Values to Set',
|
displayName: 'Values to Set',
|
||||||
|
@ -532,7 +541,8 @@ class NodeTypesClass implements INodeTypes {
|
||||||
name: 'name',
|
name: 'name',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: 'propertyName',
|
default: 'propertyName',
|
||||||
description: 'Name of the property to write data to.<br />Supports dot-notation.<br />Example: "data.person[0].name"',
|
description:
|
||||||
|
'Name of the property to write data to.<br />Supports dot-notation.<br />Example: "data.person[0].name"',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Value',
|
displayName: 'Value',
|
||||||
|
@ -552,7 +562,8 @@ class NodeTypesClass implements INodeTypes {
|
||||||
name: 'name',
|
name: 'name',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: 'propertyName',
|
default: 'propertyName',
|
||||||
description: 'Name of the property to write data to.<br />Supports dot-notation.<br />Example: "data.person[0].name"',
|
description:
|
||||||
|
'Name of the property to write data to.<br />Supports dot-notation.<br />Example: "data.person[0].name"',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Value',
|
displayName: 'Value',
|
||||||
|
@ -572,7 +583,8 @@ class NodeTypesClass implements INodeTypes {
|
||||||
name: 'name',
|
name: 'name',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: 'propertyName',
|
default: 'propertyName',
|
||||||
description: 'Name of the property to write data to.<br />Supports dot-notation.<br />Example: "data.person[0].name"',
|
description:
|
||||||
|
'Name of the property to write data to.<br />Supports dot-notation.<br />Example: "data.person[0].name"',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Value',
|
displayName: 'Value',
|
||||||
|
@ -608,7 +620,6 @@ class NodeTypesClass implements INodeTypes {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
|
||||||
const items = this.getInputData();
|
const items = this.getInputData();
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
|
@ -641,31 +652,37 @@ class NodeTypesClass implements INodeTypes {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add boolean values
|
// Add boolean values
|
||||||
(this.getNodeParameter('values.boolean', itemIndex, []) as INodeParameters[]).forEach((setItem) => {
|
(this.getNodeParameter('values.boolean', itemIndex, []) as INodeParameters[]).forEach(
|
||||||
if (options.dotNotation === false) {
|
(setItem) => {
|
||||||
newItem.json[setItem.name as string] = !!setItem.value;
|
if (options.dotNotation === false) {
|
||||||
} else {
|
newItem.json[setItem.name as string] = !!setItem.value;
|
||||||
set(newItem.json, setItem.name as string, !!setItem.value);
|
} else {
|
||||||
}
|
set(newItem.json, setItem.name as string, !!setItem.value);
|
||||||
});
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Add number values
|
// Add number values
|
||||||
(this.getNodeParameter('values.number', itemIndex, []) as INodeParameters[]).forEach((setItem) => {
|
(this.getNodeParameter('values.number', itemIndex, []) as INodeParameters[]).forEach(
|
||||||
if (options.dotNotation === false) {
|
(setItem) => {
|
||||||
newItem.json[setItem.name as string] = setItem.value;
|
if (options.dotNotation === false) {
|
||||||
} else {
|
newItem.json[setItem.name as string] = setItem.value;
|
||||||
set(newItem.json, setItem.name as string, setItem.value);
|
} else {
|
||||||
}
|
set(newItem.json, setItem.name as string, setItem.value);
|
||||||
});
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Add string values
|
// Add string values
|
||||||
(this.getNodeParameter('values.string', itemIndex, []) as INodeParameters[]).forEach((setItem) => {
|
(this.getNodeParameter('values.string', itemIndex, []) as INodeParameters[]).forEach(
|
||||||
if (options.dotNotation === false) {
|
(setItem) => {
|
||||||
newItem.json[setItem.name as string] = setItem.value;
|
if (options.dotNotation === false) {
|
||||||
} else {
|
newItem.json[setItem.name as string] = setItem.value;
|
||||||
set(newItem.json, setItem.name as string, setItem.value);
|
} else {
|
||||||
}
|
set(newItem.json, setItem.name as string, setItem.value);
|
||||||
});
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
returnData.push(newItem);
|
returnData.push(newItem);
|
||||||
}
|
}
|
||||||
|
@ -700,7 +717,7 @@ class NodeTypesClass implements INodeTypes {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
async init(nodeTypes: INodeTypeData): Promise<void> { }
|
async init(nodeTypes: INodeTypeData): Promise<void> {}
|
||||||
|
|
||||||
getAll(): INodeType[] {
|
getAll(): INodeType[] {
|
||||||
return Object.values(this.nodeTypes).map((data) => data.type);
|
return Object.values(this.nodeTypes).map((data) => data.type);
|
||||||
|
@ -713,7 +730,6 @@ class NodeTypesClass implements INodeTypes {
|
||||||
|
|
||||||
let nodeTypesInstance: NodeTypesClass | undefined;
|
let nodeTypesInstance: NodeTypesClass | undefined;
|
||||||
|
|
||||||
|
|
||||||
export function NodeTypes(): NodeTypesClass {
|
export function NodeTypes(): NodeTypesClass {
|
||||||
if (nodeTypesInstance === undefined) {
|
if (nodeTypesInstance === undefined) {
|
||||||
nodeTypesInstance = new NodeTypesClass();
|
nodeTypesInstance = new NodeTypesClass();
|
||||||
|
@ -723,8 +739,10 @@ export function NodeTypes(): NodeTypesClass {
|
||||||
return nodeTypesInstance;
|
return nodeTypesInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function WorkflowExecuteAdditionalData(
|
||||||
export function WorkflowExecuteAdditionalData(waitPromise: IDeferredPromise<IRun>, nodeExecutionOrder: string[]): IWorkflowExecuteAdditionalData {
|
waitPromise: IDeferredPromise<IRun>,
|
||||||
|
nodeExecutionOrder: string[],
|
||||||
|
): IWorkflowExecuteAdditionalData {
|
||||||
const hookFunctions = {
|
const hookFunctions = {
|
||||||
nodeExecuteAfter: [
|
nodeExecuteAfter: [
|
||||||
async (nodeName: string, data: ITaskData): Promise<void> => {
|
async (nodeName: string, data: ITaskData): Promise<void> => {
|
||||||
|
@ -748,15 +766,15 @@ export function WorkflowExecuteAdditionalData(waitPromise: IDeferredPromise<IRun
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
credentials: {},
|
credentialsHelper: new CredentialsHelper(''),
|
||||||
credentialsHelper: new CredentialsHelper({}, ''),
|
|
||||||
hooks: new WorkflowHooks(hookFunctions, 'trigger', '1', workflowData),
|
hooks: new WorkflowHooks(hookFunctions, 'trigger', '1', workflowData),
|
||||||
executeWorkflow: async (workflowInfo: IExecuteWorkflowInfo): Promise<any> => {}, // tslint:disable-line:no-any
|
executeWorkflow: async (workflowInfo: IExecuteWorkflowInfo): Promise<any> => {},
|
||||||
sendMessageToUI: (message: string) => {},
|
sendMessageToUI: (message: string) => {},
|
||||||
restApiUrl: '',
|
restApiUrl: '',
|
||||||
encryptionKey: 'test',
|
encryptionKey: 'test',
|
||||||
timezone: 'America/New_York',
|
timezone: 'America/New_York',
|
||||||
webhookBaseUrl: 'webhook',
|
webhookBaseUrl: 'webhook',
|
||||||
|
webhookWaitingBaseUrl: 'webhook-waiting',
|
||||||
webhookTestBaseUrl: 'webhook-test',
|
webhookTestBaseUrl: 'webhook-test',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
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