mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-10 06:34:05 -08:00
Merge branch 'master' of github.com:n8n-io/n8n into node-374-openai-node-change-max_tokens-default
This commit is contained in:
commit
024867a774
5
.github/scripts/bump-versions.mjs
vendored
5
.github/scripts/bump-versions.mjs
vendored
|
@ -28,7 +28,10 @@ for (let { name, path, version, private: isPrivate, dependencies } of packages)
|
||||||
packageMap[name] = { path, isDirty, version };
|
packageMap[name] = { path, isDirty, version };
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.ok(packageMap['n8n'].isDirty, 'No changes found since the last release');
|
assert.ok(
|
||||||
|
Object.values(packageMap).some(({ isDirty }) => isDirty),
|
||||||
|
'No changes found since the last release',
|
||||||
|
);
|
||||||
|
|
||||||
// Keep the monorepo version up to date with the released version
|
// Keep the monorepo version up to date with the released version
|
||||||
packageMap['monorepo-root'].version = packageMap['n8n'].version;
|
packageMap['monorepo-root'].version = packageMap['n8n'].version;
|
||||||
|
|
5
.github/workflows/ci-master.yml
vendored
5
.github/workflows/ci-master.yml
vendored
|
@ -35,6 +35,11 @@ jobs:
|
||||||
- name: Test
|
- name: Test
|
||||||
run: pnpm test
|
run: pnpm test
|
||||||
|
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v3
|
||||||
|
with:
|
||||||
|
files: packages/cli/coverage/cobertura-coverage.xml,packages/core/coverage/cobertura-coverage.xml,packages/design-system/coverage/cobertura-coverage.xml,packages/editor-ui/coverage/cobertura-coverage.xml,packages/nodes-base/coverage/cobertura-coverage.xml,packages/workflow/coverage/cobertura-coverage.xml
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
env:
|
env:
|
||||||
CI_LINT_MASTER: true
|
CI_LINT_MASTER: true
|
||||||
|
|
19
.github/workflows/ci-pull-requests.yml
vendored
19
.github/workflows/ci-pull-requests.yml
vendored
|
@ -67,6 +67,11 @@ jobs:
|
||||||
- name: Test
|
- name: Test
|
||||||
run: pnpm test
|
run: pnpm test
|
||||||
|
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v3
|
||||||
|
with:
|
||||||
|
files: packages/cli/coverage/cobertura-coverage.xml,packages/core/coverage/cobertura-coverage.xml,packages/design-system/coverage/cobertura-coverage.xml,packages/editor-ui/coverage/cobertura-coverage.xml,packages/nodes-base/coverage/cobertura-coverage.xml,packages/workflow/coverage/cobertura-coverage.xml
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
name: Lint changes
|
name: Lint changes
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -108,7 +113,7 @@ jobs:
|
||||||
uses: ./.github/workflows/e2e-reusable.yml
|
uses: ./.github/workflows/e2e-reusable.yml
|
||||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-e2e') }}
|
if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-e2e') }}
|
||||||
with:
|
with:
|
||||||
branch: ${{ github.event.pull_request.base.ref }}
|
branch: ${{ github.event.pull_request.head.ref }}
|
||||||
user: ${{ github.event.inputs.user || 'PR User' }}
|
user: ${{ github.event.inputs.user || 'PR User' }}
|
||||||
spec: ${{ github.event.inputs.spec || 'e2e/0-smoke.cy.ts' }}
|
spec: ${{ github.event.inputs.spec || 'e2e/0-smoke.cy.ts' }}
|
||||||
run-env: base:16.18.1
|
run-env: base:16.18.1
|
||||||
|
@ -117,3 +122,15 @@ jobs:
|
||||||
containers: '[1]'
|
containers: '[1]'
|
||||||
secrets:
|
secrets:
|
||||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||||
|
|
||||||
|
checklist_job:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Checklist job
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v1
|
||||||
|
- name: Checklist
|
||||||
|
uses: wyozi/contextual-qa-checklist-action@master
|
||||||
|
with:
|
||||||
|
gh-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
comment-footer: Make sure to check off this list before asking for review.
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -3,6 +3,7 @@ node_modules
|
||||||
.tmp
|
.tmp
|
||||||
tmp
|
tmp
|
||||||
dist
|
dist
|
||||||
|
coverage
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn.lock
|
yarn.lock
|
||||||
google-generated-credentials.json
|
google-generated-credentials.json
|
||||||
|
|
148
CHANGELOG.md
148
CHANGELOG.md
|
@ -1,3 +1,151 @@
|
||||||
|
## [0.221.2](https://github.com/n8n-io/n8n/compare/n8n@0.221.1...n8n@0.221.2) (2023-03-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **core:** Assign properties.success earlier to set executionStatus correctly ([6c7772a](https://github.com/n8n-io/n8n/commit/6c7772a0b3276ffe9e5ab8029e17a0ed743ee5a7))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.221.1](https://github.com/n8n-io/n8n/compare/n8n@0.221.0...n8n@0.221.1) (2023-03-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **core:** Initialize queue in the webhook server as well ([163859b](https://github.com/n8n-io/n8n/commit/163859b87a19f3795e2689ce8eba2e2ce6766684))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.221.0](https://github.com/n8n-io/n8n/compare/n8n@0.220.0...n8n@0.221.0) (2023-03-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **core:** Fix calling error workflows in main mode recovery ([#5698](https://github.com/n8n-io/n8n/issues/5698)) ([e0ea97a](https://github.com/n8n-io/n8n/commit/e0ea97af8d7aaa014680f5f9d5702d1cafd49757))
|
||||||
|
* **core:** Fix telemetry execution status for manual workflows executions ([#5712](https://github.com/n8n-io/n8n/issues/5712)) ([a28396e](https://github.com/n8n-io/n8n/commit/a28396ee91bfbccc6596812606c237a8e2c34088))
|
||||||
|
* **core:** Return saml attributes after connection test ([#5717](https://github.com/n8n-io/n8n/issues/5717)) ([be172cb](https://github.com/n8n-io/n8n/commit/be172cb720c8a44ebd1f0b86dddab321e1e3c9fd))
|
||||||
|
* **editor:** Disable tooltip for display modes that don't support mapping ([#5715](https://github.com/n8n-io/n8n/issues/5715)) ([fb8755e](https://github.com/n8n-io/n8n/commit/fb8755ea3c720c98f002a6756c39b8fed11482c0))
|
||||||
|
* **editor:** Fix execution list item selection ([#5606](https://github.com/n8n-io/n8n/issues/5606)) ([7a352ef](https://github.com/n8n-io/n8n/commit/7a352efff944c52062412e53ea2c1a034a25f908))
|
||||||
|
* **editor:** Fix for large notifications being cut off ([#5705](https://github.com/n8n-io/n8n/issues/5705)) ([c07f838](https://github.com/n8n-io/n8n/commit/c07f838ce60dc33261fe3e1b5e7dd6fe05f1d63b))
|
||||||
|
* **editor:** Fix redo in code and expression editor ([#5708](https://github.com/n8n-io/n8n/issues/5708)) ([cd7a55b](https://github.com/n8n-io/n8n/commit/cd7a55ba5aeb83d1e540a65b5c6b2c74fd742461))
|
||||||
|
* **editor:** Fix the canvas node distance when automatically injecting manual trigger ([#5716](https://github.com/n8n-io/n8n/issues/5716)) ([cb2ba97](https://github.com/n8n-io/n8n/commit/cb2ba97f3837b572e237da1256b9f2ee376767a9))
|
||||||
|
* **HTTP Request Node:** Fix AWS credentials to automatically deconstruct the url ([#5751](https://github.com/n8n-io/n8n/issues/5751)) ([4ac944a](https://github.com/n8n-io/n8n/commit/4ac944af3028b70ae600000300c16de77c1af1d5))
|
||||||
|
* **Split In Batches Node:** Roll back changes in v1 and create v2 ([#5747](https://github.com/n8n-io/n8n/issues/5747)) ([cefec77](https://github.com/n8n-io/n8n/commit/cefec7739b6da820d64f9476476e1901d4f386bf))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **core:** Augment data instead of copying it ([#5487](https://github.com/n8n-io/n8n/issues/5487)) ([0876c38](https://github.com/n8n-io/n8n/commit/0876c38aaeb8355141fecbc14e84cdda0b2c737b))
|
||||||
|
* **editor:** Move canvas by holding Space or Middle mouse button ([#5719](https://github.com/n8n-io/n8n/issues/5719)) ([19dded1](https://github.com/n8n-io/n8n/commit/19dded18c9a588a30b9ac1fc274dcd967e9b7b6b))
|
||||||
|
* **editor:** Recommend and pre-select auth type with overrides ([#5684](https://github.com/n8n-io/n8n/issues/5684)) ([f59b591](https://github.com/n8n-io/n8n/commit/f59b591c93ecd7cbd279668abe6494ef2b88c831))
|
||||||
|
* **editor:** SSO login button ([#5615](https://github.com/n8n-io/n8n/issues/5615)) ([6916628](https://github.com/n8n-io/n8n/commit/6916628a9f11e07cbcdf390f747f396fb0ef9e3c))
|
||||||
|
* **QuickChart Node:** Add QuickChart node ([#3572](https://github.com/n8n-io/n8n/issues/3572)) ([233f1fa](https://github.com/n8n-io/n8n/commit/233f1fa7ec230e92e868de0247e315aa6a705ead))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.220.1](https://github.com/n8n-io/n8n/compare/n8n@0.220.0...n8n@0.220.1) (2023-03-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Split In Batches Node:** Roll back changes in v1 and create v2 ([#5747](https://github.com/n8n-io/n8n/issues/5747)) ([6d1c88e](https://github.com/n8n-io/n8n/commit/6d1c88ea8c2e5dc72c6e6edeeeef52dc1fba4075))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.220.0](https://github.com/n8n-io/n8n/compare/n8n@0.219.1...n8n@0.220.0) (2023-03-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **core:** Initialize License and LDAP in the correct order ([#5673](https://github.com/n8n-io/n8n/issues/5673)) ([90afa5e](https://github.com/n8n-io/n8n/commit/90afa5e55f96fbe46417f4be8f764795fb5c2225))
|
||||||
|
* **editor:** Display correct error message for env access ([#5634](https://github.com/n8n-io/n8n/issues/5634)) ([5f238ea](https://github.com/n8n-io/n8n/commit/5f238ea6413d25704a5865d339401117e81dbbab))
|
||||||
|
* **editor:** Fix autocomplete for complex expresions ([#5695](https://github.com/n8n-io/n8n/issues/5695)) ([11bf260](https://github.com/n8n-io/n8n/commit/11bf260bf164c6c9dffe71b875fde139c93f905d))
|
||||||
|
* **editor:** Fix owner set-up checkbox wording ([#5697](https://github.com/n8n-io/n8n/issues/5697)) ([58232be](https://github.com/n8n-io/n8n/commit/58232bec618edd403f18527913c489bfa11f570b))
|
||||||
|
* **editor:** Properly handle mapping of dragged expression if it contains hyphen ([#5703](https://github.com/n8n-io/n8n/issues/5703)) ([7025efe](https://github.com/n8n-io/n8n/commit/7025efe8654a8a55ff10e2105ddc6ce2dc5a89d6))
|
||||||
|
* **Metabase Node:** Fix issue with question results not correctly being returned ([#5665](https://github.com/n8n-io/n8n/issues/5665)) ([d1e3c19](https://github.com/n8n-io/n8n/commit/d1e3c192ba9e2dfd852e570e88f6135d42d2ed45))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **core:** Improve SAML connection test ([#5680](https://github.com/n8n-io/n8n/issues/5680)) ([ef07528](https://github.com/n8n-io/n8n/commit/ef07528cc21f06ee52f93bafb34ac54a244609f9))
|
||||||
|
* **editor:** Add basic Datatable and Pagination components ([#5652](https://github.com/n8n-io/n8n/issues/5652)) ([29f2629](https://github.com/n8n-io/n8n/commit/29f2629716e3693372ec9a4572113a3f3721ff5e))
|
||||||
|
* **editor:** Add support for schema view in the NDV output ([#5688](https://github.com/n8n-io/n8n/issues/5688)) ([541850f](https://github.com/n8n-io/n8n/commit/541850f95f1c42fc16d9aeee3a3fef68a4b77082))
|
||||||
|
* **editor:** Do not show actions panel for single-action nodes ([#5683](https://github.com/n8n-io/n8n/issues/5683)) ([de1db92](https://github.com/n8n-io/n8n/commit/de1db927cbdc5fc8ef7d697cccbd8603f66391ea))
|
||||||
|
* **Item Lists Node:** Update actions ([#5648](https://github.com/n8n-io/n8n/issues/5648)) ([332d50c](https://github.com/n8n-io/n8n/commit/332d50c5f1f8ba63b87325799360adecdbaa06bf))
|
||||||
|
* **OpenAI Node:** Add support for gpt4 on chat completion ([#5692](https://github.com/n8n-io/n8n/issues/5692)) ([ba73fff](https://github.com/n8n-io/n8n/commit/ba73fff27d2972093746acc3f7016c7420e23459))
|
||||||
|
* **Split In Batches Node:** Make it easy to combine processed data ([#5655](https://github.com/n8n-io/n8n/issues/5655)) ([2f7639e](https://github.com/n8n-io/n8n/commit/2f7639e9e4b10f08c5cb1c4916fc6ae031375cf3))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.215.4](https://github.com/n8n-io/n8n/compare/n8n@0.215.3...n8n@0.215.4) (2023-03-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **core:** Revert `isPending` check on the user entity ([#5571](https://github.com/n8n-io/n8n/issues/5571)) ([6d2c50d](https://github.com/n8n-io/n8n/commit/6d2c50dfed0aeffa2afdb09f5aac80c0e25a6a06))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.214.5](https://github.com/n8n-io/n8n/compare/n8n@0.214.4...n8n@0.214.5) (2023-03-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **core:** Revert `isPending` check on the user entity ([#5571](https://github.com/n8n-io/n8n/issues/5571)) ([b94af03](https://github.com/n8n-io/n8n/commit/b94af0384243d634683212d5199316067956f628))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.219.1](https://github.com/n8n-io/n8n/compare/n8n@0.219.0...n8n@0.219.1) (2023-03-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **HTTP Request Node:** Remove streaming response ([#5663](https://github.com/n8n-io/n8n/issues/5663)) ([974d57d](https://github.com/n8n-io/n8n/commit/974d57dfed78489d3f22c8c63e0ea624c637bfe0))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.219.0](https://github.com/n8n-io/n8n/compare/n8n@0.218.0...n8n@0.219.0) (2023-03-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **core:** Allow serving icons for custom nodes with npm scoped names ([#5626](https://github.com/n8n-io/n8n/issues/5626)) ([45ccdd3](https://github.com/n8n-io/n8n/commit/45ccdd3bb5d5601ccc60438d96aadb40cb87588b))
|
||||||
|
* **core:** Rename advancedFilters to advancedExecutionFilters ([#5643](https://github.com/n8n-io/n8n/issues/5643)) ([419969c](https://github.com/n8n-io/n8n/commit/419969c0d761b1ac7870e7821c0398ecfca1f0ce))
|
||||||
|
* **editor:** Fix ElButton overrides ([#5605](https://github.com/n8n-io/n8n/issues/5605)) ([2eba050](https://github.com/n8n-io/n8n/commit/2eba05046141bd13145f95c6a1ec1e6fb95b37c2))
|
||||||
|
* **editor:** Only fetch new versions at app launch ([#5647](https://github.com/n8n-io/n8n/issues/5647)) ([5b9c521](https://github.com/n8n-io/n8n/commit/5b9c521d04bc34a9f84be966a4646f23c56ca3da))
|
||||||
|
* Fetch credentials on workflows view to include in duplicated workflows ([#5532](https://github.com/n8n-io/n8n/issues/5532)) ([493f7a1](https://github.com/n8n-io/n8n/commit/493f7a1c92d77d3c75fc311892e53f43e1fb367f))
|
||||||
|
* Fix color discrepancies for executions list items ([#5640](https://github.com/n8n-io/n8n/issues/5640)) ([c81656d](https://github.com/n8n-io/n8n/commit/c81656d149764dc398b93d3eb8626a402eddb0ef))
|
||||||
|
* **OpenAI Node:** Fix issue with expressions not working with chat complete ([#5609](https://github.com/n8n-io/n8n/issues/5609)) ([e949db3](https://github.com/n8n-io/n8n/commit/e949db352526033394083476077519598dd8061c))
|
||||||
|
* **OpenAI Node:** Simplify code ([#5618](https://github.com/n8n-io/n8n/issues/5618)) ([1c65bff](https://github.com/n8n-io/n8n/commit/1c65bff31d86ea76ff5fca10341e71389d4de7b5))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Cal Trigger Node:** Update to support v2 webhooks ([#5331](https://github.com/n8n-io/n8n/issues/5331)) ([2889e53](https://github.com/n8n-io/n8n/commit/2889e53b3767f2a61ee7fb3ea9fe1db7c65aaf70))
|
||||||
|
* **core:** Add advancedFilters feature flag ([#5638](https://github.com/n8n-io/n8n/issues/5638)) ([0b5ef09](https://github.com/n8n-io/n8n/commit/0b5ef09e7cbd0c80a4b79311976b5c06c2be8747))
|
||||||
|
* **core:** Add SAML post and test endpoints ([#5595](https://github.com/n8n-io/n8n/issues/5595)) ([523fa71](https://github.com/n8n-io/n8n/commit/523fa71705c0408c6c60c7cb5135323e3488e8c9))
|
||||||
|
* **core:** Add SAML XML validation ([#5600](https://github.com/n8n-io/n8n/issues/5600)) ([ca66ec8](https://github.com/n8n-io/n8n/commit/ca66ec8f4d5ab0e427390b1f1874fb668bc53479))
|
||||||
|
* **core:** Limit user changes when saml is enabled ([#5577](https://github.com/n8n-io/n8n/issues/5577)) ([b517959](https://github.com/n8n-io/n8n/commit/b5179597f3ec4ae468b2eb91969fa56322088e6f))
|
||||||
|
* **core:** Refactor and add SAML preferences for service provider instance ([#5637](https://github.com/n8n-io/n8n/issues/5637)) ([6f27b44](https://github.com/n8n-io/n8n/commit/6f27b445ca2307b94ffc7d4eeb24e76d63516a19))
|
||||||
|
* **editor:** Do not automatically add manual trigger on node plus ([#5644](https://github.com/n8n-io/n8n/issues/5644)) ([ac2f89a](https://github.com/n8n-io/n8n/commit/ac2f89a18a4c25ef1b39bcacc624884de9197fdf))
|
||||||
|
* **editor:** Redirect users to canvas if they don't have any workflows ([#5629](https://github.com/n8n-io/n8n/issues/5629)) ([354edf6](https://github.com/n8n-io/n8n/commit/354edf6886fa3cc5a59d317dcab59cc75e62dc2d))
|
||||||
|
* **HTTP Request Node:** Move from Binary Buffer to Binary streaming ([#5610](https://github.com/n8n-io/n8n/issues/5610)) ([ce0d9d2](https://github.com/n8n-io/n8n/commit/ce0d9d2bede7d87b97e18c45b63ea31ecf592255))
|
||||||
|
* **Mattermost Node:** Add self signed certificate support ([#5630](https://github.com/n8n-io/n8n/issues/5630)) ([01a2160](https://github.com/n8n-io/n8n/commit/01a2160b3b8d36509f4b2871249a3f45358cf692))
|
||||||
|
* **Microsoft SQL Node:** Add support for self signed certificates ([#5160](https://github.com/n8n-io/n8n/issues/5160)) ([971d5ae](https://github.com/n8n-io/n8n/commit/971d5ae8f5d2bfe319fc700fee2bcf597ea4c07e))
|
||||||
|
* **Mindee Node:** Add support for v4 API ([#5559](https://github.com/n8n-io/n8n/issues/5559)) ([e56fbfe](https://github.com/n8n-io/n8n/commit/e56fbfef3ebb50706f24ab07505a0031f361d9b1))
|
||||||
|
* **Slack Node:** Move from Binary Buffer to Binary streaming ([#5612](https://github.com/n8n-io/n8n/issues/5612)) ([9420b0f](https://github.com/n8n-io/n8n/commit/9420b0fdf8ccccb95780c8c97e2b5d14cc4d513e))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.217.1](https://github.com/n8n-io/n8n/compare/n8n@0.217.0...n8n@0.217.1) (2023-02-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **core:** Revert `isPending` check on the user entity ([#5571](https://github.com/n8n-io/n8n/issues/5571)) ([5282fd2](https://github.com/n8n-io/n8n/commit/5282fd266c26e7053ceb887addceed26b741f995))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [0.218.0](https://github.com/n8n-io/n8n/compare/n8n@0.217.2...n8n@0.218.0) (2023-03-02)
|
# [0.218.0](https://github.com/n8n-io/n8n/compare/n8n@0.217.2...n8n@0.218.0) (2023-03-02)
|
||||||
|
|
||||||
|
|
||||||
|
|
48
CHECKLIST.yml
Normal file
48
CHECKLIST.yml
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
paths:
|
||||||
|
"packages/**":
|
||||||
|
- If fixing bug, added test to cover scenario.
|
||||||
|
- If addressing forum or Github issue, added link to description.
|
||||||
|
"packages/**/*.ts":
|
||||||
|
- Added unit tests to cover new or updated functionality.
|
||||||
|
"**/*.vue":
|
||||||
|
- Used composition API for all new components.
|
||||||
|
- Added component or unit tests to cover functionality.
|
||||||
|
|
||||||
|
# cli
|
||||||
|
"packages/cli/src/databases/migrations/**":
|
||||||
|
- Requested review from at least two engineers on migration.
|
||||||
|
- Avoided irreversible data migrations.
|
||||||
|
- Avoided deleting or updating data keys.
|
||||||
|
- Wrote 'down' migration if possible.
|
||||||
|
"n8n/packages/cli/src/api/**":
|
||||||
|
- Added integration tests for new endpoints.
|
||||||
|
|
||||||
|
# editor ui
|
||||||
|
"packages/editor-ui/**/*.vue":
|
||||||
|
- Added E2E if adding new features.
|
||||||
|
- Used design system tokens (colors, spacings...) where possible.
|
||||||
|
"packages/editor-ui/src/mixins/restApi.ts":
|
||||||
|
- Avoided adding new methods. Only deleted from here.
|
||||||
|
"packages/editor-ui/src/mixins/**":
|
||||||
|
- Avoided adding new mixins (use composables instead). Only removed code from here.
|
||||||
|
"packages/editor-ui/src/views/NodeView.vue":
|
||||||
|
- Avoided adding code here. Only refactored to make it smaller.
|
||||||
|
"packages/editor-ui/src/hooks/**":
|
||||||
|
- Avoided adding new hooks. Only refactored to move hooks to relevant store instead.
|
||||||
|
|
||||||
|
# nodes-base
|
||||||
|
"packages/nodes-base/nodes/**":
|
||||||
|
- Added workflow tests for nodes if possible.
|
||||||
|
|
||||||
|
# design-system
|
||||||
|
"packages/design-system/**/*.vue":
|
||||||
|
- Used design system tokens (colors, spacings...) where possible.
|
||||||
|
- Updated Storybook with new component or updated functionality.
|
||||||
|
|
||||||
|
# e2e
|
||||||
|
"cypress/e2e/**":
|
||||||
|
- Avoided chaining commands more than two or three times (to avoid flakiness because only last one will be retried).
|
||||||
|
- Spoofed endpoints that are not critical for the test (to avoid flakiness).
|
||||||
|
- Picked most efficient path to start the test (for example skipped account setup and starting at /workflow/new for a canvas test).
|
||||||
|
- Avoided adding waits on time (use request intercepts instead).
|
||||||
|
- Ensured each spec does not depend on any another spec to pass.
|
|
@ -11,6 +11,8 @@ module.exports = defineConfig({
|
||||||
},
|
},
|
||||||
defaultCommandTimeout: 10000,
|
defaultCommandTimeout: 10000,
|
||||||
requestTimeout: 12000,
|
requestTimeout: 12000,
|
||||||
|
numTestsKeptInMemory: 0,
|
||||||
|
experimentalMemoryManagement: true,
|
||||||
e2e: {
|
e2e: {
|
||||||
baseUrl: BASE_URL,
|
baseUrl: BASE_URL,
|
||||||
video: true,
|
video: true,
|
||||||
|
|
|
@ -1,13 +1,7 @@
|
||||||
import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants';
|
|
||||||
import { randFirstName, randLastName } from '@ngneat/falso';
|
|
||||||
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
|
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
|
||||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
const email = DEFAULT_USER_EMAIL;
|
|
||||||
const password = DEFAULT_USER_PASSWORD;
|
|
||||||
const firstName = randFirstName();
|
|
||||||
const lastName = randLastName();
|
|
||||||
const WorkflowsPage = new WorkflowsPageClass();
|
const WorkflowsPage = new WorkflowsPageClass();
|
||||||
const WorkflowPage = new WorkflowPageClass();
|
const WorkflowPage = new WorkflowPageClass();
|
||||||
|
|
||||||
|
@ -16,23 +10,17 @@ const multipleWorkflowsCount = 5;
|
||||||
describe('Workflows', () => {
|
describe('Workflows', () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.resetAll();
|
cy.resetAll();
|
||||||
cy.setup({ email, firstName, lastName, password });
|
cy.skipSetup();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.on('uncaught:exception', (err, runnable) => {
|
cy.visit(WorkflowsPage.url);
|
||||||
expect(err.message).to.include('Not logged in');
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.signin({ email, password });
|
it('should create a new workflow using empty state card', () => {
|
||||||
cy.visit('/');
|
WorkflowsPage.getters.newWorkflowButtonCard().should('be.visible');
|
||||||
cy.waitForLoad();
|
WorkflowsPage.getters.newWorkflowButtonCard().click();
|
||||||
});
|
|
||||||
|
|
||||||
it('should land on empty canvas after registration', () => {
|
|
||||||
cy.url().should('include', WorkflowPage.url);
|
|
||||||
cy.createFixtureWorkflow('Test_workflow_1.json', `Empty State Card Workflow ${uuid()}`);
|
cy.createFixtureWorkflow('Test_workflow_1.json', `Empty State Card Workflow ${uuid()}`);
|
||||||
|
|
||||||
WorkflowPage.getters.workflowTags().should('contain.text', 'some-tag-1');
|
WorkflowPage.getters.workflowTags().should('contain.text', 'some-tag-1');
|
||||||
|
@ -89,13 +77,5 @@ describe('Workflows', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
WorkflowsPage.getters.newWorkflowButtonCard().should('be.visible');
|
WorkflowsPage.getters.newWorkflowButtonCard().should('be.visible');
|
||||||
WorkflowsPage.getters.newWorkflowTemplateCard().should('be.visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should redirect to new canvas if no workflows', () => {
|
|
||||||
cy.wait(1000);
|
|
||||||
cy.visit(WorkflowsPage.url);
|
|
||||||
cy.wait(1000);
|
|
||||||
cy.url().should('include', WorkflowPage.url);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
172
cypress/e2e/12-canvas-actions.cy.ts
Normal file
172
cypress/e2e/12-canvas-actions.cy.ts
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
import {
|
||||||
|
MANUAL_TRIGGER_NODE_NAME,
|
||||||
|
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
|
||||||
|
CODE_NODE_NAME,
|
||||||
|
SCHEDULE_TRIGGER_NODE_NAME,
|
||||||
|
SET_NODE_NAME,
|
||||||
|
IF_NODE_NAME,
|
||||||
|
HTTP_REQUEST_NODE_NAME,
|
||||||
|
} from './../constants';
|
||||||
|
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||||
|
|
||||||
|
const WorkflowPage = new WorkflowPageClass();
|
||||||
|
describe('Canvas Actions', () => {
|
||||||
|
before(() => {
|
||||||
|
cy.resetAll();
|
||||||
|
cy.skipSetup();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
WorkflowPage.actions.visit();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render canvas', () => {
|
||||||
|
WorkflowPage.getters.nodeViewRoot().should('be.visible');
|
||||||
|
WorkflowPage.getters.canvasPlusButton().should('be.visible');
|
||||||
|
WorkflowPage.getters.zoomToFitButton().should('be.visible');
|
||||||
|
WorkflowPage.getters.zoomInButton().should('be.visible');
|
||||||
|
WorkflowPage.getters.zoomOutButton().should('be.visible');
|
||||||
|
WorkflowPage.getters.executeWorkflowButton().should('be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should connect and disconnect a simple node', () => {
|
||||||
|
WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME);
|
||||||
|
WorkflowPage.getters.nodeViewBackground().click(600, 200, { force: true });
|
||||||
|
cy.get('.jtk-connector').should('have.length', 1);
|
||||||
|
WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME);
|
||||||
|
|
||||||
|
// Change connection from Set to Set1
|
||||||
|
cy.draganddrop(
|
||||||
|
WorkflowPage.getters.getEndpointSelector('input', SET_NODE_NAME),
|
||||||
|
WorkflowPage.getters.getEndpointSelector('input', `${SET_NODE_NAME}1`),
|
||||||
|
);
|
||||||
|
|
||||||
|
WorkflowPage.getters
|
||||||
|
.canvasNodeInputEndpointByName(`${SET_NODE_NAME}1`)
|
||||||
|
.should('have.class', 'jtk-endpoint-connected');
|
||||||
|
|
||||||
|
cy.get('.jtk-connector').should('have.length', 1);
|
||||||
|
// Disconnect Set1
|
||||||
|
cy.drag(WorkflowPage.getters.getEndpointSelector('input', `${SET_NODE_NAME}1`), [-200, 100]);
|
||||||
|
cy.get('.jtk-connector').should('have.length', 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add first step', () => {
|
||||||
|
WorkflowPage.getters.canvasPlusButton().should('be.visible');
|
||||||
|
WorkflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
WorkflowPage.getters.canvasNodes().should('have.length', 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a node via plus endpoint drag', () => {
|
||||||
|
WorkflowPage.getters.canvasPlusButton().should('be.visible');
|
||||||
|
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME, true);
|
||||||
|
|
||||||
|
cy.drag(
|
||||||
|
WorkflowPage.getters.getEndpointSelector('plus', SCHEDULE_TRIGGER_NODE_NAME),
|
||||||
|
[100, 100],
|
||||||
|
);
|
||||||
|
|
||||||
|
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
|
||||||
|
WorkflowPage.actions.addNodeToCanvas(IF_NODE_NAME, false);
|
||||||
|
WorkflowPage.getters.nodeViewBackground().click({ force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should add a connected node using plus endpoint', () => {
|
||||||
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
cy.get('.plus-endpoint').should('be.visible').click();
|
||||||
|
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
|
||||||
|
WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME);
|
||||||
|
WorkflowPage.getters.nodeCreatorSearchBar().type('{enter}');
|
||||||
|
cy.get('body').type('{esc}');
|
||||||
|
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
||||||
|
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add disconnected node if nothing is selected', () => {
|
||||||
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
// Deselect nodes
|
||||||
|
WorkflowPage.getters.nodeViewBackground().click({ force: true });
|
||||||
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
|
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
||||||
|
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add node between two connected nodes', () => {
|
||||||
|
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||||
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
|
WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME);
|
||||||
|
WorkflowPage.actions.zoomToFit();
|
||||||
|
WorkflowPage.actions.addNodeBetweenNodes(CODE_NODE_NAME, SET_NODE_NAME, HTTP_REQUEST_NODE_NAME);
|
||||||
|
WorkflowPage.getters.canvasNodes().should('have.length', 4);
|
||||||
|
WorkflowPage.getters.nodeConnections().should('have.length', 3);
|
||||||
|
// And last node should be pushed to the right
|
||||||
|
WorkflowPage.getters
|
||||||
|
.canvasNodes()
|
||||||
|
.last()
|
||||||
|
.should('have.attr', 'style', 'left: 860px; top: 260px;');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete connections by pressing the delete button', () => {
|
||||||
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
|
WorkflowPage.getters.nodeConnections().first().realHover();
|
||||||
|
cy.get('.connection-actions .delete').first().click({ force: true });
|
||||||
|
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete a connection by moving it away from endpoint', () => {
|
||||||
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
|
cy.drag(WorkflowPage.getters.getEndpointSelector('input', CODE_NODE_NAME), [0, -100]);
|
||||||
|
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should execute node', () => {
|
||||||
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
|
WorkflowPage.getters
|
||||||
|
.canvasNodes()
|
||||||
|
.last()
|
||||||
|
.find('[data-test-id="execute-node-button"]')
|
||||||
|
.click({ force: true });
|
||||||
|
WorkflowPage.getters.successToast().should('contain', 'Node executed successfully');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should copy selected nodes', () => {
|
||||||
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
|
WorkflowPage.actions.selectAll();
|
||||||
|
WorkflowPage.actions.hitCopy();
|
||||||
|
WorkflowPage.getters.successToast().should('contain', 'Copied!');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select all nodes', () => {
|
||||||
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
|
WorkflowPage.actions.selectAll();
|
||||||
|
WorkflowPage.getters.selectedNodes().should('have.length', 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select nodes using arrow keys', () => {
|
||||||
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
|
cy.wait(500);
|
||||||
|
cy.get('body').type('{leftArrow}');
|
||||||
|
WorkflowPage.getters.canvasNodes().first().should('have.class', 'jtk-drag-selected');
|
||||||
|
cy.get('body').type('{rightArrow}');
|
||||||
|
WorkflowPage.getters.canvasNodes().last().should('have.class', 'jtk-drag-selected');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select nodes using shift and arrow keys', () => {
|
||||||
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
|
cy.wait(500);
|
||||||
|
cy.get('body').type('{shift}', { release: false }).type('{leftArrow}');
|
||||||
|
WorkflowPage.getters.selectedNodes().should('have.length', 2);
|
||||||
|
});
|
||||||
|
});
|
|
@ -20,7 +20,7 @@ const ZOOM_OUT_X1_FACTOR = 0.8;
|
||||||
const ZOOM_OUT_X2_FACTOR = 0.64;
|
const ZOOM_OUT_X2_FACTOR = 0.64;
|
||||||
const RENAME_NODE_NAME = 'Something else';
|
const RENAME_NODE_NAME = 'Something else';
|
||||||
|
|
||||||
describe('Canvas Actions', () => {
|
describe('Canvas Node Manipulation and Navigation', () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.resetAll();
|
cy.resetAll();
|
||||||
cy.skipSetup();
|
cy.skipSetup();
|
||||||
|
@ -30,56 +30,6 @@ describe('Canvas Actions', () => {
|
||||||
WorkflowPage.actions.visit();
|
WorkflowPage.actions.visit();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render canvas', () => {
|
|
||||||
WorkflowPage.getters.nodeViewRoot().should('be.visible');
|
|
||||||
WorkflowPage.getters.canvasPlusButton().should('be.visible');
|
|
||||||
WorkflowPage.getters.zoomToFitButton().should('be.visible');
|
|
||||||
WorkflowPage.getters.zoomInButton().should('be.visible');
|
|
||||||
WorkflowPage.getters.zoomOutButton().should('be.visible');
|
|
||||||
WorkflowPage.getters.executeWorkflowButton().should('be.visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should connect and disconnect a simple node', () => {
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME);
|
|
||||||
WorkflowPage.getters.nodeViewBackground().click(600, 200, { force: true });
|
|
||||||
cy.get('.jtk-connector').should('have.length', 1);
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME);
|
|
||||||
|
|
||||||
// Change connection from Set to Set1
|
|
||||||
cy.draganddrop(
|
|
||||||
WorkflowPage.getters.getEndpointSelector('input', SET_NODE_NAME),
|
|
||||||
WorkflowPage.getters.getEndpointSelector('input', `${SET_NODE_NAME}1`),
|
|
||||||
);
|
|
||||||
|
|
||||||
WorkflowPage.getters
|
|
||||||
.canvasNodeInputEndpointByName(`${SET_NODE_NAME}1`)
|
|
||||||
.should('have.class', 'jtk-endpoint-connected');
|
|
||||||
|
|
||||||
cy.get('.jtk-connector').should('have.length', 1);
|
|
||||||
// Disconnect Set1
|
|
||||||
cy.drag(WorkflowPage.getters.getEndpointSelector('input', `${SET_NODE_NAME}1`), [-200, 100]);
|
|
||||||
cy.get('.jtk-connector').should('have.length', 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add first step', () => {
|
|
||||||
WorkflowPage.getters.canvasPlusButton().should('be.visible');
|
|
||||||
WorkflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
|
||||||
WorkflowPage.getters.canvasNodes().should('have.length', 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add a node via plus endpoint drag', () => {
|
|
||||||
WorkflowPage.getters.canvasPlusButton().should('be.visible');
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME, true);
|
|
||||||
|
|
||||||
cy.drag(
|
|
||||||
WorkflowPage.getters.getEndpointSelector('plus', SCHEDULE_TRIGGER_NODE_NAME),
|
|
||||||
[100, 100],
|
|
||||||
);
|
|
||||||
|
|
||||||
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(IF_NODE_NAME, false);
|
|
||||||
WorkflowPage.getters.nodeViewBackground().click({ force: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add switch node and test connections', () => {
|
it('should add switch node and test connections', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(SWITCH_NODE_NAME, true);
|
WorkflowPage.actions.addNodeToCanvas(SWITCH_NODE_NAME, true);
|
||||||
|
@ -142,6 +92,8 @@ describe('Canvas Actions', () => {
|
||||||
|
|
||||||
cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 4);
|
cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 4);
|
||||||
WorkflowPage.actions.executeWorkflow();
|
WorkflowPage.actions.executeWorkflow();
|
||||||
|
WorkflowPage.getters.stopExecutionButton().should('not.exist');
|
||||||
|
|
||||||
// If the merged set nodes are connected and executed correctly, there should be 2 items in the output of merge node
|
// If the merged set nodes are connected and executed correctly, there should be 2 items in the output of merge node
|
||||||
cy.get('[data-label="2 items"]').should('be.visible');
|
cy.get('[data-label="2 items"]').should('be.visible');
|
||||||
});
|
});
|
||||||
|
@ -167,41 +119,6 @@ describe('Canvas Actions', () => {
|
||||||
cy.get('.jtk-connector').should('have.length', 4);
|
cy.get('.jtk-connector').should('have.length', 4);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add a connected node using plus endpoint', () => {
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
|
||||||
cy.get('.plus-endpoint').should('be.visible').click();
|
|
||||||
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
|
|
||||||
WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME);
|
|
||||||
WorkflowPage.getters.nodeCreatorSearchBar().type('{enter}');
|
|
||||||
cy.get('body').type('{esc}');
|
|
||||||
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add disconnected node if nothing is selected', () => {
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
|
||||||
// Deselect nodes
|
|
||||||
WorkflowPage.getters.nodeViewBackground().click({ force: true });
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
|
||||||
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add note between two connected nodes', () => {
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME);
|
|
||||||
WorkflowPage.actions.zoomToFit();
|
|
||||||
WorkflowPage.actions.addNodeBetweenNodes(CODE_NODE_NAME, SET_NODE_NAME, HTTP_REQUEST_NODE_NAME);
|
|
||||||
WorkflowPage.getters.canvasNodes().should('have.length', 4);
|
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 3);
|
|
||||||
// And last node should be pushed to the right
|
|
||||||
WorkflowPage.getters
|
|
||||||
.canvasNodes()
|
|
||||||
.last()
|
|
||||||
.should('have.attr', 'style', 'left: 860px; top: 260px;');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should delete node using node action button', () => {
|
it('should delete node using node action button', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
|
@ -319,50 +236,6 @@ describe('Canvas Actions', () => {
|
||||||
WorkflowPage.getters.canvasNodes().last().should('be.visible');
|
WorkflowPage.getters.canvasNodes().last().should('be.visible');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should select all nodes', () => {
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
|
||||||
WorkflowPage.actions.selectAll();
|
|
||||||
WorkflowPage.getters.selectedNodes().should('have.length', 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should select nodes using arrow keys', () => {
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
|
||||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
|
||||||
cy.wait(500);
|
|
||||||
cy.get('body').type('{leftArrow}');
|
|
||||||
WorkflowPage.getters.canvasNodes().first().should('have.class', 'jtk-drag-selected');
|
|
||||||
cy.get('body').type('{rightArrow}');
|
|
||||||
WorkflowPage.getters.canvasNodes().last().should('have.class', 'jtk-drag-selected');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should select nodes using shift and arrow keys', () => {
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
|
||||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
|
||||||
cy.wait(500);
|
|
||||||
cy.get('body').type('{shift}', { release: false }).type('{leftArrow}');
|
|
||||||
WorkflowPage.getters.selectedNodes().should('have.length', 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should delete connections by pressing the delete button', () => {
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
|
||||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
|
||||||
WorkflowPage.getters.nodeConnections().first().realHover();
|
|
||||||
cy.get('.connection-actions .delete').first().click({ force: true });
|
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should delete a connection by moving it away from endpoint', () => {
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
|
||||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
|
||||||
cy.drag(WorkflowPage.getters.getEndpointSelector('input', CODE_NODE_NAME), [0, -100]);
|
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should disable node by pressing the disable button', () => {
|
it('should disable node by pressing the disable button', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
|
@ -418,23 +291,4 @@ describe('Canvas Actions', () => {
|
||||||
WorkflowPage.getters.canvasNodes().should('have.length', 3);
|
WorkflowPage.getters.canvasNodes().should('have.length', 3);
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should execute node', () => {
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
|
||||||
WorkflowPage.getters
|
|
||||||
.canvasNodes()
|
|
||||||
.last()
|
|
||||||
.find('[data-test-id="execute-node-button"]')
|
|
||||||
.click({ force: true });
|
|
||||||
WorkflowPage.getters.successToast().should('contain', 'Node executed successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should copy selected nodes', () => {
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
|
||||||
WorkflowPage.actions.selectAll();
|
|
||||||
WorkflowPage.actions.hitCopy();
|
|
||||||
WorkflowPage.getters.successToast().should('contain', 'Copied!');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,7 +11,6 @@ describe('Data transformation expressions', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wf.actions.visit();
|
wf.actions.visit();
|
||||||
cy.waitForLoad();
|
|
||||||
|
|
||||||
cy.window()
|
cy.window()
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
|
@ -16,6 +16,9 @@ import {
|
||||||
} from '../constants';
|
} from '../constants';
|
||||||
import { randFirstName, randLastName } from '@ngneat/falso';
|
import { randFirstName, randLastName } from '@ngneat/falso';
|
||||||
import { CredentialsPage, CredentialsModal, WorkflowPage, NDV } from '../pages';
|
import { CredentialsPage, CredentialsModal, WorkflowPage, NDV } from '../pages';
|
||||||
|
import CustomNodeWithN8nCredentialFixture from '../fixtures/Custom_node_n8n_credential.json';
|
||||||
|
import CustomNodeWithCustomCredentialFixture from '../fixtures/Custom_node_custom_credential.json';
|
||||||
|
import CustomCredential from '../fixtures/Custom_credential.json';
|
||||||
|
|
||||||
const email = DEFAULT_USER_EMAIL;
|
const email = DEFAULT_USER_EMAIL;
|
||||||
const password = DEFAULT_USER_PASSWORD;
|
const password = DEFAULT_USER_PASSWORD;
|
||||||
|
@ -31,25 +34,10 @@ const NEW_CREDENTIAL_NAME = 'Something else';
|
||||||
describe('Credentials', () => {
|
describe('Credentials', () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.resetAll();
|
cy.resetAll();
|
||||||
cy.setup({ email, firstName, lastName, password });
|
cy.skipSetup();
|
||||||
|
|
||||||
// Always intercept the request to test credentials and return a success
|
|
||||||
cy.intercept('POST', '/rest/credentials/test', {
|
|
||||||
statusCode: 200,
|
|
||||||
body: {
|
|
||||||
data: { status: 'success', message: 'Tested successfully' },
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.on('uncaught:exception', (err, runnable) => {
|
|
||||||
expect(err.message).to.include('Not logged in');
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.signin({ email, password });
|
|
||||||
cy.visit(credentialsPage.url);
|
cy.visit(credentialsPage.url);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -250,24 +238,4 @@ describe('Credentials', () => {
|
||||||
credentialsModal.actions.fillCredentialsForm();
|
credentialsModal.actions.fillCredentialsForm();
|
||||||
workflowPage.getters.nodeCredentialsSelect().should('contain', NEW_QUERY_AUTH_ACCOUNT_NAME);
|
workflowPage.getters.nodeCredentialsSelect().should('contain', NEW_QUERY_AUTH_ACCOUNT_NAME);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render custom node with n8n credential', () => {
|
|
||||||
workflowPage.actions.visit();
|
|
||||||
workflowPage.actions.addNodeToCanvas('Manual');
|
|
||||||
workflowPage.actions.addNodeToCanvas('E2E Node with native n8n credential', true, true);
|
|
||||||
workflowPage.getters.nodeCredentialsLabel().click();
|
|
||||||
cy.contains('Create New Credential').click();
|
|
||||||
credentialsModal.getters.editCredentialModal().should('be.visible');
|
|
||||||
credentialsModal.getters.editCredentialModal().should('contain.text', 'Notion API');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render custom node with custom credential', () => {
|
|
||||||
workflowPage.actions.visit();
|
|
||||||
workflowPage.actions.addNodeToCanvas('Manual');
|
|
||||||
workflowPage.actions.addNodeToCanvas('E2E Node with custom credential', true, true);
|
|
||||||
workflowPage.getters.nodeCredentialsLabel().click();
|
|
||||||
cy.contains('Create New Credential').click();
|
|
||||||
credentialsModal.getters.editCredentialModal().should('be.visible');
|
|
||||||
credentialsModal.getters.editCredentialModal().should('contain.text', 'Custom E2E Credential');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
101
cypress/e2e/21-community-nodes.cy.ts
Normal file
101
cypress/e2e/21-community-nodes.cy.ts
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
import { NodeCreator } from '../pages/features/node-creator';
|
||||||
|
import CustomNodeFixture from '../fixtures/Custom_node.json';
|
||||||
|
import { CredentialsModal, WorkflowPage } from '../pages';
|
||||||
|
import CustomNodeWithN8nCredentialFixture from '../fixtures/Custom_node_n8n_credential.json';
|
||||||
|
import CustomNodeWithCustomCredentialFixture from '../fixtures/Custom_node_custom_credential.json';
|
||||||
|
import CustomCredential from '../fixtures/Custom_credential.json';
|
||||||
|
|
||||||
|
const credentialsModal = new CredentialsModal();
|
||||||
|
const nodeCreatorFeature = new NodeCreator();
|
||||||
|
const workflowPage = new WorkflowPage();
|
||||||
|
|
||||||
|
// We separate-out the custom nodes because they require injecting nodes and credentials
|
||||||
|
// so the /nodes and /credentials endpoints are intercepted and non-cached.
|
||||||
|
// We want to keep the other tests as fast as possible so we don't want to break the cache in those.
|
||||||
|
describe('Community Nodes', () => {
|
||||||
|
before(() => {
|
||||||
|
cy.resetAll();
|
||||||
|
cy.skipSetup();
|
||||||
|
})
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.intercept('/types/nodes.json', { middleware: true }, (req) => {
|
||||||
|
req.headers['cache-control'] = 'no-cache, no-store';
|
||||||
|
|
||||||
|
req.on('response', (res) => {
|
||||||
|
const nodes = res.body || [];
|
||||||
|
|
||||||
|
nodes.push(CustomNodeFixture, CustomNodeWithN8nCredentialFixture, CustomNodeWithCustomCredentialFixture);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.intercept('/types/credentials.json', { middleware: true }, (req) => {
|
||||||
|
req.headers['cache-control'] = 'no-cache, no-store';
|
||||||
|
|
||||||
|
req.on('response', (res) => {
|
||||||
|
const credentials = res.body || [];
|
||||||
|
|
||||||
|
credentials.push(CustomCredential);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
workflowPage.actions.visit();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render and select community node', () => {
|
||||||
|
const customNode = 'E2E Node';
|
||||||
|
|
||||||
|
nodeCreatorFeature.actions.openNodeCreator();
|
||||||
|
nodeCreatorFeature.getters.searchBar().find('input').clear().type(customNode);
|
||||||
|
|
||||||
|
nodeCreatorFeature.getters
|
||||||
|
.getCreatorItem(customNode)
|
||||||
|
.findChildByTestId('node-creator-item-tooltip')
|
||||||
|
.should('exist');
|
||||||
|
nodeCreatorFeature.actions.selectNode(customNode);
|
||||||
|
|
||||||
|
// TODO: Replace once we have canvas feature utils
|
||||||
|
cy.get('.data-display .node-name').contains(customNode).should('exist');
|
||||||
|
|
||||||
|
const nodeParameters = () => cy.getByTestId('node-parameters');
|
||||||
|
const firstParameter = () => nodeParameters().find('.parameter-item').eq(0);
|
||||||
|
const secondParameter = () => nodeParameters().find('.parameter-item').eq(1);
|
||||||
|
|
||||||
|
// Check correct fields are rendered
|
||||||
|
nodeParameters().should('exist');
|
||||||
|
// Test property text input
|
||||||
|
firstParameter().contains('Test property').should('exist');
|
||||||
|
firstParameter().find('input.el-input__inner').should('have.value', 'Some default');
|
||||||
|
// Resource select input
|
||||||
|
secondParameter().find('label').contains('Resource').should('exist');
|
||||||
|
secondParameter().find('input.el-input__inner').should('have.value', 'option2');
|
||||||
|
secondParameter().find('.el-select').click();
|
||||||
|
secondParameter().find('.el-select-dropdown__list').should('exist');
|
||||||
|
// Check if all options are rendered and select the fourth one
|
||||||
|
secondParameter().find('.el-select-dropdown__list').children().should('have.length', 4);
|
||||||
|
secondParameter()
|
||||||
|
.find('.el-select-dropdown__list')
|
||||||
|
.children()
|
||||||
|
.eq(3)
|
||||||
|
.contains('option4')
|
||||||
|
.should('exist')
|
||||||
|
.click();
|
||||||
|
secondParameter().find('input.el-input__inner').should('have.value', 'option4');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render custom node with n8n credential', () => {
|
||||||
|
workflowPage.actions.addNodeToCanvas('Manual');
|
||||||
|
workflowPage.actions.addNodeToCanvas('E2E Node with native n8n credential', true, true);
|
||||||
|
workflowPage.getters.nodeCredentialsLabel().click();
|
||||||
|
cy.contains('Create New Credential').click();
|
||||||
|
credentialsModal.getters.editCredentialModal().should('be.visible');
|
||||||
|
credentialsModal.getters.editCredentialModal().should('contain.text', 'Notion API');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render custom node with custom credential', () => {
|
||||||
|
workflowPage.actions.addNodeToCanvas('Manual');
|
||||||
|
workflowPage.actions.addNodeToCanvas('E2E Node with custom credential', true, true);
|
||||||
|
workflowPage.getters.nodeCredentialsLabel().click();
|
||||||
|
cy.contains('Create New Credential').click();
|
||||||
|
credentialsModal.getters.editCredentialModal().should('be.visible');
|
||||||
|
credentialsModal.getters.editCredentialModal().should('contain.text', 'Custom E2E Credential');
|
||||||
|
});
|
||||||
|
});
|
|
@ -6,6 +6,7 @@ const nodeCreatorFeature = new NodeCreator();
|
||||||
const WorkflowPage = new WorkflowPageClass();
|
const WorkflowPage = new WorkflowPageClass();
|
||||||
const NDVModal = new NDV();
|
const NDVModal = new NDV();
|
||||||
|
|
||||||
|
|
||||||
describe('Node Creator', () => {
|
describe('Node Creator', () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.resetAll();
|
cy.resetAll();
|
||||||
|
@ -104,44 +105,75 @@ describe('Node Creator', () => {
|
||||||
NDVModal.getters.parameterInput('operation').should('contain.text', 'Rename');
|
NDVModal.getters.parameterInput('operation').should('contain.text', 'Rename');
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render and select community node', () => {
|
it('should not show actions for single action nodes', () => {
|
||||||
const customNode = 'E2E Node';
|
const singleActionNodes = [
|
||||||
|
'DHL',
|
||||||
|
'iCalendar',
|
||||||
|
'LingvaNex',
|
||||||
|
'Mailcheck',
|
||||||
|
'MSG91',
|
||||||
|
'OpenThesaurus',
|
||||||
|
'Spontit',
|
||||||
|
'Vonage',
|
||||||
|
'Send Email',
|
||||||
|
'Toggl Trigger'
|
||||||
|
]
|
||||||
|
const doubleActionNode = 'OpenWeatherMap'
|
||||||
|
|
||||||
nodeCreatorFeature.actions.openNodeCreator();
|
nodeCreatorFeature.actions.openNodeCreator();
|
||||||
nodeCreatorFeature.getters.searchBar().find('input').clear().type(customNode);
|
singleActionNodes.forEach((node) => {
|
||||||
|
nodeCreatorFeature.getters.searchBar().find('input').clear().type(node);
|
||||||
|
nodeCreatorFeature.getters.getCreatorItem(node).find('button[class*="panelIcon"]').should('not.exist');
|
||||||
|
})
|
||||||
|
nodeCreatorFeature.getters.searchBar().find('input').clear().type(doubleActionNode);
|
||||||
|
nodeCreatorFeature.getters.getCreatorItem(doubleActionNode).click();
|
||||||
|
nodeCreatorFeature.getters.creatorItem().should('have.length', 2);
|
||||||
|
})
|
||||||
|
|
||||||
nodeCreatorFeature.getters
|
describe('should correctly append manual trigger for regular actions', () => {
|
||||||
.getCreatorItem(customNode)
|
// For these sources, manual node should be added
|
||||||
.findChildByTestId('node-creator-item-tooltip')
|
const sourcesWithAppend = [
|
||||||
.should('exist');
|
{
|
||||||
nodeCreatorFeature.actions.selectNode(customNode);
|
name: 'canvas add button',
|
||||||
|
handler: () => nodeCreatorFeature.getters.canvasAddButton().click(),
|
||||||
|
}, {
|
||||||
|
name: 'plus button',
|
||||||
|
handler: () => nodeCreatorFeature.getters.plusButton().click(),
|
||||||
|
},
|
||||||
|
// We can't test this one because it's not possible to trigger tab key in Cypress
|
||||||
|
// only way is to use `realPress` which is hanging the tests in Electron for some reason
|
||||||
|
// {
|
||||||
|
// name: 'tab key',
|
||||||
|
// handler: () => cy.realPress('Tab'),
|
||||||
|
// },
|
||||||
|
]
|
||||||
|
sourcesWithAppend.forEach((source) => {
|
||||||
|
it(`should append manual trigger when source is ${source.name}`, () => {
|
||||||
|
source.handler()
|
||||||
|
nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n');
|
||||||
|
nodeCreatorFeature.getters.getCreatorItem('n8n').click();
|
||||||
|
nodeCreatorFeature.getters.getCreatorItem('Create a credential').click();
|
||||||
|
NDVModal.actions.close();
|
||||||
|
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// TODO: Replace once we have canvas feature utils
|
it('should not append manual trigger when source is canvas related', () => {
|
||||||
cy.get('.data-display .node-name').contains(customNode).should('exist');
|
nodeCreatorFeature.getters.canvasAddButton().click();
|
||||||
|
nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n');
|
||||||
const nodeParameters = () => cy.getByTestId('node-parameters');
|
nodeCreatorFeature.getters.getCreatorItem('n8n').click();
|
||||||
const firstParameter = () => nodeParameters().find('.parameter-item').eq(0);
|
nodeCreatorFeature.getters.getCreatorItem('Create a credential').click();
|
||||||
const secondParameter = () => nodeParameters().find('.parameter-item').eq(1);
|
NDVModal.actions.close();
|
||||||
|
WorkflowPage.actions.deleteNode('When clicking "Execute Workflow"')
|
||||||
// Check correct fields are rendered
|
WorkflowPage.getters.canvasNodePlusEndpointByName('n8n').click()
|
||||||
nodeParameters().should('exist');
|
nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n');
|
||||||
// Test property text input
|
nodeCreatorFeature.getters.getCreatorItem('n8n').click();
|
||||||
firstParameter().contains('Test property').should('exist');
|
nodeCreatorFeature.getters.getCreatorItem('Create a credential').click();
|
||||||
firstParameter().find('input.el-input__inner').should('have.value', 'Some default');
|
NDVModal.actions.close();
|
||||||
// Resource select input
|
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
||||||
secondParameter().find('label').contains('Resource').should('exist');
|
WorkflowPage.actions.zoomToFit();
|
||||||
secondParameter().find('input.el-input__inner').should('have.value', 'option2');
|
WorkflowPage.actions.addNodeBetweenNodes('n8n', 'n8n1', 'Item Lists')
|
||||||
secondParameter().find('.el-select').click();
|
WorkflowPage.getters.canvasNodes().should('have.length', 3);
|
||||||
secondParameter().find('.el-select-dropdown__list').should('exist');
|
})
|
||||||
// Check if all options are rendered and select the fourth one
|
|
||||||
secondParameter().find('.el-select-dropdown__list').children().should('have.length', 4);
|
|
||||||
secondParameter()
|
|
||||||
.find('.el-select-dropdown__list')
|
|
||||||
.children()
|
|
||||||
.eq(3)
|
|
||||||
.contains('option4')
|
|
||||||
.should('exist')
|
|
||||||
.click();
|
|
||||||
secondParameter().find('input.el-input__inner').should('have.value', 'option4');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -89,4 +89,42 @@ describe('NDV', () => {
|
||||||
cy.get('[class*=hasIssues]').should('have.length', 1);
|
cy.get('[class*=hasIssues]').should('have.length', 1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('test output schema view', () => {
|
||||||
|
const schemaKeys = ['id', 'name', 'email', 'notes', 'country', 'created', 'objectValue', 'prop1', 'prop2'];
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.createFixtureWorkflow('Test_workflow_schema_test.json', `NDV test schema view ${uuid()}`);
|
||||||
|
workflowPage.actions.zoomToFit();
|
||||||
|
workflowPage.actions.openNode('Set');
|
||||||
|
ndv.actions.execute();
|
||||||
|
});
|
||||||
|
it('should switch to output schema view and validate it', () => {
|
||||||
|
ndv.getters.outputDisplayMode().children().should('have.length', 3);
|
||||||
|
ndv.getters.outputDisplayMode().find('[class*=active]').should('contain', 'Table');
|
||||||
|
ndv.getters.outputDisplayMode().contains('Schema').click();
|
||||||
|
ndv.getters.outputDisplayMode().find('[class*=active]').should('contain', 'Schema');
|
||||||
|
|
||||||
|
schemaKeys.forEach((key) => {
|
||||||
|
ndv.getters.outputPanel().find('[data-test-id=run-data-schema-item]').contains(key).should('exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should preserve schema view after execution', () => {
|
||||||
|
ndv.getters.outputDisplayMode().contains('Schema').click();
|
||||||
|
ndv.actions.execute();
|
||||||
|
ndv.getters.outputDisplayMode().find('[class*=active]').should('contain', 'Schema');
|
||||||
|
})
|
||||||
|
it('should collapse and expand nested schema object', () => {
|
||||||
|
const expandedObjectProps = ['prop1', 'prop2'];;
|
||||||
|
const getObjectValueItem = () => ndv.getters.outputPanel().find('[data-test-id=run-data-schema-item]').filter(':contains("objectValue")');
|
||||||
|
ndv.getters.outputDisplayMode().contains('Schema').click();
|
||||||
|
|
||||||
|
expandedObjectProps.forEach((key) => {
|
||||||
|
ndv.getters.outputPanel().find('[data-test-id=run-data-schema-item]').contains(key).should('be.visible');
|
||||||
|
});
|
||||||
|
getObjectValueItem().find('label').click();
|
||||||
|
expandedObjectProps.forEach((key) => {
|
||||||
|
ndv.getters.outputPanel().find('[data-test-id=run-data-schema-item]').contains(key).should('not.be.visible');
|
||||||
|
});
|
||||||
|
})
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||||
|
|
||||||
const NEW_WORKFLOW_NAME = 'Something else';
|
const NEW_WORKFLOW_NAME = 'Something else';
|
||||||
const IMPORT_WORKFLOW_URL = 'https://www.jsonkeeper.com/b/FNB0#.json';
|
const IMPORT_WORKFLOW_URL = 'https://gist.githubusercontent.com/OlegIvaniv/010bd3f45c8a94f8eb7012e663a8b671/raw/3afea1aec15573cc168d9af7e79395bd76082906/test-workflow.json';
|
||||||
const DUPLICATE_WORKFLOW_NAME = 'Duplicated workflow';
|
const DUPLICATE_WORKFLOW_NAME = 'Duplicated workflow';
|
||||||
const DUPLICATE_WORKFLOW_TAG = 'Duplicate';
|
const DUPLICATE_WORKFLOW_TAG = 'Duplicate';
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ describe('Workflow Actions', () => {
|
||||||
cy.get('.el-message-box').should('be.visible');
|
cy.get('.el-message-box').should('be.visible');
|
||||||
cy.get('.el-message-box').find('input').type(IMPORT_WORKFLOW_URL);
|
cy.get('.el-message-box').find('input').type(IMPORT_WORKFLOW_URL);
|
||||||
cy.get('body').type('{enter}');
|
cy.get('body').type('{enter}');
|
||||||
cy.waitForLoad();
|
cy.waitForLoad(false)
|
||||||
WorkflowPage.actions.zoomToFit();
|
WorkflowPage.actions.zoomToFit();
|
||||||
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
||||||
|
@ -104,7 +104,7 @@ describe('Workflow Actions', () => {
|
||||||
WorkflowPage.getters
|
WorkflowPage.getters
|
||||||
.workflowImportInput()
|
.workflowImportInput()
|
||||||
.selectFile('cypress/fixtures/Test_workflow-actions_paste-data.json', { force: true });
|
.selectFile('cypress/fixtures/Test_workflow-actions_paste-data.json', { force: true });
|
||||||
cy.waitForLoad();
|
cy.waitForLoad(false)
|
||||||
WorkflowPage.actions.zoomToFit();
|
WorkflowPage.actions.zoomToFit();
|
||||||
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
||||||
|
|
92
cypress/fixtures/Test_workflow_schema_test.json
Normal file
92
cypress/fixtures/Test_workflow_schema_test.json
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
{
|
||||||
|
"name": "My workflow 8",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"operation": "getAllPeople",
|
||||||
|
"limit": 10
|
||||||
|
},
|
||||||
|
"id": "39cd80ce-5a8f-4339-b3d5-c4af969dd330",
|
||||||
|
"name": "Customer Datastore (n8n training)",
|
||||||
|
"type": "n8n-nodes-base.n8nTrainingCustomerDatastore",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
940,
|
||||||
|
680
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"values": {
|
||||||
|
"number": [
|
||||||
|
{
|
||||||
|
"name": "objectValue.prop1",
|
||||||
|
"value": 123
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"string": [
|
||||||
|
{
|
||||||
|
"name": "objectValue.prop2",
|
||||||
|
"value": "someText"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"dotNotation": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "6e4490f6-ba95-4400-beec-2caefdd4895a",
|
||||||
|
"name": "Set",
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
1300,
|
||||||
|
680
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "58512a93-dabf-4584-817f-27c608c1bdd5",
|
||||||
|
"name": "When clicking \"Execute Workflow\"",
|
||||||
|
"type": "n8n-nodes-base.manualTrigger",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
720,
|
||||||
|
680
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pinData": {},
|
||||||
|
"connections": {
|
||||||
|
"Customer Datastore (n8n training)": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Set",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"When clicking \"Execute Workflow\"": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Customer Datastore (n8n training)",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": false,
|
||||||
|
"settings": {},
|
||||||
|
"versionId": "4a4f292a-92be-427c-848a-9582527f5ed3",
|
||||||
|
"id": "8",
|
||||||
|
"meta": {
|
||||||
|
"instanceId": "032eceae7493054b723340499be69ecbf4cbe28a7ec6df676b759000750b968d"
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
}
|
|
@ -24,7 +24,6 @@ export class NodeCreator extends BasePage {
|
||||||
};
|
};
|
||||||
actions = {
|
actions = {
|
||||||
openNodeCreator: () => {
|
openNodeCreator: () => {
|
||||||
cy.waitForLoad();
|
|
||||||
this.getters.plusButton().click();
|
this.getters.plusButton().click();
|
||||||
this.getters.nodeCreator().should('be.visible');
|
this.getters.nodeCreator().should('be.visible');
|
||||||
},
|
},
|
||||||
|
|
|
@ -13,10 +13,9 @@ export class NDV extends BasePage {
|
||||||
outputPanel: () => cy.getByTestId('output-panel'),
|
outputPanel: () => cy.getByTestId('output-panel'),
|
||||||
executingLoader: () => cy.getByTestId('ndv-executing'),
|
executingLoader: () => cy.getByTestId('ndv-executing'),
|
||||||
inputDataContainer: () => this.getters.inputPanel().findChildByTestId('ndv-data-container'),
|
inputDataContainer: () => this.getters.inputPanel().findChildByTestId('ndv-data-container'),
|
||||||
inputDisplayMode: () => this.getters.inputPanel().getByTestId('ndv-run-data-display-mode'),
|
inputDisplayMode: () => this.getters.inputPanel().findChildByTestId('ndv-run-data-display-mode').first(),
|
||||||
outputDataContainer: () => this.getters.outputPanel().findChildByTestId('ndv-data-container'),
|
outputDataContainer: () => this.getters.outputPanel().findChildByTestId('ndv-data-container'),
|
||||||
outputDisplayMode: () => this.getters.outputPanel().getByTestId('ndv-run-data-display-mode'),
|
outputDisplayMode: () => this.getters.outputPanel().findChildByTestId('ndv-run-data-display-mode').first(),
|
||||||
digital: () => cy.getByTestId('ndv-run-data-display-mode'),
|
|
||||||
pinDataButton: () => cy.getByTestId('ndv-pin-data'),
|
pinDataButton: () => cy.getByTestId('ndv-pin-data'),
|
||||||
editPinnedDataButton: () => cy.getByTestId('ndv-edit-pinned-data'),
|
editPinnedDataButton: () => cy.getByTestId('ndv-edit-pinned-data'),
|
||||||
pinnedDataEditor: () => this.getters.outputPanel().find('.monaco-editor[role=code]'),
|
pinnedDataEditor: () => this.getters.outputPanel().find('.monaco-editor[role=code]'),
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { SettingsSidebar } from './sidebar/settings-sidebar';
|
import { SettingsSidebar } from './sidebar/settings-sidebar';
|
||||||
import { MainSidebar } from './sidebar/main-sidebar';
|
import { MainSidebar } from './sidebar/main-sidebar';
|
||||||
import { WorkflowPage } from './workflow';
|
import { WorkflowPage } from './workflow';
|
||||||
|
import { WorkflowsPage } from './workflows';
|
||||||
import { BasePage } from './base';
|
import { BasePage } from './base';
|
||||||
|
|
||||||
const workflowPage = new WorkflowPage();
|
const workflowPage = new WorkflowPage();
|
||||||
|
const workflowsPage = new WorkflowsPage();
|
||||||
const mainSidebar = new MainSidebar();
|
const mainSidebar = new MainSidebar();
|
||||||
const settingsSidebar = new SettingsSidebar();
|
const settingsSidebar = new SettingsSidebar();
|
||||||
|
|
||||||
|
@ -39,7 +41,7 @@ export class SettingsUsersPage extends BasePage {
|
||||||
settingsSidebar.getters.menuItem('Users').should('not.exist');
|
settingsSidebar.getters.menuItem('Users').should('not.exist');
|
||||||
// Should be redirected to workflows page if trying to access UM url
|
// Should be redirected to workflows page if trying to access UM url
|
||||||
cy.visit('/settings/users');
|
cy.visit('/settings/users');
|
||||||
cy.url().should('match', new RegExp(workflowPage.url));
|
cy.url().should('match', new RegExp(workflowsPage.url));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
opedDeleteDialog: (email: string) => {
|
opedDeleteDialog: (email: string) => {
|
||||||
|
|
|
@ -174,6 +174,10 @@ export class WorkflowPage extends BasePage {
|
||||||
saveWorkflowUsingKeyboardShortcut: () => {
|
saveWorkflowUsingKeyboardShortcut: () => {
|
||||||
cy.get('body').type('{meta}', { release: false }).type('s');
|
cy.get('body').type('{meta}', { release: false }).type('s');
|
||||||
},
|
},
|
||||||
|
deleteNode: (name: string) => {
|
||||||
|
this.getters.canvasNodeByName(name).first().click();
|
||||||
|
cy.get('body').type('{del}');
|
||||||
|
},
|
||||||
setWorkflowName: (name: string) => {
|
setWorkflowName: (name: string) => {
|
||||||
this.getters.workflowNameInput().should('be.disabled');
|
this.getters.workflowNameInput().should('be.disabled');
|
||||||
this.getters.workflowNameInput().parent().click();
|
this.getters.workflowNameInput().parent().click();
|
||||||
|
|
|
@ -26,7 +26,6 @@
|
||||||
import 'cypress-real-events';
|
import 'cypress-real-events';
|
||||||
import { WorkflowsPage, SigninPage, SignupPage, SettingsUsersPage, WorkflowPage } from '../pages';
|
import { WorkflowsPage, SigninPage, SignupPage, SettingsUsersPage, WorkflowPage } from '../pages';
|
||||||
import { N8N_AUTH_COOKIE } from '../constants';
|
import { N8N_AUTH_COOKIE } from '../constants';
|
||||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
|
||||||
import { MessageBox } from '../pages/modals/message-box';
|
import { MessageBox } from '../pages/modals/message-box';
|
||||||
|
|
||||||
Cypress.Commands.add('getByTestId', (selector, ...args) => {
|
Cypress.Commands.add('getByTestId', (selector, ...args) => {
|
||||||
|
@ -34,15 +33,17 @@ Cypress.Commands.add('getByTestId', (selector, ...args) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add('createFixtureWorkflow', (fixtureKey, workflowName) => {
|
Cypress.Commands.add('createFixtureWorkflow', (fixtureKey, workflowName) => {
|
||||||
const WorkflowPage = new WorkflowPageClass();
|
const workflowPage = new WorkflowPage();
|
||||||
|
|
||||||
// We need to force the click because the input is hidden
|
// We need to force the click because the input is hidden
|
||||||
WorkflowPage.getters
|
workflowPage.getters
|
||||||
.workflowImportInput()
|
.workflowImportInput()
|
||||||
.selectFile(`cypress/fixtures/${fixtureKey}`, { force: true });
|
.selectFile(`cypress/fixtures/${fixtureKey}`, { force: true });
|
||||||
WorkflowPage.actions.setWorkflowName(workflowName);
|
|
||||||
|
|
||||||
WorkflowPage.getters.saveButton().should('contain', 'Saved');
|
cy.waitForLoad(false);
|
||||||
|
workflowPage.actions.setWorkflowName(workflowName);
|
||||||
|
|
||||||
|
workflowPage.getters.saveButton().should('contain', 'Saved');
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add(
|
Cypress.Commands.add(
|
||||||
|
@ -53,14 +54,20 @@ Cypress.Commands.add(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
Cypress.Commands.add('waitForLoad', () => {
|
Cypress.Commands.add('waitForLoad', (waitForIntercepts = true) => {
|
||||||
|
// These aliases are set-up before each test in cypress/support/e2e.ts
|
||||||
|
// we can't set them up here because at this point it would be too late
|
||||||
|
// and the requests would already have been made
|
||||||
|
if(waitForIntercepts) {
|
||||||
|
cy.wait(['@loadSettings', '@loadLogin'])
|
||||||
|
}
|
||||||
cy.getByTestId('node-view-loader', { timeout: 20000 }).should('not.exist');
|
cy.getByTestId('node-view-loader', { timeout: 20000 }).should('not.exist');
|
||||||
cy.get('.el-loading-mask', { timeout: 20000 }).should('not.exist');
|
cy.get('.el-loading-mask', { timeout: 20000 }).should('not.exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add('signin', ({ email, password }) => {
|
Cypress.Commands.add('signin', ({ email, password }) => {
|
||||||
const signinPage = new SigninPage();
|
const signinPage = new SigninPage();
|
||||||
const workflowPage = new WorkflowPage();
|
const workflowsPage = new WorkflowsPage();
|
||||||
|
|
||||||
cy.session(
|
cy.session(
|
||||||
[email, password],
|
[email, password],
|
||||||
|
@ -74,10 +81,7 @@ Cypress.Commands.add('signin', ({ email, password }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// we should be redirected to /workflows
|
// we should be redirected to /workflows
|
||||||
cy.visit(workflowPage.url);
|
cy.url().should('include', workflowsPage.url);
|
||||||
cy.url().should('include', workflowPage.url);
|
|
||||||
cy.intercept('GET', '/rest/workflows/new').as('loading');
|
|
||||||
cy.wait('@loading');
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
validate() {
|
validate() {
|
||||||
|
@ -212,7 +216,9 @@ Cypress.Commands.add('grantBrowserPermissions', (...permissions: string[]) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add('readClipboard', () => cy.window().then(win => win.navigator.clipboard.readText()))
|
Cypress.Commands.add('readClipboard', () =>
|
||||||
|
cy.window().then((win) => win.navigator.clipboard.readText()),
|
||||||
|
);
|
||||||
|
|
||||||
Cypress.Commands.add('paste', { prevSubject: true }, (selector, pastePayload) => {
|
Cypress.Commands.add('paste', { prevSubject: true }, (selector, pastePayload) => {
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Element/paste_event
|
// https://developer.mozilla.org/en-US/docs/Web/API/Element/paste_event
|
||||||
|
|
|
@ -14,28 +14,17 @@
|
||||||
// ***********************************************************
|
// ***********************************************************
|
||||||
|
|
||||||
import './commands';
|
import './commands';
|
||||||
import CustomNodeFixture from '../fixtures/Custom_node.json';
|
|
||||||
import CustomNodeWithN8nCredentialFixture from '../fixtures/Custom_node_n8n_credential.json';
|
|
||||||
import CustomNodeWithCustomCredentialFixture from '../fixtures/Custom_node_custom_credential.json';
|
|
||||||
import CustomCredential from '../fixtures/Custom_credential.json';
|
|
||||||
|
|
||||||
// Load custom nodes and credentials fixtures
|
// Load custom nodes and credentials fixtures
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.intercept('GET', '/types/nodes.json', (req) => {
|
cy.intercept('GET', '/rest/settings').as('loadSettings');
|
||||||
req.on('response', (res) => {
|
cy.intercept('GET', '/rest/login').as('loadLogin');
|
||||||
const nodes = res.body || [];
|
|
||||||
|
|
||||||
res.headers['cache-control'] = 'no-cache, no-store';
|
// Always intercept the request to test credentials and return a success
|
||||||
nodes.push(CustomNodeFixture, CustomNodeWithN8nCredentialFixture, CustomNodeWithCustomCredentialFixture);
|
cy.intercept('POST', '/rest/credentials/test', {
|
||||||
|
statusCode: 200,
|
||||||
|
body: {
|
||||||
|
data: { status: 'success', message: 'Tested successfully' },
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}).as('nodesIntercept');
|
|
||||||
|
|
||||||
cy.intercept('GET', '/types/credentials.json', (req) => {
|
|
||||||
req.on('response', (res) => {
|
|
||||||
const credentials = res.body || [];
|
|
||||||
|
|
||||||
res.headers['cache-control'] = 'no-cache, no-store';
|
|
||||||
credentials.push(CustomCredential);
|
|
||||||
})
|
|
||||||
}).as('credentialsIntercept');
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -43,7 +43,7 @@ declare global {
|
||||||
skipSetup(): void;
|
skipSetup(): void;
|
||||||
resetAll(): void;
|
resetAll(): void;
|
||||||
enableFeature(feature: string): void;
|
enableFeature(feature: string): void;
|
||||||
waitForLoad(): void;
|
waitForLoad(waitForIntercepts?: boolean): void;
|
||||||
grantBrowserPermissions(...permissions: string[]): void;
|
grantBrowserPermissions(...permissions: string[]): void;
|
||||||
readClipboard(): Chainable<string>;
|
readClipboard(): Chainable<string>;
|
||||||
paste(pastePayload: string): void;
|
paste(pastePayload: string): void;
|
||||||
|
|
|
@ -20,7 +20,7 @@ services:
|
||||||
- ${DATA_FOLDER}/letsencrypt:/letsencrypt
|
- ${DATA_FOLDER}/letsencrypt:/letsencrypt
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
n8n:
|
n8n:
|
||||||
image: n8nio/n8n
|
image: docker.n8n.io/n8nio/n8n
|
||||||
ports:
|
ports:
|
||||||
- '127.0.0.1:5678:5678'
|
- '127.0.0.1:5678:5678'
|
||||||
labels:
|
labels:
|
||||||
|
|
|
@ -23,7 +23,7 @@ services:
|
||||||
retries: 10
|
retries: 10
|
||||||
|
|
||||||
n8n:
|
n8n:
|
||||||
image: n8nio/n8n
|
image: docker.n8n.io/n8nio/n8n
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
- DB_TYPE=mariadb
|
- DB_TYPE=mariadb
|
||||||
|
|
|
@ -24,7 +24,7 @@ services:
|
||||||
retries: 10
|
retries: 10
|
||||||
|
|
||||||
n8n:
|
n8n:
|
||||||
image: n8nio/n8n
|
image: docker.n8n.io/n8nio/n8n
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
- DB_TYPE=postgresdb
|
- DB_TYPE=postgresdb
|
||||||
|
|
|
@ -63,14 +63,14 @@ services:
|
||||||
|
|
||||||
n8n:
|
n8n:
|
||||||
<<: *shared
|
<<: *shared
|
||||||
image: n8nio/n8n
|
image: docker.n8n.io/n8nio/n8n
|
||||||
command: /bin/sh -c "n8n start --tunnel"
|
command: /bin/sh -c "n8n start --tunnel"
|
||||||
ports:
|
ports:
|
||||||
- 5678:5678
|
- 5678:5678
|
||||||
|
|
||||||
n8n-worker:
|
n8n-worker:
|
||||||
<<: *shared
|
<<: *shared
|
||||||
image: n8nio/n8n
|
image: docker.n8n.io/n8nio/n8n
|
||||||
command: /bin/sh -c "sleep 5; n8n worker"
|
command: /bin/sh -c "sleep 5; n8n worker"
|
||||||
depends_on:
|
depends_on:
|
||||||
- n8n
|
- n8n
|
||||||
|
|
|
@ -3,7 +3,7 @@ ARG NODE_VERSION=16
|
||||||
# 1. Create an image to build n8n
|
# 1. Create an image to build n8n
|
||||||
FROM n8nio/base:${NODE_VERSION} as builder
|
FROM n8nio/base:${NODE_VERSION} as builder
|
||||||
|
|
||||||
COPY turbo.json package.json .npmrc pnpm-lock.yaml pnpm-workspace.yaml tsconfig.json ./
|
COPY turbo.json package.json .npmrc pnpm-lock.yaml pnpm-workspace.yaml jest.config.js tsconfig.json ./
|
||||||
COPY scripts ./scripts
|
COPY scripts ./scripts
|
||||||
COPY packages ./packages
|
COPY packages ./packages
|
||||||
COPY patches ./patches
|
COPY patches ./patches
|
||||||
|
|
|
@ -40,12 +40,12 @@ Additional information and example workflows on the n8n.io website: [https://n8n
|
||||||
|
|
||||||
## Start n8n in Docker
|
## Start n8n in Docker
|
||||||
|
|
||||||
```
|
```bash
|
||||||
docker run -it --rm \
|
docker run -it --rm \
|
||||||
--name n8n \
|
--name n8n \
|
||||||
-p 5678:5678 \
|
-p 5678:5678 \
|
||||||
-v ~/.n8n:/home/node/.n8n \
|
-v ~/.n8n:/home/node/.n8n \
|
||||||
n8nio/n8n
|
docker.n8n.io/n8nio/n8n
|
||||||
```
|
```
|
||||||
|
|
||||||
You can then access n8n by opening:
|
You can then access n8n by opening:
|
||||||
|
@ -62,12 +62,12 @@ n8n instance.
|
||||||
|
|
||||||
To use it simply start n8n with `--tunnel`
|
To use it simply start n8n with `--tunnel`
|
||||||
|
|
||||||
```
|
```bash
|
||||||
docker run -it --rm \
|
docker run -it --rm \
|
||||||
--name n8n \
|
--name n8n \
|
||||||
-p 5678:5678 \
|
-p 5678:5678 \
|
||||||
-v ~/.n8n:/home/node/.n8n \
|
-v ~/.n8n:/home/node/.n8n \
|
||||||
n8nio/n8n \
|
docker.n8n.io/n8nio/n8n \
|
||||||
n8n start --tunnel
|
n8n start --tunnel
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ to make sure that n8n is protected!
|
||||||
Right now we have very basic protection via basic-auth in place. It can be activated
|
Right now we have very basic protection via basic-auth in place. It can be activated
|
||||||
by setting the following environment variables:
|
by setting the following environment variables:
|
||||||
|
|
||||||
```
|
```text
|
||||||
N8N_BASIC_AUTH_ACTIVE=true
|
N8N_BASIC_AUTH_ACTIVE=true
|
||||||
N8N_BASIC_AUTH_USER=<USER>
|
N8N_BASIC_AUTH_USER=<USER>
|
||||||
N8N_BASIC_AUTH_PASSWORD=<PASSWORD>
|
N8N_BASIC_AUTH_PASSWORD=<PASSWORD>
|
||||||
|
@ -91,12 +91,12 @@ The workflow data gets by default saved in an SQLite database in the user
|
||||||
folder (`/home/node/.n8n`). That folder also additionally contains the
|
folder (`/home/node/.n8n`). That folder also additionally contains the
|
||||||
settings like webhook URL and encryption key.
|
settings like webhook URL and encryption key.
|
||||||
|
|
||||||
```
|
```bash
|
||||||
docker run -it --rm \
|
docker run -it --rm \
|
||||||
--name n8n \
|
--name n8n \
|
||||||
-p 5678:5678 \
|
-p 5678:5678 \
|
||||||
-v ~/.n8n:/home/node/.n8n \
|
-v ~/.n8n:/home/node/.n8n \
|
||||||
n8nio/n8n
|
docker.n8n.io/n8nio/n8n
|
||||||
```
|
```
|
||||||
|
|
||||||
### Start with other Database
|
### Start with other Database
|
||||||
|
@ -123,7 +123,7 @@ Replace the following placeholders with the actual data:
|
||||||
- POSTGRES_USER
|
- POSTGRES_USER
|
||||||
- POSTGRES_SCHEMA
|
- POSTGRES_SCHEMA
|
||||||
|
|
||||||
```
|
```bash
|
||||||
docker run -it --rm \
|
docker run -it --rm \
|
||||||
--name n8n \
|
--name n8n \
|
||||||
-p 5678:5678 \
|
-p 5678:5678 \
|
||||||
|
@ -135,7 +135,7 @@ docker run -it --rm \
|
||||||
-e DB_POSTGRESDB_SCHEMA=<POSTGRES_SCHEMA> \
|
-e DB_POSTGRESDB_SCHEMA=<POSTGRES_SCHEMA> \
|
||||||
-e DB_POSTGRESDB_PASSWORD=<POSTGRES_PASSWORD> \
|
-e DB_POSTGRESDB_PASSWORD=<POSTGRES_PASSWORD> \
|
||||||
-v ~/.n8n:/home/node/.n8n \
|
-v ~/.n8n:/home/node/.n8n \
|
||||||
n8nio/n8n \
|
docker.n8n.io/n8nio/n8n \
|
||||||
n8n start
|
n8n start
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -151,7 +151,7 @@ Replace the following placeholders with the actual data:
|
||||||
- MYSQLDB_PORT
|
- MYSQLDB_PORT
|
||||||
- MYSQLDB_USER
|
- MYSQLDB_USER
|
||||||
|
|
||||||
```
|
```bash
|
||||||
docker run -it --rm \
|
docker run -it --rm \
|
||||||
--name n8n \
|
--name n8n \
|
||||||
-p 5678:5678 \
|
-p 5678:5678 \
|
||||||
|
@ -162,7 +162,7 @@ docker run -it --rm \
|
||||||
-e DB_MYSQLDB_USER=<MYSQLDB_USER> \
|
-e DB_MYSQLDB_USER=<MYSQLDB_USER> \
|
||||||
-e DB_MYSQLDB_PASSWORD=<MYSQLDB_PASSWORD> \
|
-e DB_MYSQLDB_PASSWORD=<MYSQLDB_PASSWORD> \
|
||||||
-v ~/.n8n:/home/node/.n8n \
|
-v ~/.n8n:/home/node/.n8n \
|
||||||
n8nio/n8n \
|
docker.n8n.io/n8nio/n8n \
|
||||||
n8n start
|
n8n start
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -191,16 +191,21 @@ A basic step by step example setup of n8n with docker-compose and Lets Encrypt i
|
||||||
|
|
||||||
## Updating a running docker-compose instance
|
## Updating a running docker-compose instance
|
||||||
|
|
||||||
```
|
1. Pull the latest version from the registry
|
||||||
# Pull down the latest version from dockerhub
|
|
||||||
docker pull n8nio/n8n
|
`docker pull docker.n8n.io/n8nio/n8n`
|
||||||
# Stop current setup
|
|
||||||
sudo docker-compose stop
|
2. Stop the current setup
|
||||||
# Delete it (will only delete the docker-containers, data is stored separately)
|
|
||||||
sudo docker-compose rm
|
`sudo docker-compose stop`
|
||||||
# Then start it again
|
|
||||||
sudo docker-compose up -d
|
3. Delete it (will only delete the docker-containers, data is stored separately)
|
||||||
```
|
|
||||||
|
`sudo docker-compose rm`
|
||||||
|
|
||||||
|
4. Then start it again
|
||||||
|
|
||||||
|
`sudo docker-compose up -d`
|
||||||
|
|
||||||
## Setting Timezone
|
## Setting Timezone
|
||||||
|
|
||||||
|
@ -212,22 +217,22 @@ the environment variable `TZ`.
|
||||||
|
|
||||||
Example to use the same timezone for both:
|
Example to use the same timezone for both:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
docker run -it --rm \
|
docker run -it --rm \
|
||||||
--name n8n \
|
--name n8n \
|
||||||
-p 5678:5678 \
|
-p 5678:5678 \
|
||||||
-e GENERIC_TIMEZONE="Europe/Berlin" \
|
-e GENERIC_TIMEZONE="Europe/Berlin" \
|
||||||
-e TZ="Europe/Berlin" \
|
-e TZ="Europe/Berlin" \
|
||||||
n8nio/n8n
|
docker.n8n.io/n8nio/n8n
|
||||||
```
|
```
|
||||||
|
|
||||||
## Build Docker-Image
|
## Build Docker-Image
|
||||||
|
|
||||||
```
|
```bash
|
||||||
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 --build-arg N8N_VERSION=<VERSION> -t n8nio/n8n:<VERSION> .
|
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 --build-arg N8N_VERSION=<VERSION> -t n8n:<VERSION> .
|
||||||
|
|
||||||
# For example:
|
# For example:
|
||||||
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 --build-arg N8N_VERSION=0.114.0 -t n8nio/n8n:0.114.0 .
|
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 --build-arg N8N_VERSION=0.114.0 -t n8n:0.114.0 .
|
||||||
```
|
```
|
||||||
|
|
||||||
## What does n8n mean and how do you pronounce it?
|
## What does n8n mean and how do you pronounce it?
|
||||||
|
|
|
@ -22,11 +22,14 @@ const config = {
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'^@/(.*)$': '<rootDir>/src/$1',
|
'^@/(.*)$': '<rootDir>/src/$1',
|
||||||
},
|
},
|
||||||
|
collectCoverage: true,
|
||||||
|
coverageReporters: [process.env.COVERAGE_REPORT === 'true' ? 'text' : 'text-summary'],
|
||||||
|
collectCoverageFrom: ['src/**/*.ts'],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (process.env.CI === 'true') {
|
if (process.env.CI === 'true') {
|
||||||
config.maxWorkers = 2;
|
config.workerIdleMemoryLimit = 1024;
|
||||||
config.workerIdleMemoryLimit = 2048;
|
config.coverageReporters = ['cobertura'];
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
|
16
package.json
16
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n",
|
"name": "n8n",
|
||||||
"version": "0.218.0",
|
"version": "0.221.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "https://n8n.io",
|
"homepage": "https://n8n.io",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
@ -39,15 +39,15 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@n8n_io/eslint-config": "workspace:*",
|
"@n8n_io/eslint-config": "workspace:*",
|
||||||
"@ngneat/falso": "^6.1.0",
|
"@ngneat/falso": "^6.1.0",
|
||||||
"@types/jest": "^29.2.2",
|
"@types/jest": "^29.5.0",
|
||||||
"@types/supertest": "^2.0.12",
|
"@types/supertest": "^2.0.12",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"cypress": "^12.7.0",
|
"cypress": "^12.8.1",
|
||||||
"cypress-real-events": "^1.7.6",
|
"cypress-real-events": "^1.7.6",
|
||||||
"jest": "^29.4.2",
|
"jest": "^29.5.0",
|
||||||
"jest-environment-jsdom": "^29.4.2",
|
"jest-environment-jsdom": "^29.5.0",
|
||||||
"jest-mock": "^29.4.2",
|
"jest-mock": "^29.5.0",
|
||||||
"jest-mock-extended": "^3.0.1",
|
"jest-mock-extended": "^3.0.3",
|
||||||
"nock": "^13.2.9",
|
"nock": "^13.2.9",
|
||||||
"node-fetch": "^2.6.7",
|
"node-fetch": "^2.6.7",
|
||||||
"p-limit": "^3.1.0",
|
"p-limit": "^3.1.0",
|
||||||
|
@ -69,8 +69,10 @@
|
||||||
"@types/node": "^16.18.12",
|
"@types/node": "^16.18.12",
|
||||||
"browserslist": "^4.21.4",
|
"browserslist": "^4.21.4",
|
||||||
"chokidar": "3.5.2",
|
"chokidar": "3.5.2",
|
||||||
|
"decode-uri-component": "0.2.2",
|
||||||
"ejs": "^3.1.8",
|
"ejs": "^3.1.8",
|
||||||
"fork-ts-checker-webpack-plugin": "^6.0.4",
|
"fork-ts-checker-webpack-plugin": "^6.0.4",
|
||||||
|
"http-cache-semantics": "4.1.1",
|
||||||
"jsonwebtoken": "9.0.0",
|
"jsonwebtoken": "9.0.0",
|
||||||
"prettier": "^2.8.3",
|
"prettier": "^2.8.3",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
|
|
|
@ -58,7 +58,7 @@ To play around with n8n, you can also start it using Docker:
|
||||||
docker run -it --rm \
|
docker run -it --rm \
|
||||||
--name n8n \
|
--name n8n \
|
||||||
-p 5678:5678 \
|
-p 5678:5678 \
|
||||||
n8nio/n8n
|
docker.n8n.io/n8nio/n8n
|
||||||
```
|
```
|
||||||
|
|
||||||
Be aware that all the data will be lost once the Docker container gets removed. To persist the data mount the `~/.n8n` folder:
|
Be aware that all the data will be lost once the Docker container gets removed. To persist the data mount the `~/.n8n` folder:
|
||||||
|
@ -68,11 +68,9 @@ docker run -it --rm \
|
||||||
--name n8n \
|
--name n8n \
|
||||||
-p 5678:5678 \
|
-p 5678:5678 \
|
||||||
-v ~/.n8n:/home/node/.n8n \
|
-v ~/.n8n:/home/node/.n8n \
|
||||||
n8nio/n8n
|
docker.n8n.io/n8nio/n8n
|
||||||
```
|
```
|
||||||
|
|
||||||
n8n also offers a Docker image for Raspberry Pi: `n8nio/n8n:latest-rpi`.
|
|
||||||
|
|
||||||
Refer to the [documentation](https://github.com/n8n-io/n8n/blob/master/docker/images/n8n/README.md) for more information on the Docker setup.
|
Refer to the [documentation](https://github.com/n8n-io/n8n/blob/master/docker/images/n8n/README.md) for more information on the Docker setup.
|
||||||
|
|
||||||
### Install with npm
|
### Install with npm
|
||||||
|
|
|
@ -11,4 +11,5 @@ module.exports = {
|
||||||
'^@/(.*)$': '<rootDir>/src/$1',
|
'^@/(.*)$': '<rootDir>/src/$1',
|
||||||
'^@db/(.*)$': '<rootDir>/src/databases/$1',
|
'^@db/(.*)$': '<rootDir>/src/databases/$1',
|
||||||
},
|
},
|
||||||
|
coveragePathIgnorePatterns: ['/src/databases/migrations/'],
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n",
|
"name": "n8n",
|
||||||
"version": "0.218.0",
|
"version": "0.221.2",
|
||||||
"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",
|
||||||
|
@ -70,7 +70,7 @@
|
||||||
"@types/body-parser-xml": "^2.0.2",
|
"@types/body-parser-xml": "^2.0.2",
|
||||||
"@types/compression": "1.0.1",
|
"@types/compression": "1.0.1",
|
||||||
"@types/connect-history-api-fallback": "^1.3.1",
|
"@types/connect-history-api-fallback": "^1.3.1",
|
||||||
"@types/convict": "^4.2.1",
|
"@types/convict": "^6.1.1",
|
||||||
"@types/cookie-parser": "^1.4.2",
|
"@types/cookie-parser": "^1.4.2",
|
||||||
"@types/express": "^4.17.6",
|
"@types/express": "^4.17.6",
|
||||||
"@types/json-diff": "^0.5.1",
|
"@types/json-diff": "^0.5.1",
|
||||||
|
@ -114,7 +114,7 @@
|
||||||
"tsconfig-paths": "^4.1.2"
|
"tsconfig-paths": "^4.1.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@n8n_io/license-sdk": "^1.8.0",
|
"@n8n_io/license-sdk": "^1.9.1",
|
||||||
"@oclif/command": "^1.8.16",
|
"@oclif/command": "^1.8.16",
|
||||||
"@oclif/core": "^1.16.4",
|
"@oclif/core": "^1.16.4",
|
||||||
"@oclif/errors": "^1.3.6",
|
"@oclif/errors": "^1.3.6",
|
||||||
|
@ -134,13 +134,14 @@
|
||||||
"client-oauth2": "^4.2.5",
|
"client-oauth2": "^4.2.5",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"connect-history-api-fallback": "^1.6.0",
|
"connect-history-api-fallback": "^1.6.0",
|
||||||
"convict": "^6.0.1",
|
"convict": "^6.2.4",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"crypto-js": "~4.1.1",
|
"crypto-js": "~4.1.1",
|
||||||
"csrf": "^3.1.0",
|
"csrf": "^3.1.0",
|
||||||
"curlconverter": "^3.0.0",
|
"curlconverter": "^3.0.0",
|
||||||
"dotenv": "^8.0.0",
|
"dotenv": "^8.0.0",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"express-handlebars": "^7.0.2",
|
||||||
"express-async-errors": "^3.1.1",
|
"express-async-errors": "^3.1.1",
|
||||||
"express-openapi-validator": "^4.13.6",
|
"express-openapi-validator": "^4.13.6",
|
||||||
"express-prom-bundle": "^6.6.0",
|
"express-prom-bundle": "^6.6.0",
|
||||||
|
@ -169,7 +170,7 @@
|
||||||
"lodash.uniq": "^4.5.0",
|
"lodash.uniq": "^4.5.0",
|
||||||
"lodash.uniqby": "^4.7.0",
|
"lodash.uniqby": "^4.7.0",
|
||||||
"lodash.unset": "^4.5.2",
|
"lodash.unset": "^4.5.2",
|
||||||
"luxon": "^3.1.0",
|
"luxon": "^3.3.0",
|
||||||
"mysql2": "~2.3.3",
|
"mysql2": "~2.3.3",
|
||||||
"n8n-core": "workspace:*",
|
"n8n-core": "workspace:*",
|
||||||
"n8n-editor-ui": "workspace:*",
|
"n8n-editor-ui": "workspace:*",
|
||||||
|
@ -195,7 +196,7 @@
|
||||||
"semver": "^7.3.8",
|
"semver": "^7.3.8",
|
||||||
"shelljs": "^0.8.5",
|
"shelljs": "^0.8.5",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"sqlite3": "^5.1.4",
|
"sqlite3": "^5.1.6",
|
||||||
"sse-channel": "^4.0.0",
|
"sse-channel": "^4.0.0",
|
||||||
"swagger-ui-express": "^4.3.0",
|
"swagger-ui-express": "^4.3.0",
|
||||||
"syslog-client": "^1.1.1",
|
"syslog-client": "^1.1.1",
|
||||||
|
|
|
@ -302,7 +302,7 @@ export abstract class AbstractServer {
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
protected setupWaitingWebhookEndpoint() {
|
protected setupWaitingWebhookEndpoint() {
|
||||||
const endpoint = this.endpointWebhookWaiting;
|
const endpoint = this.endpointWebhookWaiting;
|
||||||
const waitingWebhooks = new WaitingWebhooks();
|
const waitingWebhooks = Container.get(WaitingWebhooks);
|
||||||
|
|
||||||
// Register all webhook-waiting requests
|
// Register all webhook-waiting requests
|
||||||
this.app.all(`/${endpoint}/*`, async (req, res) => {
|
this.app.all(`/${endpoint}/*`, async (req, res) => {
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
/* eslint-disable prefer-template */
|
|
||||||
/* eslint-disable @typescript-eslint/restrict-plus-operands */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
|
/* 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-unsafe-call */
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
|
@ -25,7 +22,7 @@ import type {
|
||||||
IWorkflowExecutionDataProcess,
|
IWorkflowExecutionDataProcess,
|
||||||
} from '@/Interfaces';
|
} from '@/Interfaces';
|
||||||
import * as ResponseHelper from '@/ResponseHelper';
|
import * as ResponseHelper from '@/ResponseHelper';
|
||||||
import * as WorkflowHelpers from '@/WorkflowHelpers';
|
import { isWorkflowIdValid } from '@/utils';
|
||||||
import { Service } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
|
@ -60,7 +57,7 @@ export class ActiveExecutions {
|
||||||
}
|
}
|
||||||
|
|
||||||
const workflowId = executionData.workflowData.id;
|
const workflowId = executionData.workflowData.id;
|
||||||
if (workflowId !== undefined && WorkflowHelpers.isWorkflowIdValid(workflowId)) {
|
if (workflowId !== undefined && isWorkflowIdValid(workflowId)) {
|
||||||
fullExecutionData.workflowId = workflowId;
|
fullExecutionData.workflowId = workflowId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
|
|
||||||
import { Container, Service } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
import { ActiveWorkflows, NodeExecuteFunctions } from 'n8n-core';
|
import { ActiveWorkflows, NodeExecuteFunctions } from 'n8n-core';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
@ -82,7 +82,11 @@ export class ActiveWorkflowRunner {
|
||||||
[key: string]: IQueuedWorkflowActivations;
|
[key: string]: IQueuedWorkflowActivations;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
constructor(private externalHooks: ExternalHooks) {}
|
constructor(
|
||||||
|
private activeExecutions: ActiveExecutions,
|
||||||
|
private externalHooks: ExternalHooks,
|
||||||
|
private nodeTypes: NodeTypes,
|
||||||
|
) {}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
async init() {
|
async init() {
|
||||||
|
@ -271,14 +275,13 @@ export class ActiveWorkflowRunner {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodeTypes = Container.get(NodeTypes);
|
|
||||||
const workflow = new Workflow({
|
const workflow = new Workflow({
|
||||||
id: webhook.workflowId,
|
id: webhook.workflowId,
|
||||||
name: workflowData.name,
|
name: workflowData.name,
|
||||||
nodes: workflowData.nodes,
|
nodes: workflowData.nodes,
|
||||||
connections: workflowData.connections,
|
connections: workflowData.connections,
|
||||||
active: workflowData.active,
|
active: workflowData.active,
|
||||||
nodeTypes,
|
nodeTypes: this.nodeTypes,
|
||||||
staticData: workflowData.staticData,
|
staticData: workflowData.staticData,
|
||||||
settings: workflowData.settings,
|
settings: workflowData.settings,
|
||||||
});
|
});
|
||||||
|
@ -514,14 +517,13 @@ export class ActiveWorkflowRunner {
|
||||||
throw new Error(`Could not find workflow with id "${workflowId}"`);
|
throw new Error(`Could not find workflow with id "${workflowId}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodeTypes = Container.get(NodeTypes);
|
|
||||||
const workflow = new Workflow({
|
const workflow = new Workflow({
|
||||||
id: workflowId,
|
id: workflowId,
|
||||||
name: workflowData.name,
|
name: workflowData.name,
|
||||||
nodes: workflowData.nodes,
|
nodes: workflowData.nodes,
|
||||||
connections: workflowData.connections,
|
connections: workflowData.connections,
|
||||||
active: workflowData.active,
|
active: workflowData.active,
|
||||||
nodeTypes,
|
nodeTypes: this.nodeTypes,
|
||||||
staticData: workflowData.staticData,
|
staticData: workflowData.staticData,
|
||||||
settings: workflowData.settings,
|
settings: workflowData.settings,
|
||||||
});
|
});
|
||||||
|
@ -638,7 +640,7 @@ export class ActiveWorkflowRunner {
|
||||||
|
|
||||||
if (donePromise) {
|
if (donePromise) {
|
||||||
executePromise.then((executionId) => {
|
executePromise.then((executionId) => {
|
||||||
Container.get(ActiveExecutions)
|
this.activeExecutions
|
||||||
.getPostExecutePromise(executionId)
|
.getPostExecutePromise(executionId)
|
||||||
.then(donePromise.resolve)
|
.then(donePromise.resolve)
|
||||||
.catch(donePromise.reject);
|
.catch(donePromise.reject);
|
||||||
|
@ -695,7 +697,7 @@ export class ActiveWorkflowRunner {
|
||||||
|
|
||||||
if (donePromise) {
|
if (donePromise) {
|
||||||
executePromise.then((executionId) => {
|
executePromise.then((executionId) => {
|
||||||
Container.get(ActiveExecutions)
|
this.activeExecutions
|
||||||
.getPostExecutePromise(executionId)
|
.getPostExecutePromise(executionId)
|
||||||
.then(donePromise.resolve)
|
.then(donePromise.resolve)
|
||||||
.catch(donePromise.reject);
|
.catch(donePromise.reject);
|
||||||
|
@ -782,14 +784,13 @@ export class ActiveWorkflowRunner {
|
||||||
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 = Container.get(NodeTypes);
|
|
||||||
workflowInstance = new Workflow({
|
workflowInstance = new Workflow({
|
||||||
id: workflowId,
|
id: workflowId,
|
||||||
name: workflowData.name,
|
name: workflowData.name,
|
||||||
nodes: workflowData.nodes,
|
nodes: workflowData.nodes,
|
||||||
connections: workflowData.connections,
|
connections: workflowData.connections,
|
||||||
active: workflowData.active,
|
active: workflowData.active,
|
||||||
nodeTypes,
|
nodeTypes: this.nodeTypes,
|
||||||
staticData: workflowData.staticData,
|
staticData: workflowData.staticData,
|
||||||
settings: workflowData.settings,
|
settings: workflowData.settings,
|
||||||
});
|
});
|
||||||
|
|
|
@ -207,7 +207,7 @@ export function hasPackageLoaded(packageName: string): boolean {
|
||||||
|
|
||||||
export function removePackageFromMissingList(packageName: string): void {
|
export function removePackageFromMissingList(packageName: string): void {
|
||||||
try {
|
try {
|
||||||
const failedPackages = (config.get('nodes.packagesMissing') as string).split(' ');
|
const failedPackages = config.get('nodes.packagesMissing').split(' ');
|
||||||
|
|
||||||
const packageFailedToLoad = failedPackages.filter(
|
const packageFailedToLoad = failedPackages.filter(
|
||||||
(packageNameAndVersion) =>
|
(packageNameAndVersion) =>
|
||||||
|
|
|
@ -393,8 +393,7 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expressionResolveValues) {
|
if (expressionResolveValues) {
|
||||||
const timezone =
|
const timezone = expressionResolveValues.workflow.settings.timezone ?? defaultTimezone;
|
||||||
(expressionResolveValues.workflow.settings.timezone as string) || defaultTimezone;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
decryptedData = expressionResolveValues.workflow.expression.getParameterValue(
|
decryptedData = expressionResolveValues.workflow.expression.getParameterValue(
|
||||||
|
@ -452,7 +451,6 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||||
type: string,
|
type: string,
|
||||||
data: ICredentialDataDecryptedObject,
|
data: ICredentialDataDecryptedObject,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// eslint-disable-next-line @typescript-eslint/await-thenable
|
|
||||||
const credentials = await this.getCredentials(nodeCredentials, type);
|
const credentials = await this.getCredentials(nodeCredentials, type);
|
||||||
|
|
||||||
if (!Db.isInitialized) {
|
if (!Db.isInitialized) {
|
||||||
|
|
|
@ -169,6 +169,8 @@ export async function init(
|
||||||
collections.InstalledPackages = linkRepository(entities.InstalledPackages);
|
collections.InstalledPackages = linkRepository(entities.InstalledPackages);
|
||||||
collections.InstalledNodes = linkRepository(entities.InstalledNodes);
|
collections.InstalledNodes = linkRepository(entities.InstalledNodes);
|
||||||
collections.WorkflowStatistics = linkRepository(entities.WorkflowStatistics);
|
collections.WorkflowStatistics = linkRepository(entities.WorkflowStatistics);
|
||||||
|
collections.ExecutionMetadata = linkRepository(entities.ExecutionMetadata);
|
||||||
|
|
||||||
collections.EventDestinations = linkRepository(entities.EventDestinations);
|
collections.EventDestinations = linkRepository(entities.EventDestinations);
|
||||||
|
|
||||||
isInitialized = true;
|
isInitialized = true;
|
||||||
|
|
|
@ -23,6 +23,7 @@ import type {
|
||||||
ExecutionStatus,
|
ExecutionStatus,
|
||||||
IExecutionsSummary,
|
IExecutionsSummary,
|
||||||
FeatureFlags,
|
FeatureFlags,
|
||||||
|
WorkflowSettings,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import type { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
|
import type { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
|
||||||
|
@ -48,6 +49,7 @@ import type { WebhookEntity } from '@db/entities/WebhookEntity';
|
||||||
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
||||||
import type { WorkflowStatistics } from '@db/entities/WorkflowStatistics';
|
import type { WorkflowStatistics } from '@db/entities/WorkflowStatistics';
|
||||||
import type { EventDestinations } from '@db/entities/MessageEventBusDestinationEntity';
|
import type { EventDestinations } from '@db/entities/MessageEventBusDestinationEntity';
|
||||||
|
import type { ExecutionMetadata } from './databases/entities/ExecutionMetadata';
|
||||||
|
|
||||||
export interface IActivationError {
|
export interface IActivationError {
|
||||||
time: number;
|
time: number;
|
||||||
|
@ -88,6 +90,7 @@ export interface IDatabaseCollections {
|
||||||
InstalledNodes: Repository<InstalledNodes>;
|
InstalledNodes: Repository<InstalledNodes>;
|
||||||
WorkflowStatistics: Repository<WorkflowStatistics>;
|
WorkflowStatistics: Repository<WorkflowStatistics>;
|
||||||
EventDestinations: Repository<EventDestinations>;
|
EventDestinations: Repository<EventDestinations>;
|
||||||
|
ExecutionMetadata: Repository<ExecutionMetadata>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
|
@ -453,16 +456,12 @@ export interface IVersionNotificationSettings {
|
||||||
export interface IN8nUISettings {
|
export interface IN8nUISettings {
|
||||||
endpointWebhook: string;
|
endpointWebhook: string;
|
||||||
endpointWebhookTest: string;
|
endpointWebhookTest: string;
|
||||||
saveDataErrorExecution: 'all' | 'none';
|
saveDataErrorExecution: WorkflowSettings.SaveDataExecution;
|
||||||
saveDataSuccessExecution: 'all' | 'none';
|
saveDataSuccessExecution: WorkflowSettings.SaveDataExecution;
|
||||||
saveManualExecutions: boolean;
|
saveManualExecutions: boolean;
|
||||||
executionTimeout: number;
|
executionTimeout: number;
|
||||||
maxExecutionTimeout: number;
|
maxExecutionTimeout: number;
|
||||||
workflowCallerPolicyDefaultOption:
|
workflowCallerPolicyDefaultOption: WorkflowSettings.CallerPolicy;
|
||||||
| 'any'
|
|
||||||
| 'none'
|
|
||||||
| 'workflowsFromAList'
|
|
||||||
| 'workflowsFromSameOwner';
|
|
||||||
oauthCallbackUrls: {
|
oauthCallbackUrls: {
|
||||||
oauth1: string;
|
oauth1: string;
|
||||||
oauth2: string;
|
oauth2: string;
|
||||||
|
|
|
@ -284,9 +284,19 @@ export class InternalHooks implements IInternalHooksClass {
|
||||||
properties.user_id = userId;
|
properties.user_id = userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
properties.success = !!runData?.finished;
|
||||||
|
|
||||||
|
let executionStatus: ExecutionStatus;
|
||||||
|
if (runData?.status === 'crashed') {
|
||||||
|
executionStatus = 'crashed';
|
||||||
|
} else if (runData?.status === 'waiting' || runData?.data?.waitTill) {
|
||||||
|
executionStatus = 'waiting';
|
||||||
|
} else {
|
||||||
|
executionStatus = properties.success ? 'success' : 'failed';
|
||||||
|
}
|
||||||
|
|
||||||
if (runData !== undefined) {
|
if (runData !== undefined) {
|
||||||
properties.execution_mode = runData.mode;
|
properties.execution_mode = runData.mode;
|
||||||
properties.success = !!runData.finished;
|
|
||||||
properties.is_manual = runData.mode === 'manual';
|
properties.is_manual = runData.mode === 'manual';
|
||||||
|
|
||||||
let nodeGraphResult: INodesGraphResult | null = null;
|
let nodeGraphResult: INodesGraphResult | null = null;
|
||||||
|
@ -342,7 +352,7 @@ export class InternalHooks implements IInternalHooksClass {
|
||||||
const manualExecEventProperties: ITelemetryTrackProperties = {
|
const manualExecEventProperties: ITelemetryTrackProperties = {
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
workflow_id: workflow.id,
|
workflow_id: workflow.id,
|
||||||
status: properties.success ? 'success' : 'failed',
|
status: executionStatus,
|
||||||
executionStatus: runData?.status ?? 'unknown',
|
executionStatus: runData?.status ?? 'unknown',
|
||||||
error_message: properties.error_message as string,
|
error_message: properties.error_message as string,
|
||||||
error_node_type: properties.error_node_type,
|
error_node_type: properties.error_node_type,
|
||||||
|
@ -392,15 +402,6 @@ export class InternalHooks implements IInternalHooksClass {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let executionStatus: ExecutionStatus;
|
|
||||||
if (runData?.status === 'crashed') {
|
|
||||||
executionStatus = 'crashed';
|
|
||||||
} else if (runData?.status === 'waiting' || runData?.data?.waitTill) {
|
|
||||||
executionStatus = 'waiting';
|
|
||||||
} else {
|
|
||||||
executionStatus = properties.success ? 'success' : 'failed';
|
|
||||||
}
|
|
||||||
|
|
||||||
promises.push(
|
promises.push(
|
||||||
Db.collections.Execution.update(executionId, {
|
Db.collections.Execution.update(executionId, {
|
||||||
status: executionStatus,
|
status: executionStatus,
|
||||||
|
|
|
@ -2,8 +2,6 @@ import type { LdapConfig } from './types';
|
||||||
|
|
||||||
export const LDAP_FEATURE_NAME = 'features.ldap';
|
export const LDAP_FEATURE_NAME = 'features.ldap';
|
||||||
|
|
||||||
export const LDAP_ENABLED = 'enterprise.features.ldap';
|
|
||||||
|
|
||||||
export const LDAP_LOGIN_LABEL = 'sso.ldap.loginLabel';
|
export const LDAP_LOGIN_LABEL = 'sso.ldap.loginLabel';
|
||||||
|
|
||||||
export const LDAP_LOGIN_ENABLED = 'sso.ldap.loginEnabled';
|
export const LDAP_LOGIN_ENABLED = 'sso.ldap.loginEnabled';
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { AES, enc } from 'crypto-js';
|
import { AES, enc } from 'crypto-js';
|
||||||
import type { Entry as LdapUser } from 'ldapts';
|
import type { Entry as LdapUser } from 'ldapts';
|
||||||
import { Filter } from 'ldapts/filters/Filter';
|
import { Filter } from 'ldapts/filters/Filter';
|
||||||
|
import { Container } from 'typedi';
|
||||||
import { UserSettings } from 'n8n-core';
|
import { UserSettings } from 'n8n-core';
|
||||||
import { validate } from 'jsonschema';
|
import { validate } from 'jsonschema';
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
|
@ -16,23 +17,26 @@ import { LdapManager } from './LdapManager.ee';
|
||||||
import {
|
import {
|
||||||
BINARY_AD_ATTRIBUTES,
|
BINARY_AD_ATTRIBUTES,
|
||||||
LDAP_CONFIG_SCHEMA,
|
LDAP_CONFIG_SCHEMA,
|
||||||
LDAP_ENABLED,
|
|
||||||
LDAP_FEATURE_NAME,
|
LDAP_FEATURE_NAME,
|
||||||
LDAP_LOGIN_ENABLED,
|
LDAP_LOGIN_ENABLED,
|
||||||
LDAP_LOGIN_LABEL,
|
LDAP_LOGIN_LABEL,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import type { ConnectionSecurity, LdapConfig } from './types';
|
import type { ConnectionSecurity, LdapConfig } from './types';
|
||||||
import { jsonParse, LoggerProxy as Logger } from 'n8n-workflow';
|
import { jsonParse, LoggerProxy as Logger } from 'n8n-workflow';
|
||||||
import { getLicense } from '@/License';
|
import { License } from '@/License';
|
||||||
import { Container } from 'typedi';
|
|
||||||
import { InternalHooks } from '@/InternalHooks';
|
import { InternalHooks } from '@/InternalHooks';
|
||||||
|
import {
|
||||||
|
isEmailCurrentAuthenticationMethod,
|
||||||
|
isLdapCurrentAuthenticationMethod,
|
||||||
|
setCurrentAuthenticationMethod,
|
||||||
|
} from '@/sso/ssoHelpers';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether the LDAP feature is disabled in the instance
|
* Check whether the LDAP feature is disabled in the instance
|
||||||
*/
|
*/
|
||||||
export const isLdapEnabled = (): boolean => {
|
export const isLdapEnabled = (): boolean => {
|
||||||
const license = getLicense();
|
const license = Container.get(License);
|
||||||
return isUserManagementEnabled() && (config.getEnv(LDAP_ENABLED) || license.isLdapEnabled());
|
return isUserManagementEnabled() && license.isLdapEnabled();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -50,8 +54,24 @@ export const setLdapLoginLabel = (value: string): void => {
|
||||||
/**
|
/**
|
||||||
* Set the LDAP login enabled to the configuration object
|
* Set the LDAP login enabled to the configuration object
|
||||||
*/
|
*/
|
||||||
export const setLdapLoginEnabled = (value: boolean): void => {
|
export const setLdapLoginEnabled = async (value: boolean): Promise<void> => {
|
||||||
config.set(LDAP_LOGIN_ENABLED, value);
|
if (config.get(LDAP_LOGIN_ENABLED) === value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// only one auth method can be active at a time, with email being the default
|
||||||
|
if (value && isEmailCurrentAuthenticationMethod()) {
|
||||||
|
// enable ldap login and disable email login, but only if email is the current auth method
|
||||||
|
config.set(LDAP_LOGIN_ENABLED, true);
|
||||||
|
await setCurrentAuthenticationMethod('ldap');
|
||||||
|
} else if (!value && isLdapCurrentAuthenticationMethod()) {
|
||||||
|
// disable ldap login, but only if ldap is the current auth method
|
||||||
|
config.set(LDAP_LOGIN_ENABLED, false);
|
||||||
|
await setCurrentAuthenticationMethod('email');
|
||||||
|
} else {
|
||||||
|
Logger.warn(
|
||||||
|
'Cannot switch LDAP login enabled state when an authentication method other than email is active',
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -126,8 +146,8 @@ export const getLdapConfig = async (): Promise<LdapConfig> => {
|
||||||
/**
|
/**
|
||||||
* Take the LDAP configuration and set login enabled and login label to the config object
|
* Take the LDAP configuration and set login enabled and login label to the config object
|
||||||
*/
|
*/
|
||||||
export const setGlobalLdapConfigVariables = (ldapConfig: LdapConfig): void => {
|
export const setGlobalLdapConfigVariables = async (ldapConfig: LdapConfig): Promise<void> => {
|
||||||
setLdapLoginEnabled(ldapConfig.loginEnabled);
|
await setLdapLoginEnabled(ldapConfig.loginEnabled);
|
||||||
setLdapLoginLabel(ldapConfig.loginLabel);
|
setLdapLoginLabel(ldapConfig.loginLabel);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -175,7 +195,7 @@ export const updateLdapConfig = async (ldapConfig: LdapConfig): Promise<void> =>
|
||||||
{ key: LDAP_FEATURE_NAME },
|
{ key: LDAP_FEATURE_NAME },
|
||||||
{ value: JSON.stringify(ldapConfig), loadOnStartup: true },
|
{ value: JSON.stringify(ldapConfig), loadOnStartup: true },
|
||||||
);
|
);
|
||||||
setGlobalLdapConfigVariables(ldapConfig);
|
await setGlobalLdapConfigVariables(ldapConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -197,7 +217,7 @@ export const handleLdapInit = async (): Promise<void> => {
|
||||||
|
|
||||||
const ldapConfig = await getLdapConfig();
|
const ldapConfig = await getLdapConfig();
|
||||||
|
|
||||||
setGlobalLdapConfigVariables(ldapConfig);
|
await setGlobalLdapConfigVariables(ldapConfig);
|
||||||
|
|
||||||
// init LDAP manager with the current
|
// init LDAP manager with the current
|
||||||
// configuration
|
// configuration
|
||||||
|
|
|
@ -1,77 +0,0 @@
|
||||||
import express from 'express';
|
|
||||||
import { LdapManager } from '../LdapManager.ee';
|
|
||||||
import { getLdapConfig, getLdapSynchronizations, updateLdapConfig } from '../helpers';
|
|
||||||
import type { LdapConfiguration } from '../types';
|
|
||||||
import pick from 'lodash.pick';
|
|
||||||
import { NON_SENSIBLE_LDAP_CONFIG_PROPERTIES } from '../constants';
|
|
||||||
import { InternalHooks } from '@/InternalHooks';
|
|
||||||
import { Container } from 'typedi';
|
|
||||||
|
|
||||||
export const ldapController = express.Router();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /ldap/config
|
|
||||||
*/
|
|
||||||
ldapController.get('/config', async (req: express.Request, res: express.Response) => {
|
|
||||||
const data = await getLdapConfig();
|
|
||||||
return res.status(200).json({ data });
|
|
||||||
});
|
|
||||||
/**
|
|
||||||
* POST /ldap/test-connection
|
|
||||||
*/
|
|
||||||
ldapController.post('/test-connection', async (req: express.Request, res: express.Response) => {
|
|
||||||
try {
|
|
||||||
await LdapManager.getInstance().service.testConnection();
|
|
||||||
} catch (error) {
|
|
||||||
const errorObject = error as { message: string };
|
|
||||||
return res.status(400).json({ message: errorObject.message });
|
|
||||||
}
|
|
||||||
return res.status(200).json();
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PUT /ldap/config
|
|
||||||
*/
|
|
||||||
ldapController.put('/config', async (req: LdapConfiguration.Update, res: express.Response) => {
|
|
||||||
try {
|
|
||||||
await updateLdapConfig(req.body);
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Error) {
|
|
||||||
return res.status(400).json({ message: e.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await getLdapConfig();
|
|
||||||
|
|
||||||
void Container.get(InternalHooks).onUserUpdatedLdapSettings({
|
|
||||||
user_id: req.user.id,
|
|
||||||
...pick(data, NON_SENSIBLE_LDAP_CONFIG_PROPERTIES),
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).json({ data });
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /ldap/sync
|
|
||||||
*/
|
|
||||||
ldapController.post('/sync', async (req: LdapConfiguration.Sync, res: express.Response) => {
|
|
||||||
const runType = req.body.type;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await LdapManager.getInstance().sync.run(runType);
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Error) {
|
|
||||||
return res.status(400).json({ message: e.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res.status(200).json({});
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /ldap/sync
|
|
||||||
*/
|
|
||||||
ldapController.get('/sync', async (req: LdapConfiguration.GetSync, res: express.Response) => {
|
|
||||||
const { page = '0', perPage = '20' } = req.query;
|
|
||||||
const data = await getLdapSynchronizations(parseInt(page, 10), parseInt(perPage, 10));
|
|
||||||
return res.status(200).json({ data });
|
|
||||||
});
|
|
|
@ -5,8 +5,14 @@ import { getLogger } from './Logger';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
import { LICENSE_FEATURES, N8N_VERSION, SETTINGS_LICENSE_CERT_KEY } from './constants';
|
import { LICENSE_FEATURES, N8N_VERSION, SETTINGS_LICENSE_CERT_KEY } from './constants';
|
||||||
|
import { Service } from 'typedi';
|
||||||
|
|
||||||
async function loadCertStr(): Promise<TLicenseContainerStr> {
|
async function loadCertStr(): Promise<TLicenseContainerStr> {
|
||||||
|
// if we have an ephemeral license, we don't want to load it from the database
|
||||||
|
const ephemeralLicense = config.get('license.cert');
|
||||||
|
if (ephemeralLicense) {
|
||||||
|
return ephemeralLicense;
|
||||||
|
}
|
||||||
const databaseSettings = await Db.collections.Settings.findOne({
|
const databaseSettings = await Db.collections.Settings.findOne({
|
||||||
where: {
|
where: {
|
||||||
key: SETTINGS_LICENSE_CERT_KEY,
|
key: SETTINGS_LICENSE_CERT_KEY,
|
||||||
|
@ -17,6 +23,8 @@ async function loadCertStr(): Promise<TLicenseContainerStr> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveCertStr(value: TLicenseContainerStr): Promise<void> {
|
async function saveCertStr(value: TLicenseContainerStr): Promise<void> {
|
||||||
|
// if we have an ephemeral license, we don't want to save it to the database
|
||||||
|
if (config.get('license.cert')) return;
|
||||||
await Db.collections.Settings.upsert(
|
await Db.collections.Settings.upsert(
|
||||||
{
|
{
|
||||||
key: SETTINGS_LICENSE_CERT_KEY,
|
key: SETTINGS_LICENSE_CERT_KEY,
|
||||||
|
@ -27,6 +35,7 @@ async function saveCertStr(value: TLicenseContainerStr): Promise<void> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Service()
|
||||||
export class License {
|
export class License {
|
||||||
private logger: ILogger;
|
private logger: ILogger;
|
||||||
|
|
||||||
|
@ -160,13 +169,3 @@ export class License {
|
||||||
return (this.getFeatureValue('planName') ?? 'Community') as string;
|
return (this.getFeatureValue('planName') ?? 'Community') as string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let licenseInstance: License | undefined;
|
|
||||||
|
|
||||||
export function getLicense(): License {
|
|
||||||
if (licenseInstance === undefined) {
|
|
||||||
licenseInstance = new License();
|
|
||||||
}
|
|
||||||
|
|
||||||
return licenseInstance;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import uniq from 'lodash.uniq';
|
import uniq from 'lodash.uniq';
|
||||||
|
import glob from 'fast-glob';
|
||||||
import type { DirectoryLoader, Types } from 'n8n-core';
|
import type { DirectoryLoader, Types } from 'n8n-core';
|
||||||
import {
|
import {
|
||||||
CUSTOM_EXTENSION_ENV,
|
CUSTOM_EXTENSION_ENV,
|
||||||
|
@ -18,18 +19,18 @@ import type {
|
||||||
import { LoggerProxy, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
|
import { LoggerProxy, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
|
||||||
|
|
||||||
import { createWriteStream } from 'fs';
|
import { createWriteStream } from 'fs';
|
||||||
import { access as fsAccess, mkdir, readdir as fsReaddir, stat as fsStat } from 'fs/promises';
|
import { mkdir } from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import type { InstalledPackages } from '@db/entities/InstalledPackages';
|
import type { InstalledPackages } from '@db/entities/InstalledPackages';
|
||||||
import { executeCommand } from '@/CommunityNodes/helpers';
|
import { executeCommand } from '@/CommunityNodes/helpers';
|
||||||
import {
|
import {
|
||||||
CLI_DIR,
|
|
||||||
GENERATED_STATIC_DIR,
|
GENERATED_STATIC_DIR,
|
||||||
RESPONSE_ERROR_MESSAGES,
|
RESPONSE_ERROR_MESSAGES,
|
||||||
CUSTOM_API_CALL_KEY,
|
CUSTOM_API_CALL_KEY,
|
||||||
CUSTOM_API_CALL_NAME,
|
CUSTOM_API_CALL_NAME,
|
||||||
inTest,
|
inTest,
|
||||||
|
CLI_DIR,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
|
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
|
||||||
import { Service } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
|
@ -52,6 +53,8 @@ export class LoadNodesAndCredentials implements INodesAndCredentials {
|
||||||
|
|
||||||
logger: ILogger;
|
logger: ILogger;
|
||||||
|
|
||||||
|
private downloadFolder: string;
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
// Make sure the imported modules can resolve dependencies fine.
|
// Make sure the imported modules can resolve dependencies fine.
|
||||||
const delimiter = process.platform === 'win32' ? ';' : ':';
|
const delimiter = process.platform === 'win32' ? ';' : ':';
|
||||||
|
@ -61,8 +64,13 @@ export class LoadNodesAndCredentials implements INodesAndCredentials {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
if (!inTest) module.constructor._initPaths();
|
if (!inTest) module.constructor._initPaths();
|
||||||
|
|
||||||
await this.loadNodesFromBasePackages();
|
this.downloadFolder = UserSettings.getUserN8nFolderDownloadedNodesPath();
|
||||||
await this.loadNodesFromDownloadedPackages();
|
|
||||||
|
// Load nodes from `n8n-nodes-base` and any other `n8n-nodes-*` package in the main `node_modules`
|
||||||
|
await this.loadNodesFromNodeModules(CLI_DIR);
|
||||||
|
// Load nodes from installed community packages
|
||||||
|
await this.loadNodesFromNodeModules(this.downloadFolder);
|
||||||
|
|
||||||
await this.loadNodesFromCustomDirectories();
|
await this.loadNodesFromCustomDirectories();
|
||||||
await this.postProcessLoaders();
|
await this.postProcessLoaders();
|
||||||
this.injectCustomApiCallOptions();
|
this.injectCustomApiCallOptions();
|
||||||
|
@ -109,32 +117,20 @@ export class LoadNodesAndCredentials implements INodesAndCredentials {
|
||||||
await writeStaticJSON('credentials', this.types.credentials);
|
await writeStaticJSON('credentials', this.types.credentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadNodesFromBasePackages() {
|
private async loadNodesFromNodeModules(scanDir: string): Promise<void> {
|
||||||
const nodeModulesPath = await this.getNodeModulesPath();
|
const nodeModulesDir = path.join(scanDir, 'node_modules');
|
||||||
const nodePackagePaths = await this.getN8nNodePackages(nodeModulesPath);
|
const globOptions = { cwd: nodeModulesDir, onlyDirectories: true };
|
||||||
|
const installedPackagePaths = [
|
||||||
|
...(await glob('n8n-nodes-*', { ...globOptions, deep: 1 })),
|
||||||
|
...(await glob('@*/n8n-nodes-*', { ...globOptions, deep: 2 })),
|
||||||
|
];
|
||||||
|
|
||||||
for (const packagePath of nodePackagePaths) {
|
for (const packagePath of installedPackagePaths) {
|
||||||
await this.runDirectoryLoader(LazyPackageDirectoryLoader, packagePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async loadNodesFromDownloadedPackages(): Promise<void> {
|
|
||||||
const nodePackages = [];
|
|
||||||
try {
|
try {
|
||||||
// Read downloaded nodes and credentials
|
await this.runDirectoryLoader(
|
||||||
const downloadedNodesDir = UserSettings.getUserN8nFolderDownloadedNodesPath();
|
LazyPackageDirectoryLoader,
|
||||||
const downloadedNodesDirModules = path.join(downloadedNodesDir, 'node_modules');
|
path.join(nodeModulesDir, packagePath),
|
||||||
await fsAccess(downloadedNodesDirModules);
|
);
|
||||||
const downloadedPackages = await this.getN8nNodePackages(downloadedNodesDirModules);
|
|
||||||
nodePackages.push(...downloadedPackages);
|
|
||||||
} catch (error) {
|
|
||||||
// Folder does not exist so ignore and return
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const packagePath of nodePackages) {
|
|
||||||
try {
|
|
||||||
await this.runDirectoryLoader(PackageDirectoryLoader, packagePath);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorReporter.error(error);
|
ErrorReporter.error(error);
|
||||||
}
|
}
|
||||||
|
@ -158,49 +154,45 @@ export class LoadNodesAndCredentials implements INodesAndCredentials {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private async installOrUpdateNpmModule(
|
||||||
* Returns all the names of the packages which could contain n8n nodes
|
packageName: string,
|
||||||
*/
|
options: { version?: string } | { installedPackage: InstalledPackages },
|
||||||
private async getN8nNodePackages(baseModulesPath: string): Promise<string[]> {
|
) {
|
||||||
const getN8nNodePackagesRecursive = async (relativePath: string): Promise<string[]> => {
|
const isUpdate = 'installedPackage' in options;
|
||||||
const results: string[] = [];
|
const command = isUpdate
|
||||||
const nodeModulesPath = `${baseModulesPath}/${relativePath}`;
|
? `npm update ${packageName}`
|
||||||
const nodeModules = await fsReaddir(nodeModulesPath);
|
: `npm install ${packageName}${options.version ? `@${options.version}` : ''}`;
|
||||||
for (const nodeModule of nodeModules) {
|
|
||||||
const isN8nNodesPackage = nodeModule.indexOf('n8n-nodes-') === 0;
|
|
||||||
const isNpmScopedPackage = nodeModule.indexOf('@') === 0;
|
|
||||||
if (!isN8nNodesPackage && !isNpmScopedPackage) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!(await fsStat(nodeModulesPath)).isDirectory()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (isN8nNodesPackage) {
|
|
||||||
results.push(`${baseModulesPath}/${relativePath}${nodeModule}`);
|
|
||||||
}
|
|
||||||
if (isNpmScopedPackage) {
|
|
||||||
results.push(...(await getN8nNodePackagesRecursive(`${relativePath}${nodeModule}/`)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
};
|
|
||||||
return getN8nNodePackagesRecursive('');
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadNpmModule(packageName: string, version?: string): Promise<InstalledPackages> {
|
|
||||||
const downloadFolder = UserSettings.getUserN8nFolderDownloadedNodesPath();
|
|
||||||
const command = `npm install ${packageName}${version ? `@${version}` : ''}`;
|
|
||||||
|
|
||||||
|
try {
|
||||||
await executeCommand(command);
|
await executeCommand(command);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.message === RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND) {
|
||||||
|
throw new Error(`The npm package "${packageName}" could not be found.`);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
const finalNodeUnpackedPath = path.join(downloadFolder, 'node_modules', packageName);
|
const finalNodeUnpackedPath = path.join(this.downloadFolder, 'node_modules', packageName);
|
||||||
|
|
||||||
const loader = await this.runDirectoryLoader(PackageDirectoryLoader, finalNodeUnpackedPath);
|
let loader: PackageDirectoryLoader;
|
||||||
|
try {
|
||||||
|
loader = await this.runDirectoryLoader(PackageDirectoryLoader, finalNodeUnpackedPath);
|
||||||
|
} catch (error) {
|
||||||
|
// Remove this package since loading it failed
|
||||||
|
const removeCommand = `npm remove ${packageName}`;
|
||||||
|
try {
|
||||||
|
await executeCommand(removeCommand);
|
||||||
|
} catch {}
|
||||||
|
throw new Error(RESPONSE_ERROR_MESSAGES.PACKAGE_LOADING_FAILED, { cause: error });
|
||||||
|
}
|
||||||
|
|
||||||
if (loader.loadedNodes.length > 0) {
|
if (loader.loadedNodes.length > 0) {
|
||||||
// Save info to DB
|
// Save info to DB
|
||||||
try {
|
try {
|
||||||
const { persistInstalledPackageData } = await import('@/CommunityNodes/packageModel');
|
const { persistInstalledPackageData, removePackageFromDatabase } = await import(
|
||||||
|
'@/CommunityNodes/packageModel'
|
||||||
|
);
|
||||||
|
if (isUpdate) await removePackageFromDatabase(options.installedPackage);
|
||||||
const installedPackage = await persistInstalledPackageData(loader);
|
const installedPackage = await persistInstalledPackageData(loader);
|
||||||
await this.postProcessLoaders();
|
await this.postProcessLoaders();
|
||||||
await this.generateTypesForFrontend();
|
await this.generateTypesForFrontend();
|
||||||
|
@ -223,6 +215,10 @@ export class LoadNodesAndCredentials implements INodesAndCredentials {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async installNpmModule(packageName: string, version?: string): Promise<InstalledPackages> {
|
||||||
|
return this.installOrUpdateNpmModule(packageName, { version });
|
||||||
|
}
|
||||||
|
|
||||||
async removeNpmModule(packageName: string, installedPackage: InstalledPackages): Promise<void> {
|
async removeNpmModule(packageName: string, installedPackage: InstalledPackages): Promise<void> {
|
||||||
const command = `npm remove ${packageName}`;
|
const command = `npm remove ${packageName}`;
|
||||||
|
|
||||||
|
@ -244,49 +240,7 @@ export class LoadNodesAndCredentials implements INodesAndCredentials {
|
||||||
packageName: string,
|
packageName: string,
|
||||||
installedPackage: InstalledPackages,
|
installedPackage: InstalledPackages,
|
||||||
): Promise<InstalledPackages> {
|
): Promise<InstalledPackages> {
|
||||||
const downloadFolder = UserSettings.getUserN8nFolderDownloadedNodesPath();
|
return this.installOrUpdateNpmModule(packageName, { installedPackage });
|
||||||
|
|
||||||
const command = `npm i ${packageName}@latest`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await executeCommand(command);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error && error.message === RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND) {
|
|
||||||
throw new Error(`The npm package "${packageName}" could not be found.`);
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalNodeUnpackedPath = path.join(downloadFolder, 'node_modules', packageName);
|
|
||||||
|
|
||||||
const loader = await this.runDirectoryLoader(PackageDirectoryLoader, finalNodeUnpackedPath);
|
|
||||||
|
|
||||||
if (loader.loadedNodes.length > 0) {
|
|
||||||
// Save info to DB
|
|
||||||
try {
|
|
||||||
const { persistInstalledPackageData, removePackageFromDatabase } = await import(
|
|
||||||
'@/CommunityNodes/packageModel'
|
|
||||||
);
|
|
||||||
await removePackageFromDatabase(installedPackage);
|
|
||||||
const newlyInstalledPackage = await persistInstalledPackageData(loader);
|
|
||||||
await this.postProcessLoaders();
|
|
||||||
await this.generateTypesForFrontend();
|
|
||||||
return newlyInstalledPackage;
|
|
||||||
} catch (error) {
|
|
||||||
LoggerProxy.error('Failed to save installed packages and nodes', {
|
|
||||||
error: error as Error,
|
|
||||||
packageName,
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Remove this package since it contains no loadable nodes
|
|
||||||
const removeCommand = `npm remove ${packageName}`;
|
|
||||||
try {
|
|
||||||
await executeCommand(removeCommand);
|
|
||||||
} catch {}
|
|
||||||
throw new Error(RESPONSE_ERROR_MESSAGES.PACKAGE_DOES_NOT_CONTAIN_NODES);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -399,27 +353,4 @@ export class LoadNodesAndCredentials implements INodesAndCredentials {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getNodeModulesPath(): Promise<string> {
|
|
||||||
// Get the path to the node-modules folder to be later able
|
|
||||||
// to load the credentials and nodes
|
|
||||||
const checkPaths = [
|
|
||||||
// In case "n8n" package is in same node_modules folder.
|
|
||||||
path.join(CLI_DIR, '..', 'n8n-workflow'),
|
|
||||||
// In case "n8n" package is the root and the packages are
|
|
||||||
// in the "node_modules" folder underneath it.
|
|
||||||
path.join(CLI_DIR, 'node_modules', 'n8n-workflow'),
|
|
||||||
// In case "n8n" package is installed using npm/yarn workspaces
|
|
||||||
// the node_modules folder is in the root of the workspace.
|
|
||||||
path.join(CLI_DIR, '..', '..', 'node_modules', 'n8n-workflow'),
|
|
||||||
];
|
|
||||||
for (const checkPath of checkPaths) {
|
|
||||||
try {
|
|
||||||
await fsAccess(checkPath);
|
|
||||||
// Folder exists, so use it.
|
|
||||||
return path.dirname(checkPath);
|
|
||||||
} catch {} // Folder does not exist so get next one
|
|
||||||
}
|
|
||||||
throw new Error('Could not find "node_modules" folder!');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
6
packages/cli/src/PublicApi/types.d.ts
vendored
6
packages/cli/src/PublicApi/types.d.ts
vendored
|
@ -7,7 +7,7 @@ import type { Role } from '@db/entities/Role';
|
||||||
|
|
||||||
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
||||||
|
|
||||||
import type * as UserManagementMailer from '@/UserManagement/email/UserManagementMailer';
|
import type { UserManagementMailer } from '@/UserManagement/email';
|
||||||
|
|
||||||
import type { Risk } from '@/audit/types';
|
import type { Risk } from '@/audit/types';
|
||||||
|
|
||||||
|
@ -26,10 +26,10 @@ export type AuthenticatedRequest<
|
||||||
> = express.Request<RouteParams, ResponseBody, RequestBody, RequestQuery> & {
|
> = express.Request<RouteParams, ResponseBody, RequestBody, RequestQuery> & {
|
||||||
user: User;
|
user: User;
|
||||||
globalMemberRole?: Role;
|
globalMemberRole?: Role;
|
||||||
mailer?: UserManagementMailer.UserManagementMailer;
|
mailer?: UserManagementMailer;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PaginatatedRequest = AuthenticatedRequest<
|
export type PaginatedRequest = AuthenticatedRequest<
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type express from 'express';
|
import type express from 'express';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import type { FindManyOptions, FindOptionsWhere } from 'typeorm';
|
import type { FindOptionsWhere } from 'typeorm';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
|
|
||||||
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
|
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
|
||||||
|
@ -20,12 +20,11 @@ import {
|
||||||
updateWorkflow,
|
updateWorkflow,
|
||||||
hasStartNode,
|
hasStartNode,
|
||||||
getStartNode,
|
getStartNode,
|
||||||
getWorkflows,
|
|
||||||
getSharedWorkflows,
|
getSharedWorkflows,
|
||||||
getWorkflowsCount,
|
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
getWorkflowIdsViaTags,
|
getWorkflowIdsViaTags,
|
||||||
parseTagNames,
|
parseTagNames,
|
||||||
|
getWorkflowsAndCount,
|
||||||
} from './workflows.service';
|
} from './workflows.service';
|
||||||
import { WorkflowsService } from '@/workflows/workflows.services';
|
import { WorkflowsService } from '@/workflows/workflows.services';
|
||||||
import { InternalHooks } from '@/InternalHooks';
|
import { InternalHooks } from '@/InternalHooks';
|
||||||
|
@ -98,28 +97,15 @@ export = {
|
||||||
async (req: WorkflowRequest.GetAll, res: express.Response): Promise<express.Response> => {
|
async (req: WorkflowRequest.GetAll, res: express.Response): Promise<express.Response> => {
|
||||||
const { offset = 0, limit = 100, active = undefined, tags = undefined } = req.query;
|
const { offset = 0, limit = 100, active = undefined, tags = undefined } = req.query;
|
||||||
|
|
||||||
let workflows: WorkflowEntity[];
|
|
||||||
let count: number;
|
|
||||||
|
|
||||||
const where: FindOptionsWhere<WorkflowEntity> = {
|
const where: FindOptionsWhere<WorkflowEntity> = {
|
||||||
...(active !== undefined && { active }),
|
...(active !== undefined && { active }),
|
||||||
};
|
};
|
||||||
const query: FindManyOptions<WorkflowEntity> = {
|
|
||||||
skip: offset,
|
|
||||||
take: limit,
|
|
||||||
where,
|
|
||||||
...(!config.getEnv('workflowTagsDisabled') && { relations: ['tags'] }),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isInstanceOwner(req.user)) {
|
if (isInstanceOwner(req.user)) {
|
||||||
if (tags) {
|
if (tags) {
|
||||||
const workflowIds = await getWorkflowIdsViaTags(parseTagNames(tags));
|
const workflowIds = await getWorkflowIdsViaTags(parseTagNames(tags));
|
||||||
Object.assign(where, { id: In(workflowIds) });
|
where.id = In(workflowIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
workflows = await getWorkflows(query);
|
|
||||||
|
|
||||||
count = await getWorkflowsCount(query);
|
|
||||||
} else {
|
} else {
|
||||||
const options: { workflowIds?: string[] } = {};
|
const options: { workflowIds?: string[] } = {};
|
||||||
|
|
||||||
|
@ -137,14 +123,16 @@ export = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const workflowsIds = sharedWorkflows.map((shareWorkflow) => shareWorkflow.workflowId);
|
const workflowsIds = sharedWorkflows.map((shareWorkflow) => shareWorkflow.workflowId);
|
||||||
|
where.id = In(workflowsIds);
|
||||||
Object.assign(where, { id: In(workflowsIds) });
|
|
||||||
|
|
||||||
workflows = await getWorkflows(query);
|
|
||||||
|
|
||||||
count = await getWorkflowsCount(query);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [workflows, count] = await getWorkflowsAndCount({
|
||||||
|
skip: offset,
|
||||||
|
take: limit,
|
||||||
|
where,
|
||||||
|
...(!config.getEnv('workflowTagsDisabled') && { relations: ['tags'] }),
|
||||||
|
});
|
||||||
|
|
||||||
void Container.get(InternalHooks).onUserRetrievedAllWorkflows({
|
void Container.get(InternalHooks).onUserRetrievedAllWorkflows({
|
||||||
user_id: req.user.id,
|
user_id: req.user.id,
|
||||||
public_api: true,
|
public_api: true,
|
||||||
|
|
|
@ -109,14 +109,10 @@ export async function deleteWorkflow(workflow: WorkflowEntity): Promise<Workflow
|
||||||
return Db.collections.Workflow.remove(workflow);
|
return Db.collections.Workflow.remove(workflow);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getWorkflows(
|
export async function getWorkflowsAndCount(
|
||||||
options: FindManyOptions<WorkflowEntity>,
|
options: FindManyOptions<WorkflowEntity>,
|
||||||
): Promise<WorkflowEntity[]> {
|
): Promise<[WorkflowEntity[], number]> {
|
||||||
return Db.collections.Workflow.find(options);
|
return Db.collections.Workflow.findAndCount(options);
|
||||||
}
|
|
||||||
|
|
||||||
export async function getWorkflowsCount(options: FindManyOptions<WorkflowEntity>): Promise<number> {
|
|
||||||
return Db.collections.Workflow.count(options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateWorkflow(
|
export async function updateWorkflow(
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import type express from 'express';
|
import type express from 'express';
|
||||||
|
|
||||||
import type { AuthenticatedRequest, PaginatatedRequest } from '../../../types';
|
import type { AuthenticatedRequest, PaginatedRequest } from '../../../types';
|
||||||
import { decodeCursor } from '../services/pagination.service';
|
import { decodeCursor } from '../services/pagination.service';
|
||||||
|
|
||||||
export const authorize =
|
export const authorize =
|
||||||
|
@ -22,7 +22,7 @@ export const authorize =
|
||||||
};
|
};
|
||||||
|
|
||||||
export const validCursor = (
|
export const validCursor = (
|
||||||
req: PaginatatedRequest,
|
req: PaginatedRequest,
|
||||||
res: express.Response,
|
res: express.Response,
|
||||||
next: express.NextFunction,
|
next: express.NextFunction,
|
||||||
): express.Response | void => {
|
): express.Response | void => {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import type Bull from 'bull';
|
import type Bull from 'bull';
|
||||||
import type { RedisOptions } from 'ioredis';
|
import type { RedisOptions } from 'ioredis';
|
||||||
|
import { Service } from 'typedi';
|
||||||
import type { IExecuteResponsePromiseData } from 'n8n-workflow';
|
import type { IExecuteResponsePromiseData } from 'n8n-workflow';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import { ActiveExecutions } from '@/ActiveExecutions';
|
import { ActiveExecutions } from '@/ActiveExecutions';
|
||||||
import * as WebhookHelpers from '@/WebhookHelpers';
|
import * as WebhookHelpers from '@/WebhookHelpers';
|
||||||
import { Container } from 'typedi';
|
|
||||||
|
|
||||||
export type JobId = Bull.JobId;
|
export type JobId = Bull.JobId;
|
||||||
export type Job = Bull.Job<JobData>;
|
export type Job = Bull.Job<JobData>;
|
||||||
|
@ -24,6 +24,7 @@ export interface WebhookResponse {
|
||||||
response: IExecuteResponsePromiseData;
|
response: IExecuteResponsePromiseData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Service()
|
||||||
export class Queue {
|
export class Queue {
|
||||||
private jobQueue: JobQueue;
|
private jobQueue: JobQueue;
|
||||||
|
|
||||||
|
@ -91,14 +92,3 @@ export class Queue {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let activeQueueInstance: Queue | undefined;
|
|
||||||
|
|
||||||
export async function getInstance(): Promise<Queue> {
|
|
||||||
if (activeQueueInstance === undefined) {
|
|
||||||
activeQueueInstance = new Queue(Container.get(ActiveExecutions));
|
|
||||||
await activeQueueInstance.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
return activeQueueInstance;
|
|
||||||
}
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { createHmac } from 'crypto';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
|
import { engine as expressHandlebars } from 'express-handlebars';
|
||||||
import type { ServeStaticOptions } from 'serve-static';
|
import type { ServeStaticOptions } from 'serve-static';
|
||||||
import type { FindManyOptions } from 'typeorm';
|
import type { FindManyOptions } from 'typeorm';
|
||||||
import { Not, In } from 'typeorm';
|
import { Not, In } from 'typeorm';
|
||||||
|
@ -56,10 +57,9 @@ import timezones from 'google-timezones-json';
|
||||||
import history from 'connect-history-api-fallback';
|
import history from 'connect-history-api-fallback';
|
||||||
|
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import * as Queue from '@/Queue';
|
import { Queue } from '@/Queue';
|
||||||
import { getSharedWorkflowIds } from '@/WorkflowHelpers';
|
import { getSharedWorkflowIds } from '@/WorkflowHelpers';
|
||||||
|
|
||||||
import { nodesController } from '@/api/nodes.api';
|
|
||||||
import { workflowsController } from '@/workflows/workflows.controller';
|
import { workflowsController } from '@/workflows/workflows.controller';
|
||||||
import {
|
import {
|
||||||
EDITOR_UI_DIST_DIR,
|
EDITOR_UI_DIST_DIR,
|
||||||
|
@ -83,16 +83,18 @@ import type {
|
||||||
import { registerController } from '@/decorators';
|
import { registerController } from '@/decorators';
|
||||||
import {
|
import {
|
||||||
AuthController,
|
AuthController,
|
||||||
|
LdapController,
|
||||||
MeController,
|
MeController,
|
||||||
|
NodesController,
|
||||||
|
NodeTypesController,
|
||||||
OwnerController,
|
OwnerController,
|
||||||
PasswordResetController,
|
PasswordResetController,
|
||||||
|
TagsController,
|
||||||
TranslationController,
|
TranslationController,
|
||||||
UsersController,
|
UsersController,
|
||||||
} from '@/controllers';
|
} from '@/controllers';
|
||||||
|
|
||||||
import { executionsController } from '@/executions/executions.controller';
|
import { executionsController } from '@/executions/executions.controller';
|
||||||
import { nodeTypesController } from '@/api/nodeTypes.api';
|
|
||||||
import { tagsController } from '@/api/tags.api';
|
|
||||||
import { workflowStatsController } from '@/api/workflowStats.api';
|
import { workflowStatsController } from '@/api/workflowStats.api';
|
||||||
import { loadPublicApiVersions } from '@/PublicApi';
|
import { loadPublicApiVersions } from '@/PublicApi';
|
||||||
import {
|
import {
|
||||||
|
@ -102,7 +104,7 @@ import {
|
||||||
isUserManagementEnabled,
|
isUserManagementEnabled,
|
||||||
whereClause,
|
whereClause,
|
||||||
} from '@/UserManagement/UserManagementHelper';
|
} from '@/UserManagement/UserManagementHelper';
|
||||||
import { getInstance as getMailerInstance } from '@/UserManagement/email';
|
import { UserManagementMailer } from '@/UserManagement/email';
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
import type {
|
import type {
|
||||||
ICredentialsDb,
|
ICredentialsDb,
|
||||||
|
@ -127,15 +129,18 @@ import { WaitTracker } from '@/WaitTracker';
|
||||||
import * as WebhookHelpers from '@/WebhookHelpers';
|
import * as WebhookHelpers from '@/WebhookHelpers';
|
||||||
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
|
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
|
||||||
import { toHttpNodeParameters } from '@/CurlConverterHelper';
|
import { toHttpNodeParameters } from '@/CurlConverterHelper';
|
||||||
import { eventBusRouter } from '@/eventbus/eventBusRoutes';
|
import { EventBusController } from '@/eventbus/eventBus.controller';
|
||||||
import { isLogStreamingEnabled } from '@/eventbus/MessageEventBus/MessageEventBusHelper';
|
import { isLogStreamingEnabled } from '@/eventbus/MessageEventBus/MessageEventBusHelper';
|
||||||
import { getLicense } from '@/License';
|
|
||||||
import { licenseController } from './license/license.controller';
|
import { licenseController } from './license/license.controller';
|
||||||
import { Push, setupPushServer, setupPushHandler } from '@/push';
|
import { Push, setupPushServer, setupPushHandler } from '@/push';
|
||||||
import { setupAuthMiddlewares } from './middlewares';
|
import { setupAuthMiddlewares } from './middlewares';
|
||||||
import { initEvents } from './events';
|
import { initEvents } from './events';
|
||||||
import { ldapController } from './Ldap/routes/ldap.controller.ee';
|
import {
|
||||||
import { getLdapLoginLabel, isLdapEnabled, isLdapLoginEnabled } from './Ldap/helpers';
|
getLdapLoginLabel,
|
||||||
|
handleLdapInit,
|
||||||
|
isLdapEnabled,
|
||||||
|
isLdapLoginEnabled,
|
||||||
|
} from './Ldap/helpers';
|
||||||
import { AbstractServer } from './AbstractServer';
|
import { AbstractServer } from './AbstractServer';
|
||||||
import { configureMetrics } from './metrics';
|
import { configureMetrics } from './metrics';
|
||||||
import { setupBasicAuth } from './middlewares/basicAuth';
|
import { setupBasicAuth } from './middlewares/basicAuth';
|
||||||
|
@ -149,9 +154,9 @@ import {
|
||||||
isAdvancedExecutionFiltersEnabled,
|
isAdvancedExecutionFiltersEnabled,
|
||||||
} from './executions/executionHelpers';
|
} from './executions/executionHelpers';
|
||||||
import { getSamlLoginLabel, isSamlLoginEnabled, isSamlLicensed } from './sso/saml/samlHelpers';
|
import { getSamlLoginLabel, isSamlLoginEnabled, isSamlLicensed } from './sso/saml/samlHelpers';
|
||||||
import { samlControllerPublic } from './sso/saml/routes/saml.controller.public.ee';
|
import { SamlController } from './sso/saml/routes/saml.controller.ee';
|
||||||
import { SamlService } from './sso/saml/saml.service.ee';
|
import { SamlService } from './sso/saml/saml.service.ee';
|
||||||
import { samlControllerProtected } from './sso/saml/routes/saml.controller.protected.ee';
|
import { LdapManager } from './Ldap/LdapManager.ee';
|
||||||
|
|
||||||
const exec = promisify(callbackExec);
|
const exec = promisify(callbackExec);
|
||||||
|
|
||||||
|
@ -179,6 +184,10 @@ class Server extends AbstractServer {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
this.app.engine('handlebars', expressHandlebars({ defaultLayout: false }));
|
||||||
|
this.app.set('view engine', 'handlebars');
|
||||||
|
this.app.set('views', TEMPLATES_DIR);
|
||||||
|
|
||||||
this.loadNodesAndCredentials = Container.get(LoadNodesAndCredentials);
|
this.loadNodesAndCredentials = Container.get(LoadNodesAndCredentials);
|
||||||
this.credentialTypes = Container.get(CredentialTypes);
|
this.credentialTypes = Container.get(CredentialTypes);
|
||||||
this.nodeTypes = Container.get(NodeTypes);
|
this.nodeTypes = Container.get(NodeTypes);
|
||||||
|
@ -302,8 +311,8 @@ class Server extends AbstractServer {
|
||||||
sharing: false,
|
sharing: false,
|
||||||
ldap: false,
|
ldap: false,
|
||||||
saml: false,
|
saml: false,
|
||||||
logStreaming: config.getEnv('enterprise.features.logStreaming'),
|
logStreaming: false,
|
||||||
advancedExecutionFilters: config.getEnv('enterprise.features.advancedExecutionFilters'),
|
advancedExecutionFilters: false,
|
||||||
},
|
},
|
||||||
hideUsagePage: config.getEnv('hideUsagePage'),
|
hideUsagePage: config.getEnv('hideUsagePage'),
|
||||||
license: {
|
license: {
|
||||||
|
@ -356,35 +365,32 @@ class Server extends AbstractServer {
|
||||||
return this.frontendSettings;
|
return this.frontendSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
async initLicense(): Promise<void> {
|
|
||||||
const license = getLicense();
|
|
||||||
await license.init(this.frontendSettings.instanceId);
|
|
||||||
|
|
||||||
const activationKey = config.getEnv('license.activationKey');
|
|
||||||
if (activationKey) {
|
|
||||||
try {
|
|
||||||
await license.activate(activationKey);
|
|
||||||
} catch (e) {
|
|
||||||
LoggerProxy.error('Could not activate license', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private registerControllers(ignoredEndpoints: Readonly<string[]>) {
|
private registerControllers(ignoredEndpoints: Readonly<string[]>) {
|
||||||
const { app, externalHooks, activeWorkflowRunner } = this;
|
const { app, externalHooks, activeWorkflowRunner, nodeTypes } = this;
|
||||||
const repositories = Db.collections;
|
const repositories = Db.collections;
|
||||||
setupAuthMiddlewares(app, ignoredEndpoints, this.restEndpoint, repositories.User);
|
setupAuthMiddlewares(app, ignoredEndpoints, this.restEndpoint, repositories.User);
|
||||||
|
|
||||||
const logger = LoggerProxy;
|
const logger = LoggerProxy;
|
||||||
const internalHooks = Container.get(InternalHooks);
|
const internalHooks = Container.get(InternalHooks);
|
||||||
const mailer = getMailerInstance();
|
const mailer = Container.get(UserManagementMailer);
|
||||||
const postHog = this.postHog;
|
const postHog = this.postHog;
|
||||||
|
const samlService = Container.get(SamlService);
|
||||||
|
|
||||||
const controllers = [
|
const controllers: object[] = [
|
||||||
|
new EventBusController(),
|
||||||
new AuthController({ config, internalHooks, repositories, logger, postHog }),
|
new AuthController({ config, internalHooks, repositories, logger, postHog }),
|
||||||
new OwnerController({ config, internalHooks, repositories, logger }),
|
new OwnerController({ config, internalHooks, repositories, logger }),
|
||||||
new MeController({ externalHooks, internalHooks, repositories, logger }),
|
new MeController({ externalHooks, internalHooks, repositories, logger }),
|
||||||
new PasswordResetController({ config, externalHooks, internalHooks, repositories, logger }),
|
new NodeTypesController({ config, nodeTypes }),
|
||||||
|
new PasswordResetController({
|
||||||
|
config,
|
||||||
|
externalHooks,
|
||||||
|
internalHooks,
|
||||||
|
mailer,
|
||||||
|
repositories,
|
||||||
|
logger,
|
||||||
|
}),
|
||||||
|
new TagsController({ config, repositories, externalHooks }),
|
||||||
new TranslationController(config, this.credentialTypes),
|
new TranslationController(config, this.credentialTypes),
|
||||||
new UsersController({
|
new UsersController({
|
||||||
config,
|
config,
|
||||||
|
@ -396,7 +402,20 @@ class Server extends AbstractServer {
|
||||||
logger,
|
logger,
|
||||||
postHog,
|
postHog,
|
||||||
}),
|
}),
|
||||||
|
new SamlController(samlService),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (isLdapEnabled()) {
|
||||||
|
const { service, sync } = LdapManager.getInstance();
|
||||||
|
controllers.push(new LdapController(service, sync, internalHooks));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.getEnv('nodes.communityPackages.enabled')) {
|
||||||
|
controllers.push(
|
||||||
|
new NodesController(config, this.loadNodesAndCredentials, this.push, internalHooks),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
controllers.forEach((controller) => registerController(app, config, controller));
|
controllers.forEach((controller) => registerController(app, config, controller));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -413,7 +432,6 @@ class Server extends AbstractServer {
|
||||||
|
|
||||||
await this.externalHooks.run('frontend.settings', [this.frontendSettings]);
|
await this.externalHooks.run('frontend.settings', [this.frontendSettings]);
|
||||||
|
|
||||||
await this.initLicense();
|
|
||||||
await this.postHog.init(this.frontendSettings.instanceId);
|
await this.postHog.init(this.frontendSettings.instanceId);
|
||||||
|
|
||||||
const publicApiEndpoint = config.getEnv('publicApi.path');
|
const publicApiEndpoint = config.getEnv('publicApi.path');
|
||||||
|
@ -423,8 +441,6 @@ class Server extends AbstractServer {
|
||||||
'assets',
|
'assets',
|
||||||
'healthz',
|
'healthz',
|
||||||
'metrics',
|
'metrics',
|
||||||
'icons',
|
|
||||||
'types',
|
|
||||||
'e2e',
|
'e2e',
|
||||||
this.endpointWebhook,
|
this.endpointWebhook,
|
||||||
this.endpointWebhookTest,
|
this.endpointWebhookTest,
|
||||||
|
@ -475,20 +491,16 @@ class Server extends AbstractServer {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// ----------------------------------------
|
if (config.getEnv('executions.mode') === 'queue') {
|
||||||
// User Management
|
await Container.get(Queue).init();
|
||||||
// ----------------------------------------
|
}
|
||||||
|
|
||||||
|
await handleLdapInit();
|
||||||
|
|
||||||
this.registerControllers(ignoredEndpoints);
|
this.registerControllers(ignoredEndpoints);
|
||||||
|
|
||||||
this.app.use(`/${this.restEndpoint}/credentials`, credentialsController);
|
this.app.use(`/${this.restEndpoint}/credentials`, credentialsController);
|
||||||
|
|
||||||
// ----------------------------------------
|
|
||||||
// Packages and nodes management
|
|
||||||
// ----------------------------------------
|
|
||||||
if (config.getEnv('nodes.communityPackages.enabled')) {
|
|
||||||
this.app.use(`/${this.restEndpoint}/nodes`, nodesController);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
// Workflow
|
// Workflow
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
@ -504,18 +516,6 @@ class Server extends AbstractServer {
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
this.app.use(`/${this.restEndpoint}/workflow-stats`, workflowStatsController);
|
this.app.use(`/${this.restEndpoint}/workflow-stats`, workflowStatsController);
|
||||||
|
|
||||||
// ----------------------------------------
|
|
||||||
// Tags
|
|
||||||
// ----------------------------------------
|
|
||||||
this.app.use(`/${this.restEndpoint}/tags`, tagsController);
|
|
||||||
|
|
||||||
// ----------------------------------------
|
|
||||||
// LDAP
|
|
||||||
// ----------------------------------------
|
|
||||||
if (isLdapEnabled()) {
|
|
||||||
this.app.use(`/${this.restEndpoint}/ldap`, ldapController);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
// SAML
|
// SAML
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
@ -524,17 +524,13 @@ class Server extends AbstractServer {
|
||||||
// set up the initial environment
|
// set up the initial environment
|
||||||
if (isSamlLicensed()) {
|
if (isSamlLicensed()) {
|
||||||
try {
|
try {
|
||||||
await SamlService.getInstance().init();
|
await Container.get(SamlService).init();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
LoggerProxy.error(`SAML initialization failed: ${error.message}`);
|
LoggerProxy.error(`SAML initialization failed: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.app.use(`/${this.restEndpoint}/sso/saml`, samlControllerPublic);
|
|
||||||
this.app.use(`/${this.restEndpoint}/sso/saml`, samlControllerProtected);
|
|
||||||
|
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
|
||||||
// Returns parameter values which normally get loaded from an external API or
|
// Returns parameter values which normally get loaded from an external API or
|
||||||
// get generated dynamically
|
// get generated dynamically
|
||||||
this.app.get(
|
this.app.get(
|
||||||
|
@ -645,12 +641,6 @@ class Server extends AbstractServer {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// ----------------------------------------
|
|
||||||
// Node-Types
|
|
||||||
// ----------------------------------------
|
|
||||||
|
|
||||||
this.app.use(`/${this.restEndpoint}/node-types`, nodeTypesController);
|
|
||||||
|
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
// Active Workflows
|
// Active Workflows
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
@ -978,7 +968,7 @@ class Server extends AbstractServer {
|
||||||
ResponseHelper.send(
|
ResponseHelper.send(
|
||||||
async (req: ExecutionRequest.GetAllCurrent): Promise<IExecutionsSummary[]> => {
|
async (req: ExecutionRequest.GetAllCurrent): Promise<IExecutionsSummary[]> => {
|
||||||
if (config.getEnv('executions.mode') === 'queue') {
|
if (config.getEnv('executions.mode') === 'queue') {
|
||||||
const queue = await Queue.getInstance();
|
const queue = Container.get(Queue);
|
||||||
const currentJobs = await queue.getJobs(['active', 'waiting']);
|
const currentJobs = await queue.getJobs(['active', 'waiting']);
|
||||||
|
|
||||||
const currentlyRunningQueueIds = currentJobs.map((job) => job.data.executionId);
|
const currentlyRunningQueueIds = currentJobs.map((job) => job.data.executionId);
|
||||||
|
@ -1121,7 +1111,7 @@ class Server extends AbstractServer {
|
||||||
} as IExecutionsStopData;
|
} as IExecutionsStopData;
|
||||||
}
|
}
|
||||||
|
|
||||||
const queue = await Queue.getInstance();
|
const queue = Container.get(Queue);
|
||||||
const currentJobs = await queue.getJobs(['active', 'waiting']);
|
const currentJobs = await queue.getJobs(['active', 'waiting']);
|
||||||
|
|
||||||
const job = currentJobs.find((job) => job.data.executionId === req.params.id);
|
const job = currentJobs.find((job) => job.data.executionId === req.params.id);
|
||||||
|
@ -1240,8 +1230,6 @@ class Server extends AbstractServer {
|
||||||
if (!eventBus.isInitialized) {
|
if (!eventBus.isInitialized) {
|
||||||
await eventBus.initialize();
|
await eventBus.initialize();
|
||||||
}
|
}
|
||||||
// add Event Bus REST endpoints
|
|
||||||
this.app.use(`/${this.restEndpoint}/eventbus`, eventBusRouter);
|
|
||||||
|
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
// Webhooks
|
// Webhooks
|
||||||
|
|
|
@ -116,7 +116,7 @@ export class PermissionChecker {
|
||||||
if (parentWorkflowId === undefined) {
|
if (parentWorkflowId === undefined) {
|
||||||
throw errorToThrow;
|
throw errorToThrow;
|
||||||
}
|
}
|
||||||
const allowedCallerIds = (subworkflow.settings.callerIds as string | undefined)
|
const allowedCallerIds = subworkflow.settings.callerIds
|
||||||
?.split(',')
|
?.split(',')
|
||||||
.map((id) => id.trim())
|
.map((id) => id.trim())
|
||||||
.filter((id) => id !== '');
|
.filter((id) => id !== '');
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import type express from 'express';
|
import type express from 'express';
|
||||||
import { compare, genSaltSync, hash } from 'bcryptjs';
|
import { compare, genSaltSync, hash } from 'bcryptjs';
|
||||||
|
import Container from 'typedi';
|
||||||
|
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
import * as ResponseHelper from '@/ResponseHelper';
|
import * as ResponseHelper from '@/ResponseHelper';
|
||||||
|
@ -13,7 +14,7 @@ import type { Role } from '@db/entities/Role';
|
||||||
import type { AuthenticatedRequest } from '@/requests';
|
import type { AuthenticatedRequest } from '@/requests';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import { getWebhookBaseUrl } from '@/WebhookHelpers';
|
import { getWebhookBaseUrl } from '@/WebhookHelpers';
|
||||||
import { getLicense } from '@/License';
|
import { License } from '@/License';
|
||||||
import { RoleService } from '@/role/role.service';
|
import { RoleService } from '@/role/role.service';
|
||||||
import type { PostHogClient } from '@/posthog';
|
import type { PostHogClient } from '@/posthog';
|
||||||
|
|
||||||
|
@ -55,11 +56,8 @@ export function isUserManagementEnabled(): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isSharingEnabled(): boolean {
|
export function isSharingEnabled(): boolean {
|
||||||
const license = getLicense();
|
const license = Container.get(License);
|
||||||
return (
|
return isUserManagementEnabled() && license.isSharingEnabled();
|
||||||
isUserManagementEnabled() &&
|
|
||||||
(config.getEnv('enterprise.features.sharing') || license.isSharingEnabled())
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getRoleId(scope: Role['scope'], name: Role['name']): Promise<Role['id']> {
|
export async function getRoleId(scope: Role['scope'], name: Role['name']): Promise<Role['id']> {
|
||||||
|
|
|
@ -1,9 +1,3 @@
|
||||||
export interface UserManagementMailerImplementation {
|
|
||||||
init: () => Promise<void>;
|
|
||||||
sendMail: (mailData: MailData) => Promise<SendEmailResult>;
|
|
||||||
verifyConnection: () => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type InviteEmailData = {
|
export type InviteEmailData = {
|
||||||
email: string;
|
email: string;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
|
|
|
@ -3,9 +3,9 @@ import type { Transporter } from 'nodemailer';
|
||||||
import { createTransport } from 'nodemailer';
|
import { createTransport } from 'nodemailer';
|
||||||
import { ErrorReporterProxy as ErrorReporter, LoggerProxy as Logger } from 'n8n-workflow';
|
import { ErrorReporterProxy as ErrorReporter, LoggerProxy as Logger } from 'n8n-workflow';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import type { MailData, SendEmailResult, UserManagementMailerImplementation } from './Interfaces';
|
import type { MailData, SendEmailResult } from './Interfaces';
|
||||||
|
|
||||||
export class NodeMailer implements UserManagementMailerImplementation {
|
export class NodeMailer {
|
||||||
private transport?: Transporter;
|
private transport?: Transporter;
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
|
|
|
@ -2,13 +2,9 @@ import { existsSync } from 'fs';
|
||||||
import { readFile } from 'fs/promises';
|
import { readFile } from 'fs/promises';
|
||||||
import Handlebars from 'handlebars';
|
import Handlebars from 'handlebars';
|
||||||
import { join as pathJoin } from 'path';
|
import { join as pathJoin } from 'path';
|
||||||
|
import { Service } from 'typedi';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import type {
|
import type { InviteEmailData, PasswordResetData, SendEmailResult } from './Interfaces';
|
||||||
InviteEmailData,
|
|
||||||
PasswordResetData,
|
|
||||||
SendEmailResult,
|
|
||||||
UserManagementMailerImplementation,
|
|
||||||
} from './Interfaces';
|
|
||||||
import { NodeMailer } from './NodeMailer';
|
import { NodeMailer } from './NodeMailer';
|
||||||
|
|
||||||
type Template = HandlebarsTemplateDelegate<unknown>;
|
type Template = HandlebarsTemplateDelegate<unknown>;
|
||||||
|
@ -36,8 +32,9 @@ async function getTemplate(
|
||||||
return template;
|
return template;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Service()
|
||||||
export class UserManagementMailer {
|
export class UserManagementMailer {
|
||||||
private mailer: UserManagementMailerImplementation | undefined;
|
private mailer: NodeMailer | undefined;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Other implementations can be used in the future.
|
// Other implementations can be used in the future.
|
||||||
|
@ -81,12 +78,3 @@ export class UserManagementMailer {
|
||||||
return result ?? { emailSent: false };
|
return result ?? { emailSent: false };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mailerInstance: UserManagementMailer | undefined;
|
|
||||||
|
|
||||||
export function getInstance(): UserManagementMailer {
|
|
||||||
if (mailerInstance === undefined) {
|
|
||||||
mailerInstance = new UserManagementMailer();
|
|
||||||
}
|
|
||||||
return mailerInstance;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
import { getInstance, UserManagementMailer } from './UserManagementMailer';
|
import { UserManagementMailer } from './UserManagementMailer';
|
||||||
|
|
||||||
export { getInstance, UserManagementMailer };
|
export { UserManagementMailer };
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
LoggerProxy as Logger,
|
LoggerProxy as Logger,
|
||||||
WorkflowOperationError,
|
WorkflowOperationError,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
import { Service } from 'typedi';
|
||||||
import type { FindManyOptions, ObjectLiteral } from 'typeorm';
|
import type { FindManyOptions, ObjectLiteral } from 'typeorm';
|
||||||
import { Not, LessThanOrEqual } from 'typeorm';
|
import { Not, LessThanOrEqual } from 'typeorm';
|
||||||
import { DateUtils } from 'typeorm/util/DateUtils';
|
import { DateUtils } from 'typeorm/util/DateUtils';
|
||||||
|
@ -17,7 +18,6 @@ import { DateUtils } from 'typeorm/util/DateUtils';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
import * as ResponseHelper from '@/ResponseHelper';
|
import * as ResponseHelper from '@/ResponseHelper';
|
||||||
import { ActiveExecutions } from '@/ActiveExecutions';
|
|
||||||
import type {
|
import type {
|
||||||
IExecutionFlattedDb,
|
IExecutionFlattedDb,
|
||||||
IExecutionsStopData,
|
IExecutionsStopData,
|
||||||
|
@ -25,12 +25,9 @@ import type {
|
||||||
} from '@/Interfaces';
|
} from '@/Interfaces';
|
||||||
import { WorkflowRunner } from '@/WorkflowRunner';
|
import { WorkflowRunner } from '@/WorkflowRunner';
|
||||||
import { getWorkflowOwner } from '@/UserManagement/UserManagementHelper';
|
import { getWorkflowOwner } from '@/UserManagement/UserManagementHelper';
|
||||||
import { Container, Service } from 'typedi';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class WaitTracker {
|
export class WaitTracker {
|
||||||
activeExecutionsInstance: ActiveExecutions;
|
|
||||||
|
|
||||||
private waitingExecutions: {
|
private waitingExecutions: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
executionId: string;
|
executionId: string;
|
||||||
|
@ -41,8 +38,6 @@ export class WaitTracker {
|
||||||
mainTimer: NodeJS.Timeout;
|
mainTimer: NodeJS.Timeout;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.activeExecutionsInstance = Container.get(ActiveExecutions);
|
|
||||||
|
|
||||||
// Poll every 60 seconds a list of upcoming executions
|
// Poll every 60 seconds a list of upcoming executions
|
||||||
this.mainTimer = setInterval(() => {
|
this.mainTimer = setInterval(() => {
|
||||||
this.getWaitingExecutions();
|
this.getWaitingExecutions();
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
/* eslint-disable no-param-reassign */
|
/* eslint-disable no-param-reassign */
|
||||||
import type { INode, WebhookHttpMethod } from 'n8n-workflow';
|
import type { INode, WebhookHttpMethod } from 'n8n-workflow';
|
||||||
import { NodeHelpers, Workflow, LoggerProxy as Logger } from 'n8n-workflow';
|
import { NodeHelpers, Workflow, LoggerProxy as Logger } from 'n8n-workflow';
|
||||||
|
import { Service } from 'typedi';
|
||||||
import type express from 'express';
|
import type express from 'express';
|
||||||
|
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
|
@ -13,9 +13,11 @@ import { NodeTypes } from '@/NodeTypes';
|
||||||
import type { IExecutionResponse, IResponseCallbackData, IWorkflowDb } from '@/Interfaces';
|
import type { IExecutionResponse, IResponseCallbackData, IWorkflowDb } from '@/Interfaces';
|
||||||
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
|
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
|
||||||
import { getWorkflowOwner } from '@/UserManagement/UserManagementHelper';
|
import { getWorkflowOwner } from '@/UserManagement/UserManagementHelper';
|
||||||
import { Container } from 'typedi';
|
|
||||||
|
|
||||||
|
@Service()
|
||||||
export class WaitingWebhooks {
|
export class WaitingWebhooks {
|
||||||
|
constructor(private nodeTypes: NodeTypes) {}
|
||||||
|
|
||||||
async executeWebhook(
|
async executeWebhook(
|
||||||
httpMethod: WebhookHttpMethod,
|
httpMethod: WebhookHttpMethod,
|
||||||
fullPath: string,
|
fullPath: string,
|
||||||
|
@ -79,14 +81,13 @@ export class WaitingWebhooks {
|
||||||
|
|
||||||
const { workflowData } = fullExecutionData;
|
const { workflowData } = fullExecutionData;
|
||||||
|
|
||||||
const nodeTypes = Container.get(NodeTypes);
|
|
||||||
const workflow = new Workflow({
|
const workflow = new Workflow({
|
||||||
id: workflowData.id!.toString(),
|
id: workflowData.id!.toString(),
|
||||||
name: workflowData.name,
|
name: workflowData.name,
|
||||||
nodes: workflowData.nodes,
|
nodes: workflowData.nodes,
|
||||||
connections: workflowData.connections,
|
connections: workflowData.connections,
|
||||||
active: workflowData.active,
|
active: workflowData.active,
|
||||||
nodeTypes,
|
nodeTypes: this.nodeTypes,
|
||||||
staticData: workflowData.staticData,
|
staticData: workflowData.staticData,
|
||||||
settings: workflowData.settings,
|
settings: workflowData.settings,
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
import type express from 'express';
|
import type express from 'express';
|
||||||
import get from 'lodash.get';
|
import get from 'lodash.get';
|
||||||
|
|
||||||
import { BINARY_ENCODING, BinaryDataManager, NodeExecuteFunctions, eventEmitter } from 'n8n-core';
|
import { BinaryDataManager, NodeExecuteFunctions, eventEmitter } from 'n8n-core';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
IBinaryKeyData,
|
IBinaryKeyData,
|
||||||
|
@ -35,6 +35,7 @@ import type {
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import {
|
import {
|
||||||
|
BINARY_ENCODING,
|
||||||
createDeferredPromise,
|
createDeferredPromise,
|
||||||
ErrorReporterProxy as ErrorReporter,
|
ErrorReporterProxy as ErrorReporter,
|
||||||
LoggerProxy as Logger,
|
LoggerProxy as Logger,
|
||||||
|
|
|
@ -66,11 +66,12 @@ import * as ResponseHelper from '@/ResponseHelper';
|
||||||
import * as WebhookHelpers from '@/WebhookHelpers';
|
import * as WebhookHelpers from '@/WebhookHelpers';
|
||||||
import * as WorkflowHelpers from '@/WorkflowHelpers';
|
import * as WorkflowHelpers from '@/WorkflowHelpers';
|
||||||
import { getWorkflowOwner } from '@/UserManagement/UserManagementHelper';
|
import { getWorkflowOwner } from '@/UserManagement/UserManagementHelper';
|
||||||
import { findSubworkflowStart } from '@/utils';
|
import { findSubworkflowStart, isWorkflowIdValid } from '@/utils';
|
||||||
import { PermissionChecker } from './UserManagement/PermissionChecker';
|
import { PermissionChecker } from './UserManagement/PermissionChecker';
|
||||||
import { WorkflowsService } from './workflows/workflows.services';
|
import { WorkflowsService } from './workflows/workflows.services';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import { InternalHooks } from '@/InternalHooks';
|
import { InternalHooks } from '@/InternalHooks';
|
||||||
|
import type { ExecutionMetadata } from './databases/entities/ExecutionMetadata';
|
||||||
|
|
||||||
const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType');
|
const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType');
|
||||||
|
|
||||||
|
@ -135,17 +136,11 @@ export function executeErrorWorkflow(
|
||||||
|
|
||||||
// Run the error workflow
|
// Run the error workflow
|
||||||
// To avoid an infinite loop do not run the error workflow again if the error-workflow itself failed and it is its own error-workflow.
|
// To avoid an infinite loop do not run the error workflow again if the error-workflow itself failed and it is its own error-workflow.
|
||||||
if (
|
const { errorWorkflow } = workflowData.settings ?? {};
|
||||||
workflowData.settings?.errorWorkflow &&
|
if (errorWorkflow && !(mode === 'error' && workflowId && errorWorkflow === workflowId)) {
|
||||||
!(
|
|
||||||
mode === 'error' &&
|
|
||||||
workflowId &&
|
|
||||||
workflowData.settings.errorWorkflow.toString() === workflowId
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Logger.verbose('Start external error workflow', {
|
Logger.verbose('Start external error workflow', {
|
||||||
executionId,
|
executionId,
|
||||||
errorWorkflowId: workflowData.settings.errorWorkflow.toString(),
|
errorWorkflowId: errorWorkflow,
|
||||||
workflowId,
|
workflowId,
|
||||||
});
|
});
|
||||||
// If a specific error workflow is set run only that one
|
// If a specific error workflow is set run only that one
|
||||||
|
@ -159,11 +154,7 @@ export function executeErrorWorkflow(
|
||||||
}
|
}
|
||||||
getWorkflowOwner(workflowId)
|
getWorkflowOwner(workflowId)
|
||||||
.then((user) => {
|
.then((user) => {
|
||||||
void WorkflowHelpers.executeErrorWorkflow(
|
void WorkflowHelpers.executeErrorWorkflow(errorWorkflow, workflowErrorData, user);
|
||||||
workflowData.settings!.errorWorkflow as string,
|
|
||||||
workflowErrorData,
|
|
||||||
user,
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.catch((error: Error) => {
|
.catch((error: Error) => {
|
||||||
ErrorReporter.error(error);
|
ErrorReporter.error(error);
|
||||||
|
@ -171,7 +162,7 @@ export function executeErrorWorkflow(
|
||||||
`Could not execute ErrorWorkflow for execution ID ${this.executionId} because of error querying the workflow owner`,
|
`Could not execute ErrorWorkflow for execution ID ${this.executionId} because of error querying the workflow owner`,
|
||||||
{
|
{
|
||||||
executionId,
|
executionId,
|
||||||
errorWorkflowId: workflowData.settings!.errorWorkflow!.toString(),
|
errorWorkflowId: errorWorkflow,
|
||||||
workflowId,
|
workflowId,
|
||||||
error,
|
error,
|
||||||
workflowErrorData,
|
workflowErrorData,
|
||||||
|
@ -264,6 +255,22 @@ async function pruneExecutionData(this: WorkflowHooks): Promise<void> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function saveExecutionMetadata(
|
||||||
|
executionId: string,
|
||||||
|
executionMetadata: Record<string, string>,
|
||||||
|
): Promise<ExecutionMetadata[]> {
|
||||||
|
const metadataRows = [];
|
||||||
|
for (const [key, value] of Object.entries(executionMetadata)) {
|
||||||
|
metadataRows.push({
|
||||||
|
execution: { id: executionId },
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Db.collections.ExecutionMetadata.save(metadataRows);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns hook functions to push data to Editor-UI
|
* Returns hook functions to push data to Editor-UI
|
||||||
*
|
*
|
||||||
|
@ -404,21 +411,21 @@ export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowEx
|
||||||
],
|
],
|
||||||
nodeExecuteAfter: [
|
nodeExecuteAfter: [
|
||||||
async function (
|
async function (
|
||||||
|
this: WorkflowHooks,
|
||||||
nodeName: string,
|
nodeName: string,
|
||||||
data: ITaskData,
|
data: ITaskData,
|
||||||
executionData: IRunExecutionData,
|
executionData: IRunExecutionData,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (this.workflowData.settings !== undefined) {
|
const saveExecutionProgress = config.getEnv('executions.saveExecutionProgress');
|
||||||
if (this.workflowData.settings.saveExecutionProgress === false) {
|
const workflowSettings = this.workflowData.settings;
|
||||||
|
if (workflowSettings !== undefined) {
|
||||||
|
if (workflowSettings.saveExecutionProgress === false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (workflowSettings.saveExecutionProgress !== true && !saveExecutionProgress) {
|
||||||
this.workflowData.settings.saveExecutionProgress !== true &&
|
|
||||||
!config.getEnv('executions.saveExecutionProgress')
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (!config.getEnv('executions.saveExecutionProgress')) {
|
} else if (!saveExecutionProgress) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -530,11 +537,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
|
||||||
const isManualMode = [this.mode, parentProcessMode].includes('manual');
|
const isManualMode = [this.mode, parentProcessMode].includes('manual');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (
|
if (!isManualMode && isWorkflowIdValid(this.workflowData.id) && newStaticData) {
|
||||||
!isManualMode &&
|
|
||||||
WorkflowHelpers.isWorkflowIdValid(this.workflowData.id) &&
|
|
||||||
newStaticData
|
|
||||||
) {
|
|
||||||
// Workflow is saved so update in database
|
// Workflow is saved so update in database
|
||||||
try {
|
try {
|
||||||
await WorkflowHelpers.saveStaticDataById(
|
await WorkflowHelpers.saveStaticDataById(
|
||||||
|
@ -550,13 +553,11 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const workflowSettings = this.workflowData.settings;
|
||||||
let saveManualExecutions = config.getEnv('executions.saveDataManualExecutions');
|
let saveManualExecutions = config.getEnv('executions.saveDataManualExecutions');
|
||||||
if (
|
if (workflowSettings?.saveManualExecutions !== undefined) {
|
||||||
this.workflowData.settings !== undefined &&
|
|
||||||
this.workflowData.settings.saveManualExecutions !== undefined
|
|
||||||
) {
|
|
||||||
// Apply to workflow override
|
// Apply to workflow override
|
||||||
saveManualExecutions = this.workflowData.settings.saveManualExecutions as boolean;
|
saveManualExecutions = workflowSettings.saveManualExecutions as boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isManualMode && !saveManualExecutions && !fullRunData.waitTill) {
|
if (isManualMode && !saveManualExecutions && !fullRunData.waitTill) {
|
||||||
|
@ -641,7 +642,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
|
||||||
}
|
}
|
||||||
|
|
||||||
const workflowId = this.workflowData.id;
|
const workflowId = this.workflowData.id;
|
||||||
if (WorkflowHelpers.isWorkflowIdValid(workflowId)) {
|
if (isWorkflowIdValid(workflowId)) {
|
||||||
fullExecutionData.workflowId = workflowId;
|
fullExecutionData.workflowId = workflowId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -661,6 +662,14 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
|
||||||
executionData as IExecutionFlattedDb,
|
executionData as IExecutionFlattedDb,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (fullRunData.data.resultData.metadata) {
|
||||||
|
await saveExecutionMetadata(this.executionId, fullRunData.data.resultData.metadata);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Logger.error(`Failed to save metadata for execution ID ${this.executionId}`, e);
|
||||||
|
}
|
||||||
|
|
||||||
if (fullRunData.finished === true && this.retryOf !== undefined) {
|
if (fullRunData.finished === true && this.retryOf !== undefined) {
|
||||||
// If the retry was successful save the reference it on the original execution
|
// If the retry was successful save the reference it on the original execution
|
||||||
// await Db.collections.Execution.save(executionData as IExecutionFlattedDb);
|
// await Db.collections.Execution.save(executionData as IExecutionFlattedDb);
|
||||||
|
@ -729,7 +738,7 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
|
||||||
newStaticData: IDataObject,
|
newStaticData: IDataObject,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
if (WorkflowHelpers.isWorkflowIdValid(this.workflowData.id) && newStaticData) {
|
if (isWorkflowIdValid(this.workflowData.id) && newStaticData) {
|
||||||
// Workflow is saved so update in database
|
// Workflow is saved so update in database
|
||||||
try {
|
try {
|
||||||
await WorkflowHelpers.saveStaticDataById(
|
await WorkflowHelpers.saveStaticDataById(
|
||||||
|
@ -776,7 +785,7 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
|
||||||
}
|
}
|
||||||
|
|
||||||
const workflowId = this.workflowData.id;
|
const workflowId = this.workflowData.id;
|
||||||
if (WorkflowHelpers.isWorkflowIdValid(workflowId)) {
|
if (isWorkflowIdValid(workflowId)) {
|
||||||
fullExecutionData.workflowId = workflowId;
|
fullExecutionData.workflowId = workflowId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -793,6 +802,14 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
|
||||||
status: executionData.status,
|
status: executionData.status,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (fullRunData.data.resultData.metadata) {
|
||||||
|
await saveExecutionMetadata(this.executionId, fullRunData.data.resultData.metadata);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Logger.error(`Failed to save metadata for execution ID ${this.executionId}`, e);
|
||||||
|
}
|
||||||
|
|
||||||
if (fullRunData.finished === true && this.retryOf !== undefined) {
|
if (fullRunData.finished === true && this.retryOf !== undefined) {
|
||||||
// If the retry was successful save the reference it on the original execution
|
// If the retry was successful save the reference it on the original execution
|
||||||
await Db.collections.Execution.update(this.retryOf, {
|
await Db.collections.Execution.update(this.retryOf, {
|
||||||
|
@ -995,16 +1012,14 @@ async function executeWorkflow(
|
||||||
additionalDataIntegrated.executeWorkflow = additionalData.executeWorkflow;
|
additionalDataIntegrated.executeWorkflow = additionalData.executeWorkflow;
|
||||||
|
|
||||||
let subworkflowTimeout = additionalData.executionTimeoutTimestamp;
|
let subworkflowTimeout = additionalData.executionTimeoutTimestamp;
|
||||||
if (
|
const workflowSettings = workflowData.settings;
|
||||||
workflowData.settings?.executionTimeout !== undefined &&
|
if (workflowSettings?.executionTimeout !== undefined && workflowSettings.executionTimeout > 0) {
|
||||||
workflowData.settings.executionTimeout > 0
|
|
||||||
) {
|
|
||||||
// We might have received a max timeout timestamp from the parent workflow
|
// We might have received a max timeout timestamp from the parent workflow
|
||||||
// If we did, then we get the minimum time between the two timeouts
|
// If we did, then we get the minimum time between the two timeouts
|
||||||
// If no timeout was given from the parent, then we use our timeout.
|
// If no timeout was given from the parent, then we use our timeout.
|
||||||
subworkflowTimeout = Math.min(
|
subworkflowTimeout = Math.min(
|
||||||
additionalData.executionTimeoutTimestamp || Number.MAX_SAFE_INTEGER,
|
additionalData.executionTimeoutTimestamp || Number.MAX_SAFE_INTEGER,
|
||||||
Date.now() + (workflowData.settings.executionTimeout as number) * 1000,
|
Date.now() + workflowSettings.executionTimeout * 1000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
|
import { Container } from 'typedi';
|
||||||
import type {
|
import type {
|
||||||
IDataObject,
|
IDataObject,
|
||||||
IExecuteData,
|
IExecuteData,
|
||||||
|
@ -32,7 +33,7 @@ import type { User } from '@db/entities/User';
|
||||||
import { whereClause } from '@/UserManagement/UserManagementHelper';
|
import { whereClause } from '@/UserManagement/UserManagementHelper';
|
||||||
import omit from 'lodash.omit';
|
import omit from 'lodash.omit';
|
||||||
import { PermissionChecker } from './UserManagement/PermissionChecker';
|
import { PermissionChecker } from './UserManagement/PermissionChecker';
|
||||||
import { Container } from 'typedi';
|
import { isWorkflowIdValid } from './utils';
|
||||||
|
|
||||||
const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType');
|
const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType');
|
||||||
|
|
||||||
|
@ -74,15 +75,6 @@ export function getDataLastExecutedNodeData(inputData: IRun): ITaskData | undefi
|
||||||
return lastNodeRunData;
|
return lastNodeRunData;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns if the given id is a valid workflow id
|
|
||||||
*
|
|
||||||
* @param {(string | null | undefined)} id The id to check
|
|
||||||
*/
|
|
||||||
export function isWorkflowIdValid(id: string | null | undefined): boolean {
|
|
||||||
return !(typeof id === 'string' && isNaN(parseInt(id, 10)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes the error workflow
|
* Executes the error workflow
|
||||||
*
|
*
|
||||||
|
|
|
@ -21,6 +21,7 @@ import type {
|
||||||
IRun,
|
IRun,
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
WorkflowHooks,
|
WorkflowHooks,
|
||||||
|
WorkflowSettings,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import {
|
import {
|
||||||
ErrorReporterProxy as ErrorReporter,
|
ErrorReporterProxy as ErrorReporter,
|
||||||
|
@ -44,7 +45,8 @@ import type {
|
||||||
IWorkflowExecutionDataProcessWithExecution,
|
IWorkflowExecutionDataProcessWithExecution,
|
||||||
} from '@/Interfaces';
|
} from '@/Interfaces';
|
||||||
import { NodeTypes } from '@/NodeTypes';
|
import { NodeTypes } from '@/NodeTypes';
|
||||||
import * as Queue from '@/Queue';
|
import type { Job, JobData, JobQueue, JobResponse } from '@/Queue';
|
||||||
|
import { Queue } from '@/Queue';
|
||||||
import * as ResponseHelper from '@/ResponseHelper';
|
import * as ResponseHelper from '@/ResponseHelper';
|
||||||
import * as WebhookHelpers from '@/WebhookHelpers';
|
import * as WebhookHelpers from '@/WebhookHelpers';
|
||||||
import * as WorkflowHelpers from '@/WorkflowHelpers';
|
import * as WorkflowHelpers from '@/WorkflowHelpers';
|
||||||
|
@ -63,7 +65,7 @@ export class WorkflowRunner {
|
||||||
|
|
||||||
push: Push;
|
push: Push;
|
||||||
|
|
||||||
jobQueue: Queue.JobQueue;
|
jobQueue: JobQueue;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.push = Container.get(Push);
|
this.push = Container.get(Push);
|
||||||
|
@ -167,7 +169,7 @@ export class WorkflowRunner {
|
||||||
await initErrorHandling();
|
await initErrorHandling();
|
||||||
|
|
||||||
if (executionsMode === 'queue') {
|
if (executionsMode === 'queue') {
|
||||||
const queue = await Queue.getInstance();
|
const queue = Container.get(Queue);
|
||||||
this.jobQueue = queue.getBullObjectInstance();
|
this.jobQueue = queue.getBullObjectInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,11 +249,9 @@ export class WorkflowRunner {
|
||||||
// Changes were made by adding the `workflowTimeout` to the `additionalData`
|
// Changes were made by adding the `workflowTimeout` to the `additionalData`
|
||||||
// So that the timeout will also work for executions with nested workflows.
|
// So that the timeout will also work for executions with nested workflows.
|
||||||
let executionTimeout: NodeJS.Timeout;
|
let executionTimeout: NodeJS.Timeout;
|
||||||
let workflowTimeout = config.getEnv('executions.timeout'); // initialize with default
|
|
||||||
if (data.workflowData.settings && data.workflowData.settings.executionTimeout) {
|
|
||||||
workflowTimeout = data.workflowData.settings.executionTimeout as number; // preference on workflow setting
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const workflowSettings = data.workflowData.settings ?? {};
|
||||||
|
let workflowTimeout = workflowSettings.executionTimeout ?? config.getEnv('executions.timeout'); // initialize with default
|
||||||
if (workflowTimeout > 0) {
|
if (workflowTimeout > 0) {
|
||||||
workflowTimeout = Math.min(workflowTimeout, config.getEnv('executions.maxTimeout'));
|
workflowTimeout = Math.min(workflowTimeout, config.getEnv('executions.maxTimeout'));
|
||||||
}
|
}
|
||||||
|
@ -264,7 +264,7 @@ export class WorkflowRunner {
|
||||||
active: data.workflowData.active,
|
active: data.workflowData.active,
|
||||||
nodeTypes,
|
nodeTypes,
|
||||||
staticData: data.workflowData.staticData,
|
staticData: data.workflowData.staticData,
|
||||||
settings: data.workflowData.settings,
|
settings: workflowSettings,
|
||||||
});
|
});
|
||||||
const additionalData = await WorkflowExecuteAdditionalData.getBase(
|
const additionalData = await WorkflowExecuteAdditionalData.getBase(
|
||||||
data.userId,
|
data.userId,
|
||||||
|
@ -434,7 +434,7 @@ export class WorkflowRunner {
|
||||||
this.activeExecutions.attachResponsePromise(executionId, responsePromise);
|
this.activeExecutions.attachResponsePromise(executionId, responsePromise);
|
||||||
}
|
}
|
||||||
|
|
||||||
const jobData: Queue.JobData = {
|
const jobData: JobData = {
|
||||||
executionId,
|
executionId,
|
||||||
loadStaticData: !!loadStaticData,
|
loadStaticData: !!loadStaticData,
|
||||||
};
|
};
|
||||||
|
@ -451,7 +451,7 @@ export class WorkflowRunner {
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
removeOnFail: true,
|
removeOnFail: true,
|
||||||
};
|
};
|
||||||
let job: Queue.Job;
|
let job: Job;
|
||||||
let hooks: WorkflowHooks;
|
let hooks: WorkflowHooks;
|
||||||
try {
|
try {
|
||||||
job = await this.jobQueue.add(jobData, jobOptions);
|
job = await this.jobQueue.add(jobData, jobOptions);
|
||||||
|
@ -485,7 +485,7 @@ export class WorkflowRunner {
|
||||||
async (resolve, reject, onCancel) => {
|
async (resolve, reject, onCancel) => {
|
||||||
onCancel.shouldReject = false;
|
onCancel.shouldReject = false;
|
||||||
onCancel(async () => {
|
onCancel(async () => {
|
||||||
const queue = await Queue.getInstance();
|
const queue = Container.get(Queue);
|
||||||
await queue.stopJob(job);
|
await queue.stopJob(job);
|
||||||
|
|
||||||
// We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the
|
// We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the
|
||||||
|
@ -503,11 +503,11 @@ export class WorkflowRunner {
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
const jobData: Promise<Queue.JobResponse> = job.finished();
|
const jobData: Promise<JobResponse> = job.finished();
|
||||||
|
|
||||||
const queueRecoveryInterval = config.getEnv('queue.bull.queueRecoveryInterval');
|
const queueRecoveryInterval = config.getEnv('queue.bull.queueRecoveryInterval');
|
||||||
|
|
||||||
const racingPromises: Array<Promise<Queue.JobResponse | object>> = [jobData];
|
const racingPromises: Array<Promise<JobResponse | object>> = [jobData];
|
||||||
|
|
||||||
let clearWatchdogInterval;
|
let clearWatchdogInterval;
|
||||||
if (queueRecoveryInterval > 0) {
|
if (queueRecoveryInterval > 0) {
|
||||||
|
@ -589,16 +589,12 @@ export class WorkflowRunner {
|
||||||
try {
|
try {
|
||||||
// Check if this execution data has to be removed from database
|
// Check if this execution data has to be removed from database
|
||||||
// based on workflow settings.
|
// based on workflow settings.
|
||||||
let saveDataErrorExecution = config.getEnv('executions.saveDataOnError') as string;
|
const workflowSettings = data.workflowData.settings ?? {};
|
||||||
let saveDataSuccessExecution = config.getEnv('executions.saveDataOnSuccess') as string;
|
const saveDataErrorExecution =
|
||||||
if (data.workflowData.settings !== undefined) {
|
workflowSettings.saveDataErrorExecution ?? config.getEnv('executions.saveDataOnError');
|
||||||
saveDataErrorExecution =
|
const saveDataSuccessExecution =
|
||||||
(data.workflowData.settings.saveDataErrorExecution as string) ||
|
workflowSettings.saveDataSuccessExecution ??
|
||||||
saveDataErrorExecution;
|
config.getEnv('executions.saveDataOnSuccess');
|
||||||
saveDataSuccessExecution =
|
|
||||||
(data.workflowData.settings.saveDataSuccessExecution as string) ||
|
|
||||||
saveDataSuccessExecution;
|
|
||||||
}
|
|
||||||
|
|
||||||
const workflowDidSucceed = !runData.data.resultData.error;
|
const workflowDidSucceed = !runData.data.resultData.error;
|
||||||
if (
|
if (
|
||||||
|
@ -665,10 +661,9 @@ export class WorkflowRunner {
|
||||||
|
|
||||||
// Start timeout for the execution
|
// Start timeout for the execution
|
||||||
let executionTimeout: NodeJS.Timeout;
|
let executionTimeout: NodeJS.Timeout;
|
||||||
let workflowTimeout = config.getEnv('executions.timeout'); // initialize with default
|
|
||||||
if (data.workflowData.settings && data.workflowData.settings.executionTimeout) {
|
const workflowSettings = data.workflowData.settings ?? {};
|
||||||
workflowTimeout = data.workflowData.settings.executionTimeout as number; // preference on workflow setting
|
let workflowTimeout = workflowSettings.executionTimeout ?? config.getEnv('executions.timeout'); // initialize with default
|
||||||
}
|
|
||||||
|
|
||||||
const processTimeoutFunction = (timeout: number) => {
|
const processTimeoutFunction = (timeout: number) => {
|
||||||
this.activeExecutions.stopExecution(executionId, 'timeout');
|
this.activeExecutions.stopExecution(executionId, 'timeout');
|
||||||
|
|
|
@ -54,9 +54,9 @@ import config from '@/config';
|
||||||
import { generateFailedExecutionFromError } from '@/WorkflowHelpers';
|
import { generateFailedExecutionFromError } from '@/WorkflowHelpers';
|
||||||
import { initErrorHandling } from '@/ErrorReporting';
|
import { initErrorHandling } from '@/ErrorReporting';
|
||||||
import { PermissionChecker } from '@/UserManagement/PermissionChecker';
|
import { PermissionChecker } from '@/UserManagement/PermissionChecker';
|
||||||
import { getLicense } from './License';
|
import { License } from '@/License';
|
||||||
import { InternalHooks } from './InternalHooks';
|
import { InternalHooks } from '@/InternalHooks';
|
||||||
import { PostHogClient } from './posthog';
|
import { PostHogClient } from '@/posthog';
|
||||||
|
|
||||||
class WorkflowRunnerProcess {
|
class WorkflowRunnerProcess {
|
||||||
data: IWorkflowExecutionDataProcessWithExecution | undefined;
|
data: IWorkflowExecutionDataProcessWithExecution | undefined;
|
||||||
|
@ -127,16 +127,13 @@ class WorkflowRunnerProcess {
|
||||||
// Init db since we need to read the license.
|
// Init db since we need to read the license.
|
||||||
await Db.init();
|
await Db.init();
|
||||||
|
|
||||||
const license = getLicense();
|
const license = Container.get(License);
|
||||||
await license.init(instanceId);
|
await license.init(instanceId);
|
||||||
|
|
||||||
// Start timeout for the execution
|
const workflowSettings = this.data.workflowData.settings ?? {};
|
||||||
let workflowTimeout = config.getEnv('executions.timeout'); // initialize with default
|
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
|
|
||||||
if (this.data.workflowData.settings && this.data.workflowData.settings.executionTimeout) {
|
|
||||||
workflowTimeout = this.data.workflowData.settings.executionTimeout as number; // preference on workflow setting
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Start timeout for the execution
|
||||||
|
let workflowTimeout = workflowSettings.executionTimeout ?? config.getEnv('executions.timeout'); // initialize with default
|
||||||
if (workflowTimeout > 0) {
|
if (workflowTimeout > 0) {
|
||||||
workflowTimeout = Math.min(workflowTimeout, config.getEnv('executions.maxTimeout'));
|
workflowTimeout = Math.min(workflowTimeout, config.getEnv('executions.maxTimeout'));
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
|
import type { Request } from 'express';
|
||||||
import bodyParser from 'body-parser';
|
import bodyParser from 'body-parser';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
|
@ -12,12 +13,26 @@ import * as Db from '@/Db';
|
||||||
import type { Role } from '@db/entities/Role';
|
import type { Role } from '@db/entities/Role';
|
||||||
import { hashPassword } from '@/UserManagement/UserManagementHelper';
|
import { hashPassword } from '@/UserManagement/UserManagementHelper';
|
||||||
import { eventBus } from '@/eventbus/MessageEventBus/MessageEventBus';
|
import { eventBus } from '@/eventbus/MessageEventBus/MessageEventBus';
|
||||||
|
import Container from 'typedi';
|
||||||
|
import { License } from '../License';
|
||||||
|
|
||||||
if (process.env.E2E_TESTS !== 'true') {
|
if (process.env.E2E_TESTS !== 'true') {
|
||||||
console.error('E2E endpoints only allowed during E2E tests');
|
console.error('E2E endpoints only allowed during E2E tests');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const enabledFeatures = {
|
||||||
|
sharing: true, //default to true here instead of setting it in config/index.ts for e2e
|
||||||
|
ldap: false,
|
||||||
|
saml: false,
|
||||||
|
logStreaming: false,
|
||||||
|
advancedExecutionFilters: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
type Feature = keyof typeof enabledFeatures;
|
||||||
|
|
||||||
|
Container.get(License).isFeatureEnabled = (feature: Feature) => enabledFeatures[feature] ?? false;
|
||||||
|
|
||||||
const tablesToTruncate = [
|
const tablesToTruncate = [
|
||||||
'auth_identity',
|
'auth_identity',
|
||||||
'auth_provider_sync_history',
|
'auth_provider_sync_history',
|
||||||
|
@ -78,7 +93,7 @@ const setupUserManagement = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetLogStreaming = async () => {
|
const resetLogStreaming = async () => {
|
||||||
config.set('enterprise.features.logStreaming', false);
|
enabledFeatures.logStreaming = false;
|
||||||
for (const id in eventBus.destinations) {
|
for (const id in eventBus.destinations) {
|
||||||
await eventBus.removeDestination(id);
|
await eventBus.removeDestination(id);
|
||||||
}
|
}
|
||||||
|
@ -127,7 +142,8 @@ e2eController.post('/db/setup-owner', bodyParser.json(), async (req, res) => {
|
||||||
res.writeHead(204).end();
|
res.writeHead(204).end();
|
||||||
});
|
});
|
||||||
|
|
||||||
e2eController.post('/enable-feature/:feature', async (req, res) => {
|
e2eController.post('/enable-feature/:feature', async (req: Request<{ feature: Feature }>, res) => {
|
||||||
config.set(`enterprise.features.${req.params.feature}`, true);
|
const { feature } = req.params;
|
||||||
|
enabledFeatures[feature] = true;
|
||||||
res.writeHead(204).end();
|
res.writeHead(204).end();
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,110 +0,0 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */
|
|
||||||
/* eslint-disable @typescript-eslint/no-invalid-void-type */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
||||||
/* eslint-disable no-param-reassign */
|
|
||||||
|
|
||||||
import express from 'express';
|
|
||||||
|
|
||||||
import * as Db from '@/Db';
|
|
||||||
import { ExternalHooks } from '@/ExternalHooks';
|
|
||||||
import type { ITagWithCountDb } from '@/Interfaces';
|
|
||||||
import * as ResponseHelper from '@/ResponseHelper';
|
|
||||||
import config from '@/config';
|
|
||||||
import * as TagHelpers from '@/TagHelpers';
|
|
||||||
import { validateEntity } from '@/GenericHelpers';
|
|
||||||
import { TagEntity } from '@db/entities/TagEntity';
|
|
||||||
import type { TagsRequest } from '@/requests';
|
|
||||||
import { Container } from 'typedi';
|
|
||||||
|
|
||||||
export const tagsController = express.Router();
|
|
||||||
|
|
||||||
const workflowsEnabledMiddleware: express.RequestHandler = (req, res, next) => {
|
|
||||||
if (config.getEnv('workflowTagsDisabled')) {
|
|
||||||
throw new ResponseHelper.BadRequestError('Workflow tags are disabled');
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Retrieves all tags, with or without usage count
|
|
||||||
tagsController.get(
|
|
||||||
'/',
|
|
||||||
workflowsEnabledMiddleware,
|
|
||||||
ResponseHelper.send(async (req: express.Request): Promise<TagEntity[] | ITagWithCountDb[]> => {
|
|
||||||
if (req.query.withUsageCount === 'true') {
|
|
||||||
const tablePrefix = config.getEnv('database.tablePrefix');
|
|
||||||
return TagHelpers.getTagsWithCountDb(tablePrefix);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Db.collections.Tag.find({ select: ['id', 'name', 'createdAt', 'updatedAt'] });
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Creates a tag
|
|
||||||
tagsController.post(
|
|
||||||
'/',
|
|
||||||
workflowsEnabledMiddleware,
|
|
||||||
ResponseHelper.send(async (req: express.Request): Promise<TagEntity | void> => {
|
|
||||||
const newTag = new TagEntity();
|
|
||||||
newTag.name = req.body.name.trim();
|
|
||||||
|
|
||||||
await Container.get(ExternalHooks).run('tag.beforeCreate', [newTag]);
|
|
||||||
|
|
||||||
await validateEntity(newTag);
|
|
||||||
const tag = await Db.collections.Tag.save(newTag);
|
|
||||||
|
|
||||||
await Container.get(ExternalHooks).run('tag.afterCreate', [tag]);
|
|
||||||
|
|
||||||
return tag;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Updates a tag
|
|
||||||
tagsController.patch(
|
|
||||||
'/:id(\\d+)',
|
|
||||||
workflowsEnabledMiddleware,
|
|
||||||
ResponseHelper.send(async (req: express.Request): Promise<TagEntity | void> => {
|
|
||||||
const { name } = req.body;
|
|
||||||
const { id } = req.params;
|
|
||||||
|
|
||||||
const newTag = new TagEntity();
|
|
||||||
// @ts-ignore
|
|
||||||
newTag.id = id;
|
|
||||||
newTag.name = name.trim();
|
|
||||||
|
|
||||||
await Container.get(ExternalHooks).run('tag.beforeUpdate', [newTag]);
|
|
||||||
|
|
||||||
await validateEntity(newTag);
|
|
||||||
const tag = await Db.collections.Tag.save(newTag);
|
|
||||||
|
|
||||||
await Container.get(ExternalHooks).run('tag.afterUpdate', [tag]);
|
|
||||||
|
|
||||||
return tag;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
tagsController.delete(
|
|
||||||
'/:id(\\d+)',
|
|
||||||
workflowsEnabledMiddleware,
|
|
||||||
ResponseHelper.send(async (req: TagsRequest.Delete): Promise<boolean> => {
|
|
||||||
if (
|
|
||||||
config.getEnv('userManagement.isInstanceOwnerSetUp') === true &&
|
|
||||||
req.user.globalRole.name !== 'owner'
|
|
||||||
) {
|
|
||||||
throw new ResponseHelper.UnauthorizedError(
|
|
||||||
'You are not allowed to perform this action',
|
|
||||||
'Only owners can remove tags',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const id = req.params.id;
|
|
||||||
|
|
||||||
await Container.get(ExternalHooks).run('tag.beforeDelete', [id]);
|
|
||||||
|
|
||||||
await Db.collections.Tag.delete({ id });
|
|
||||||
|
|
||||||
await Container.get(ExternalHooks).run('tag.afterDelete', [id]);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
);
|
|
|
@ -34,6 +34,8 @@ export abstract class BaseCommand extends Command {
|
||||||
|
|
||||||
protected userSettings: IUserSettings;
|
protected userSettings: IUserSettings;
|
||||||
|
|
||||||
|
protected instanceId: string;
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
await initErrorHandling();
|
await initErrorHandling();
|
||||||
|
|
||||||
|
@ -49,9 +51,9 @@ export abstract class BaseCommand extends Command {
|
||||||
const credentialTypes = Container.get(CredentialTypes);
|
const credentialTypes = Container.get(CredentialTypes);
|
||||||
CredentialsOverwrites(credentialTypes);
|
CredentialsOverwrites(credentialTypes);
|
||||||
|
|
||||||
const instanceId = this.userSettings.instanceId ?? '';
|
this.instanceId = this.userSettings.instanceId ?? '';
|
||||||
await Container.get(PostHogClient).init(instanceId);
|
await Container.get(PostHogClient).init(this.instanceId);
|
||||||
await Container.get(InternalHooks).init(instanceId);
|
await Container.get(InternalHooks).init(this.instanceId);
|
||||||
|
|
||||||
await Db.init().catch(async (error: Error) =>
|
await Db.init().catch(async (error: Error) =>
|
||||||
this.exitWithCrash('There was an error initializing DB', error),
|
this.exitWithCrash('There was an error initializing DB', error),
|
||||||
|
|
|
@ -6,11 +6,10 @@ import { ExecutionBaseError } from 'n8n-workflow';
|
||||||
|
|
||||||
import { ActiveExecutions } from '@/ActiveExecutions';
|
import { ActiveExecutions } from '@/ActiveExecutions';
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
import * as WorkflowHelpers from '@/WorkflowHelpers';
|
|
||||||
import { WorkflowRunner } from '@/WorkflowRunner';
|
import { WorkflowRunner } from '@/WorkflowRunner';
|
||||||
import type { IWorkflowExecutionDataProcess } from '@/Interfaces';
|
import type { IWorkflowExecutionDataProcess } from '@/Interfaces';
|
||||||
import { getInstanceOwner } from '@/UserManagement/UserManagementHelper';
|
import { getInstanceOwner } from '@/UserManagement/UserManagementHelper';
|
||||||
import { findCliWorkflowStart } from '@/utils';
|
import { findCliWorkflowStart, isWorkflowIdValid } from '@/utils';
|
||||||
import { initEvents } from '@/events';
|
import { initEvents } from '@/events';
|
||||||
import { BaseCommand } from './BaseCommand';
|
import { BaseCommand } from './BaseCommand';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
|
@ -101,7 +100,7 @@ export class Execute extends BaseCommand {
|
||||||
throw new Error('Failed to retrieve workflow data for requested workflow');
|
throw new Error('Failed to retrieve workflow data for requested workflow');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!WorkflowHelpers.isWorkflowIdValid(workflowId)) {
|
if (!isWorkflowIdValid(workflowId)) {
|
||||||
workflowId = undefined;
|
workflowId = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -144,12 +144,16 @@ export class ExecuteBatch extends BaseCommand {
|
||||||
'econnrefused',
|
'econnrefused',
|
||||||
'missing a required parameter',
|
'missing a required parameter',
|
||||||
'insufficient credit balance',
|
'insufficient credit balance',
|
||||||
|
'internal server error',
|
||||||
|
'503',
|
||||||
|
'502',
|
||||||
|
'504',
|
||||||
|
'insufficient balance',
|
||||||
'request timed out',
|
'request timed out',
|
||||||
'status code 401',
|
'status code 401',
|
||||||
];
|
];
|
||||||
|
|
||||||
errorMessage = errorMessage.toLowerCase();
|
errorMessage = errorMessage.toLowerCase();
|
||||||
|
|
||||||
for (let i = 0; i < warningStrings.length; i++) {
|
for (let i = 0; i < warningStrings.length; i++) {
|
||||||
if (errorMessage.includes(warningStrings[i])) {
|
if (errorMessage.includes(warningStrings[i])) {
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
import { LDAP_FEATURE_NAME } from '@/Ldap/constants';
|
import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/Ldap/constants';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import { BaseCommand } from '../BaseCommand';
|
import { BaseCommand } from '../BaseCommand';
|
||||||
|
|
||||||
|
@ -17,6 +17,11 @@ export class Reset extends BaseCommand {
|
||||||
await AuthIdentity.delete({ providerType: 'ldap' });
|
await AuthIdentity.delete({ providerType: 'ldap' });
|
||||||
await User.delete({ id: In(ldapIdentities.map((i) => i.userId)) });
|
await User.delete({ id: In(ldapIdentities.map((i) => i.userId)) });
|
||||||
await Settings.delete({ key: LDAP_FEATURE_NAME });
|
await Settings.delete({ key: LDAP_FEATURE_NAME });
|
||||||
|
await Settings.insert({
|
||||||
|
key: LDAP_FEATURE_NAME,
|
||||||
|
value: JSON.stringify(LDAP_DEFAULT_CONFIGURATION),
|
||||||
|
loadOnStartup: true,
|
||||||
|
});
|
||||||
|
|
||||||
this.logger.info('Successfully reset the database to default ldap state.');
|
this.logger.info('Successfully reset the database to default ldap state.');
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,11 +24,11 @@ import * as GenericHelpers from '@/GenericHelpers';
|
||||||
import * as Server from '@/Server';
|
import * as Server from '@/Server';
|
||||||
import { TestWebhooks } from '@/TestWebhooks';
|
import { TestWebhooks } from '@/TestWebhooks';
|
||||||
import { getAllInstalledPackages } from '@/CommunityNodes/packageModel';
|
import { getAllInstalledPackages } from '@/CommunityNodes/packageModel';
|
||||||
import { handleLdapInit } from '@/Ldap/helpers';
|
|
||||||
import { EDITOR_UI_DIST_DIR, GENERATED_STATIC_DIR } from '@/constants';
|
import { EDITOR_UI_DIST_DIR, GENERATED_STATIC_DIR } from '@/constants';
|
||||||
import { eventBus } from '@/eventbus';
|
import { eventBus } from '@/eventbus';
|
||||||
import { BaseCommand } from './BaseCommand';
|
import { BaseCommand } from './BaseCommand';
|
||||||
import { InternalHooks } from '@/InternalHooks';
|
import { InternalHooks } from '@/InternalHooks';
|
||||||
|
import { License } from '@/License';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires
|
||||||
const open = require('open');
|
const open = require('open');
|
||||||
|
@ -60,7 +60,7 @@ export class Start extends BaseCommand {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
protected activeWorkflowRunner = Container.get(ActiveWorkflowRunner);
|
protected activeWorkflowRunner: ActiveWorkflowRunner;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the UI in browser
|
* Opens the UI in browser
|
||||||
|
@ -182,11 +182,27 @@ export class Start extends BaseCommand {
|
||||||
await Promise.all(files.map(compileFile));
|
await Promise.all(files.map(compileFile));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async initLicense(): Promise<void> {
|
||||||
|
const license = Container.get(License);
|
||||||
|
await license.init(this.instanceId);
|
||||||
|
|
||||||
|
const activationKey = config.getEnv('license.activationKey');
|
||||||
|
if (activationKey) {
|
||||||
|
try {
|
||||||
|
await license.activate(activationKey);
|
||||||
|
} catch (e) {
|
||||||
|
LoggerProxy.error('Could not activate license', e as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
await this.initCrashJournal();
|
await this.initCrashJournal();
|
||||||
await super.init();
|
await super.init();
|
||||||
this.logger.info('Initializing n8n process');
|
this.logger.info('Initializing n8n process');
|
||||||
|
this.activeWorkflowRunner = Container.get(ActiveWorkflowRunner);
|
||||||
|
|
||||||
|
await this.initLicense();
|
||||||
await this.initBinaryManager();
|
await this.initBinaryManager();
|
||||||
await this.initExternalHooks();
|
await this.initExternalHooks();
|
||||||
|
|
||||||
|
@ -252,11 +268,10 @@ export class Start extends BaseCommand {
|
||||||
// Optimistic approach - stop if any installation fails
|
// Optimistic approach - stop if any installation fails
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const missingPackage of missingPackages) {
|
for (const missingPackage of missingPackages) {
|
||||||
// eslint-disable-next-line no-await-in-loop
|
await this.loadNodesAndCredentials.installNpmModule(
|
||||||
void (await this.loadNodesAndCredentials.loadNpmModule(
|
|
||||||
missingPackage.packageName,
|
missingPackage.packageName,
|
||||||
missingPackage.version,
|
missingPackage.version,
|
||||||
));
|
);
|
||||||
missingPackages.delete(missingPackage);
|
missingPackages.delete(missingPackage);
|
||||||
}
|
}
|
||||||
LoggerProxy.info('Packages reinstalled successfully. Resuming regular initialization.');
|
LoggerProxy.info('Packages reinstalled successfully. Resuming regular initialization.');
|
||||||
|
@ -331,8 +346,6 @@ export class Start extends BaseCommand {
|
||||||
// Start to get active workflows and run their triggers
|
// Start to get active workflows and run their triggers
|
||||||
await this.activeWorkflowRunner.init();
|
await this.activeWorkflowRunner.init();
|
||||||
|
|
||||||
await handleLdapInit();
|
|
||||||
|
|
||||||
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}`);
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { LoggerProxy, sleep } from 'n8n-workflow';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import { ActiveExecutions } from '@/ActiveExecutions';
|
import { ActiveExecutions } from '@/ActiveExecutions';
|
||||||
import { WebhookServer } from '@/WebhookServer';
|
import { WebhookServer } from '@/WebhookServer';
|
||||||
|
import { Queue } from '@/Queue';
|
||||||
import { BaseCommand } from './BaseCommand';
|
import { BaseCommand } from './BaseCommand';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
|
|
||||||
|
@ -79,6 +80,7 @@ export class Webhook extends BaseCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
|
await Container.get(Queue).init();
|
||||||
await new WebhookServer().start();
|
await new WebhookServer().start();
|
||||||
this.logger.info('Webhook listener waiting for requests.');
|
this.logger.info('Webhook listener waiting for requests.');
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
import type PCancelable from 'p-cancelable';
|
import type PCancelable from 'p-cancelable';
|
||||||
|
import { Container } from 'typedi';
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { flags } from '@oclif/command';
|
||||||
import { WorkflowExecute } from 'n8n-core';
|
import { WorkflowExecute } from 'n8n-core';
|
||||||
|
@ -15,7 +16,8 @@ import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'
|
||||||
import { PermissionChecker } from '@/UserManagement/PermissionChecker';
|
import { PermissionChecker } from '@/UserManagement/PermissionChecker';
|
||||||
|
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import * as Queue from '@/Queue';
|
import type { Job, JobId, JobQueue, JobResponse, WebhookResponse } from '@/Queue';
|
||||||
|
import { Queue } from '@/Queue';
|
||||||
import { getWorkflowOwner } from '@/UserManagement/UserManagementHelper';
|
import { getWorkflowOwner } from '@/UserManagement/UserManagementHelper';
|
||||||
import { generateFailedExecutionFromError } from '@/WorkflowHelpers';
|
import { generateFailedExecutionFromError } from '@/WorkflowHelpers';
|
||||||
import { N8N_VERSION } from '@/constants';
|
import { N8N_VERSION } from '@/constants';
|
||||||
|
@ -38,7 +40,7 @@ export class Worker extends BaseCommand {
|
||||||
[key: string]: PCancelable<IRun>;
|
[key: string]: PCancelable<IRun>;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
static jobQueue: Queue.JobQueue;
|
static jobQueue: JobQueue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop n8n in a graceful way.
|
* Stop n8n in a graceful way.
|
||||||
|
@ -86,7 +88,7 @@ export class Worker extends BaseCommand {
|
||||||
await this.exitSuccessFully();
|
await this.exitSuccessFully();
|
||||||
}
|
}
|
||||||
|
|
||||||
async runJob(job: Queue.Job, nodeTypes: INodeTypes): Promise<Queue.JobResponse> {
|
async runJob(job: Job, nodeTypes: INodeTypes): Promise<JobResponse> {
|
||||||
const { executionId, loadStaticData } = job.data;
|
const { executionId, loadStaticData } = job.data;
|
||||||
const executionDb = await Db.collections.Execution.findOneBy({ id: executionId });
|
const executionDb = await Db.collections.Execution.findOneBy({ id: executionId });
|
||||||
|
|
||||||
|
@ -125,14 +127,9 @@ export class Worker extends BaseCommand {
|
||||||
staticData = workflowData.staticData;
|
staticData = workflowData.staticData;
|
||||||
}
|
}
|
||||||
|
|
||||||
let workflowTimeout = config.getEnv('executions.timeout'); // initialize with default
|
const workflowSettings = currentExecutionDb.workflowData.settings ?? {};
|
||||||
if (
|
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
|
let workflowTimeout = workflowSettings.executionTimeout ?? config.getEnv('executions.timeout'); // initialize with default
|
||||||
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;
|
||||||
if (workflowTimeout > 0) {
|
if (workflowTimeout > 0) {
|
||||||
|
@ -179,7 +176,7 @@ export class Worker extends BaseCommand {
|
||||||
|
|
||||||
additionalData.hooks.hookFunctions.sendResponse = [
|
additionalData.hooks.hookFunctions.sendResponse = [
|
||||||
async (response: IExecuteResponsePromiseData): Promise<void> => {
|
async (response: IExecuteResponsePromiseData): Promise<void> => {
|
||||||
const progress: Queue.WebhookResponse = {
|
const progress: WebhookResponse = {
|
||||||
executionId,
|
executionId,
|
||||||
response: WebhookHelpers.encodeWebhookResponse(response),
|
response: WebhookHelpers.encodeWebhookResponse(response),
|
||||||
};
|
};
|
||||||
|
@ -238,7 +235,8 @@ export class Worker extends BaseCommand {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
const redisConnectionTimeoutLimit = config.getEnv('queue.bull.redis.timeoutThreshold');
|
const redisConnectionTimeoutLimit = config.getEnv('queue.bull.redis.timeoutThreshold');
|
||||||
|
|
||||||
const queue = await Queue.getInstance();
|
const queue = Container.get(Queue);
|
||||||
|
await queue.init();
|
||||||
Worker.jobQueue = queue.getBullObjectInstance();
|
Worker.jobQueue = queue.getBullObjectInstance();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
Worker.jobQueue.process(flags.concurrency, async (job) => this.runJob(job, this.nodeTypes));
|
Worker.jobQueue.process(flags.concurrency, async (job) => this.runJob(job, this.nodeTypes));
|
||||||
|
@ -248,7 +246,7 @@ export class Worker extends BaseCommand {
|
||||||
this.logger.info(` * Concurrency: ${flags.concurrency}`);
|
this.logger.info(` * Concurrency: ${flags.concurrency}`);
|
||||||
this.logger.info('');
|
this.logger.info('');
|
||||||
|
|
||||||
Worker.jobQueue.on('global:progress', (jobId: Queue.JobId, progress) => {
|
Worker.jobQueue.on('global:progress', (jobId: JobId, progress) => {
|
||||||
// Progress of a job got updated which does get used
|
// Progress of a job got updated which does get used
|
||||||
// to communicate that a job got canceled.
|
// to communicate that a job got canceled.
|
||||||
|
|
||||||
|
|
|
@ -24,11 +24,7 @@ if (inE2ETests) {
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = convict(schema);
|
const config = convict(schema, { args: [] });
|
||||||
|
|
||||||
if (inE2ETests) {
|
|
||||||
config.set('enterprise.features.sharing', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
config.getEnv = config.get;
|
config.getEnv = config.get;
|
||||||
|
|
|
@ -990,31 +990,6 @@ export const schema = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
enterprise: {
|
|
||||||
features: {
|
|
||||||
sharing: {
|
|
||||||
format: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
ldap: {
|
|
||||||
format: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
saml: {
|
|
||||||
format: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
logStreaming: {
|
|
||||||
format: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
advancedExecutionFilters: {
|
|
||||||
format: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
sso: {
|
sso: {
|
||||||
justInTimeProvisioning: {
|
justInTimeProvisioning: {
|
||||||
format: Boolean,
|
format: Boolean,
|
||||||
|
@ -1166,6 +1141,12 @@ export const schema = {
|
||||||
env: 'N8N_LICENSE_TENANT_ID',
|
env: 'N8N_LICENSE_TENANT_ID',
|
||||||
doc: 'Tenant id used by the license manager',
|
doc: 'Tenant id used by the license manager',
|
||||||
},
|
},
|
||||||
|
cert: {
|
||||||
|
format: String,
|
||||||
|
default: '',
|
||||||
|
env: 'N8N_LICENSE_CERT',
|
||||||
|
doc: 'Ephemeral license certificate',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
hideUsagePage: {
|
hideUsagePage: {
|
||||||
|
|
|
@ -18,7 +18,7 @@ export const CUSTOM_API_CALL_KEY = '__CUSTOM_API_CALL__';
|
||||||
|
|
||||||
export const CLI_DIR = resolve(__dirname, '..');
|
export const CLI_DIR = resolve(__dirname, '..');
|
||||||
export const TEMPLATES_DIR = join(CLI_DIR, 'templates');
|
export const TEMPLATES_DIR = join(CLI_DIR, 'templates');
|
||||||
export const NODES_BASE_DIR = join(CLI_DIR, '..', 'nodes-base');
|
export const NODES_BASE_DIR = dirname(require.resolve('n8n-nodes-base'));
|
||||||
export const GENERATED_STATIC_DIR = join(UserSettings.getUserHome(), '.cache/n8n/public');
|
export const GENERATED_STATIC_DIR = join(UserSettings.getUserHome(), '.cache/n8n/public');
|
||||||
export const EDITOR_UI_DIST_DIR = join(dirname(require.resolve('n8n-editor-ui')), 'dist');
|
export const EDITOR_UI_DIST_DIR = join(dirname(require.resolve('n8n-editor-ui')), 'dist');
|
||||||
|
|
||||||
|
@ -45,6 +45,7 @@ export const RESPONSE_ERROR_MESSAGES = {
|
||||||
PACKAGE_NOT_FOUND: 'Package not found in npm',
|
PACKAGE_NOT_FOUND: 'Package not found in npm',
|
||||||
PACKAGE_VERSION_NOT_FOUND: 'The specified package version was not found',
|
PACKAGE_VERSION_NOT_FOUND: 'The specified package version was not found',
|
||||||
PACKAGE_DOES_NOT_CONTAIN_NODES: 'The specified package does not contain any nodes',
|
PACKAGE_DOES_NOT_CONTAIN_NODES: 'The specified package does not contain any nodes',
|
||||||
|
PACKAGE_LOADING_FAILED: 'The specified package could not be loaded',
|
||||||
DISK_IS_FULL: 'There appears to be insufficient disk space',
|
DISK_IS_FULL: 'There appears to be insufficient disk space',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -19,8 +19,10 @@ import type {
|
||||||
} from '@/Interfaces';
|
} from '@/Interfaces';
|
||||||
import { handleEmailLogin, handleLdapLogin } from '@/auth';
|
import { handleEmailLogin, handleLdapLogin } from '@/auth';
|
||||||
import type { PostHogClient } from '@/posthog';
|
import type { PostHogClient } from '@/posthog';
|
||||||
import { isSamlCurrentAuthenticationMethod } from '../sso/ssoHelpers';
|
import {
|
||||||
import { SamlUrls } from '../sso/saml/constants';
|
isLdapCurrentAuthenticationMethod,
|
||||||
|
isSamlCurrentAuthenticationMethod,
|
||||||
|
} from '@/sso/ssoHelpers';
|
||||||
|
|
||||||
@RestController()
|
@RestController()
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
|
@ -73,19 +75,12 @@ export class AuthController {
|
||||||
if (preliminaryUser?.globalRole?.name === 'owner') {
|
if (preliminaryUser?.globalRole?.name === 'owner') {
|
||||||
user = preliminaryUser;
|
user = preliminaryUser;
|
||||||
} else {
|
} else {
|
||||||
// TODO:SAML - uncomment this block when we have a way to redirect users to the SSO flow
|
throw new AuthError('SAML is enabled, please log in with SAML');
|
||||||
// if (doRedirectUsersFromLoginToSsoFlow()) {
|
|
||||||
res.redirect(SamlUrls.restInitSSO);
|
|
||||||
return;
|
|
||||||
// return withFeatureFlags(this.postHog, sanitizeUser(preliminaryUser));
|
|
||||||
// } else {
|
|
||||||
// throw new AuthError(
|
|
||||||
// 'Login with username and password is disabled due to SAML being the default authentication method. Please use SAML to log in.',
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
} else if (isLdapCurrentAuthenticationMethod()) {
|
||||||
|
user = await handleLdapLogin(email, password);
|
||||||
} else {
|
} else {
|
||||||
user = (await handleLdapLogin(email, password)) ?? (await handleEmailLogin(email, password));
|
user = await handleEmailLogin(email, password);
|
||||||
}
|
}
|
||||||
if (user) {
|
if (user) {
|
||||||
await issueCookie(res, user);
|
await issueCookie(res, user);
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
export { AuthController } from './auth.controller';
|
export { AuthController } from './auth.controller';
|
||||||
|
export { LdapController } from './ldap.controller';
|
||||||
export { MeController } from './me.controller';
|
export { MeController } from './me.controller';
|
||||||
|
export { NodesController } from './nodes.controller';
|
||||||
|
export { NodeTypesController } from './nodeTypes.controller';
|
||||||
export { OwnerController } from './owner.controller';
|
export { OwnerController } from './owner.controller';
|
||||||
export { PasswordResetController } from './passwordReset.controller';
|
export { PasswordResetController } from './passwordReset.controller';
|
||||||
|
export { TagsController } from './tags.controller';
|
||||||
export { TranslationController } from './translation.controller';
|
export { TranslationController } from './translation.controller';
|
||||||
export { UsersController } from './users.controller';
|
export { UsersController } from './users.controller';
|
||||||
|
|
65
packages/cli/src/controllers/ldap.controller.ts
Normal file
65
packages/cli/src/controllers/ldap.controller.ts
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import pick from 'lodash.pick';
|
||||||
|
import { Get, Post, Put, RestController } from '@/decorators';
|
||||||
|
import { getLdapConfig, getLdapSynchronizations, updateLdapConfig } from '@/Ldap/helpers';
|
||||||
|
import { LdapService } from '@/Ldap/LdapService.ee';
|
||||||
|
import { LdapSync } from '@/Ldap/LdapSync.ee';
|
||||||
|
import { LdapConfiguration } from '@/Ldap/types';
|
||||||
|
import { BadRequestError } from '@/ResponseHelper';
|
||||||
|
import { NON_SENSIBLE_LDAP_CONFIG_PROPERTIES } from '@/Ldap/constants';
|
||||||
|
import { InternalHooks } from '@/InternalHooks';
|
||||||
|
|
||||||
|
@RestController('/ldap')
|
||||||
|
export class LdapController {
|
||||||
|
constructor(
|
||||||
|
private ldapService: LdapService,
|
||||||
|
private ldapSync: LdapSync,
|
||||||
|
private internalHooks: InternalHooks,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Get('/config')
|
||||||
|
async getConfig() {
|
||||||
|
return getLdapConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/test-connection')
|
||||||
|
async testConnection() {
|
||||||
|
try {
|
||||||
|
await this.ldapService.testConnection();
|
||||||
|
} catch (error) {
|
||||||
|
throw new BadRequestError((error as { message: string }).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put('/config')
|
||||||
|
async updateConfig(req: LdapConfiguration.Update) {
|
||||||
|
try {
|
||||||
|
await updateLdapConfig(req.body);
|
||||||
|
} catch (error) {
|
||||||
|
throw new BadRequestError((error as { message: string }).message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await getLdapConfig();
|
||||||
|
|
||||||
|
void this.internalHooks.onUserUpdatedLdapSettings({
|
||||||
|
user_id: req.user.id,
|
||||||
|
...pick(data, NON_SENSIBLE_LDAP_CONFIG_PROPERTIES),
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/sync')
|
||||||
|
async getLdapSync(req: LdapConfiguration.GetSync) {
|
||||||
|
const { page = '0', perPage = '20' } = req.query;
|
||||||
|
return getLdapSynchronizations(parseInt(page, 10), parseInt(perPage, 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/sync')
|
||||||
|
async syncLdap(req: LdapConfiguration.Sync) {
|
||||||
|
try {
|
||||||
|
await this.ldapSync.run(req.body.type);
|
||||||
|
} catch (error) {
|
||||||
|
throw new BadRequestError((error as { message: string }).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,39 +1,43 @@
|
||||||
import express from 'express';
|
|
||||||
import { readFile } from 'fs/promises';
|
import { readFile } from 'fs/promises';
|
||||||
import get from 'lodash.get';
|
import get from 'lodash.get';
|
||||||
|
import { Request } from 'express';
|
||||||
import type { INodeTypeDescription, INodeTypeNameVersion } from 'n8n-workflow';
|
import type { INodeTypeDescription, INodeTypeNameVersion } from 'n8n-workflow';
|
||||||
|
import { Post, RestController } from '@/decorators';
|
||||||
import config from '@/config';
|
|
||||||
import { NodeTypes } from '@/NodeTypes';
|
|
||||||
import * as ResponseHelper from '@/ResponseHelper';
|
|
||||||
import { getNodeTranslationPath } from '@/TranslationHelpers';
|
import { getNodeTranslationPath } from '@/TranslationHelpers';
|
||||||
import { Container } from 'typedi';
|
import type { Config } from '@/config';
|
||||||
|
import type { NodeTypes } from '@/NodeTypes';
|
||||||
|
|
||||||
export const nodeTypesController = express.Router();
|
@RestController('/node-types')
|
||||||
|
export class NodeTypesController {
|
||||||
|
private readonly config: Config;
|
||||||
|
|
||||||
// Returns node information based on node names and versions
|
private readonly nodeTypes: NodeTypes;
|
||||||
nodeTypesController.post(
|
|
||||||
'/',
|
constructor({ config, nodeTypes }: { config: Config; nodeTypes: NodeTypes }) {
|
||||||
ResponseHelper.send(async (req: express.Request): Promise<INodeTypeDescription[]> => {
|
this.config = config;
|
||||||
|
this.nodeTypes = nodeTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/')
|
||||||
|
async getNodeInfo(req: Request) {
|
||||||
const nodeInfos = get(req, 'body.nodeInfos', []) as INodeTypeNameVersion[];
|
const nodeInfos = get(req, 'body.nodeInfos', []) as INodeTypeNameVersion[];
|
||||||
|
|
||||||
const defaultLocale = config.getEnv('defaultLocale');
|
const defaultLocale = this.config.getEnv('defaultLocale');
|
||||||
|
|
||||||
if (defaultLocale === 'en') {
|
if (defaultLocale === 'en') {
|
||||||
return nodeInfos.reduce<INodeTypeDescription[]>((acc, { name, version }) => {
|
return nodeInfos.reduce<INodeTypeDescription[]>((acc, { name, version }) => {
|
||||||
const { description } = Container.get(NodeTypes).getByNameAndVersion(name, version);
|
const { description } = this.nodeTypes.getByNameAndVersion(name, version);
|
||||||
acc.push(description);
|
acc.push(description);
|
||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function populateTranslation(
|
const populateTranslation = async (
|
||||||
name: string,
|
name: string,
|
||||||
version: number,
|
version: number,
|
||||||
nodeTypes: INodeTypeDescription[],
|
nodeTypes: INodeTypeDescription[],
|
||||||
) {
|
) => {
|
||||||
const { description, sourcePath } = Container.get(NodeTypes).getWithSourcePath(name, version);
|
const { description, sourcePath } = this.nodeTypes.getWithSourcePath(name, version);
|
||||||
const translationPath = await getNodeTranslationPath({
|
const translationPath = await getNodeTranslationPath({
|
||||||
nodeSourcePath: sourcePath,
|
nodeSourcePath: sourcePath,
|
||||||
longNodeType: description.name,
|
longNodeType: description.name,
|
||||||
|
@ -44,12 +48,12 @@ nodeTypesController.post(
|
||||||
const translation = await readFile(translationPath, 'utf8');
|
const translation = await readFile(translationPath, 'utf8');
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
description.translation = JSON.parse(translation);
|
description.translation = JSON.parse(translation);
|
||||||
} catch (error) {
|
} catch {
|
||||||
// ignore - no translation exists at path
|
// ignore - no translation exists at path
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeTypes.push(description);
|
nodeTypes.push(description);
|
||||||
}
|
};
|
||||||
|
|
||||||
const nodeTypes: INodeTypeDescription[] = [];
|
const nodeTypes: INodeTypeDescription[] = [];
|
||||||
|
|
||||||
|
@ -60,5 +64,5 @@ nodeTypesController.post(
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
|
||||||
return nodeTypes;
|
return nodeTypes;
|
||||||
}),
|
}
|
||||||
);
|
}
|
|
@ -1,10 +1,12 @@
|
||||||
import express from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import type { PublicInstalledPackage } from 'n8n-workflow';
|
import {
|
||||||
|
RESPONSE_ERROR_MESSAGES,
|
||||||
import config from '@/config';
|
STARTER_TEMPLATE_NAME,
|
||||||
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
UNKNOWN_FAILURE_REASON,
|
||||||
import * as ResponseHelper from '@/ResponseHelper';
|
} from '@/constants';
|
||||||
|
import { Delete, Get, Middleware, Patch, Post, RestController } from '@/decorators';
|
||||||
|
import { NodeRequest } from '@/requests';
|
||||||
|
import { BadRequestError, InternalServerError } from '@/ResponseHelper';
|
||||||
import {
|
import {
|
||||||
checkNpmPackageStatus,
|
checkNpmPackageStatus,
|
||||||
executeCommand,
|
executeCommand,
|
||||||
|
@ -22,57 +24,50 @@ import {
|
||||||
getAllInstalledPackages,
|
getAllInstalledPackages,
|
||||||
isPackageInstalled,
|
isPackageInstalled,
|
||||||
} from '@/CommunityNodes/packageModel';
|
} from '@/CommunityNodes/packageModel';
|
||||||
import {
|
|
||||||
RESPONSE_ERROR_MESSAGES,
|
|
||||||
STARTER_TEMPLATE_NAME,
|
|
||||||
UNKNOWN_FAILURE_REASON,
|
|
||||||
} from '@/constants';
|
|
||||||
import { isAuthenticatedRequest } from '@/UserManagement/UserManagementHelper';
|
|
||||||
|
|
||||||
import type { InstalledPackages } from '@db/entities/InstalledPackages';
|
import type { InstalledPackages } from '@db/entities/InstalledPackages';
|
||||||
import type { CommunityPackages } from '@/Interfaces';
|
import type { CommunityPackages } from '@/Interfaces';
|
||||||
import type { NodeRequest } from '@/requests';
|
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
||||||
import { Push } from '@/push';
|
|
||||||
import { Container } from 'typedi';
|
|
||||||
import { InternalHooks } from '@/InternalHooks';
|
import { InternalHooks } from '@/InternalHooks';
|
||||||
|
import { Push } from '@/push';
|
||||||
|
import { Config } from '@/config';
|
||||||
|
import { isAuthenticatedRequest } from '@/UserManagement/UserManagementHelper';
|
||||||
|
|
||||||
const { PACKAGE_NOT_INSTALLED, PACKAGE_NAME_NOT_PROVIDED } = RESPONSE_ERROR_MESSAGES;
|
const { PACKAGE_NOT_INSTALLED, PACKAGE_NAME_NOT_PROVIDED } = RESPONSE_ERROR_MESSAGES;
|
||||||
|
|
||||||
export const nodesController = express.Router();
|
@RestController('/nodes')
|
||||||
|
export class NodesController {
|
||||||
|
constructor(
|
||||||
|
private config: Config,
|
||||||
|
private loadNodesAndCredentials: LoadNodesAndCredentials,
|
||||||
|
private push: Push,
|
||||||
|
private internalHooks: InternalHooks,
|
||||||
|
) {}
|
||||||
|
|
||||||
nodesController.use((req, res, next) => {
|
// TODO: move this into a new decorator `@Authorized`
|
||||||
if (!isAuthenticatedRequest(req) || req.user.globalRole.name !== 'owner') {
|
@Middleware()
|
||||||
|
checkIfOwner(req: Request, res: Response, next: NextFunction) {
|
||||||
|
if (!isAuthenticatedRequest(req) || req.user.globalRole.name !== 'owner')
|
||||||
res.status(403).json({ status: 'error', message: 'Unauthorized' });
|
res.status(403).json({ status: 'error', message: 'Unauthorized' });
|
||||||
return;
|
else next();
|
||||||
}
|
}
|
||||||
|
|
||||||
next();
|
// TODO: move this into a new decorator `@IfConfig('executions.mode', 'queue')`
|
||||||
});
|
@Middleware()
|
||||||
|
checkIfCommunityNodesEnabled(req: Request, res: Response, next: NextFunction) {
|
||||||
nodesController.use((req, res, next) => {
|
if (this.config.getEnv('executions.mode') === 'queue' && req.method !== 'GET')
|
||||||
if (config.getEnv('executions.mode') === 'queue' && req.method !== 'GET') {
|
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: 'Package management is disabled when running in "queue" mode',
|
message: 'Package management is disabled when running in "queue" mode',
|
||||||
});
|
});
|
||||||
return;
|
else next();
|
||||||
}
|
}
|
||||||
|
|
||||||
next();
|
@Post('/')
|
||||||
});
|
async installPackage(req: NodeRequest.Post) {
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /nodes
|
|
||||||
*
|
|
||||||
* Install an n8n community package
|
|
||||||
*/
|
|
||||||
nodesController.post(
|
|
||||||
'/',
|
|
||||||
ResponseHelper.send(async (req: NodeRequest.Post) => {
|
|
||||||
const { name } = req.body;
|
const { name } = req.body;
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
throw new ResponseHelper.BadRequestError(PACKAGE_NAME_NOT_PROVIDED);
|
throw new BadRequestError(PACKAGE_NAME_NOT_PROVIDED);
|
||||||
}
|
}
|
||||||
|
|
||||||
let parsed: CommunityPackages.ParsedPackageName;
|
let parsed: CommunityPackages.ParsedPackageName;
|
||||||
|
@ -80,13 +75,13 @@ nodesController.post(
|
||||||
try {
|
try {
|
||||||
parsed = parseNpmPackageName(name);
|
parsed = parseNpmPackageName(name);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new ResponseHelper.BadRequestError(
|
throw new BadRequestError(
|
||||||
error instanceof Error ? error.message : 'Failed to parse package name',
|
error instanceof Error ? error.message : 'Failed to parse package name',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsed.packageName === STARTER_TEMPLATE_NAME) {
|
if (parsed.packageName === STARTER_TEMPLATE_NAME) {
|
||||||
throw new ResponseHelper.BadRequestError(
|
throw new BadRequestError(
|
||||||
[
|
[
|
||||||
`Package "${parsed.packageName}" is only a template`,
|
`Package "${parsed.packageName}" is only a template`,
|
||||||
'Please enter an actual package to install',
|
'Please enter an actual package to install',
|
||||||
|
@ -98,7 +93,7 @@ nodesController.post(
|
||||||
const hasLoaded = hasPackageLoaded(name);
|
const hasLoaded = hasPackageLoaded(name);
|
||||||
|
|
||||||
if (isInstalled && hasLoaded) {
|
if (isInstalled && hasLoaded) {
|
||||||
throw new ResponseHelper.BadRequestError(
|
throw new BadRequestError(
|
||||||
[
|
[
|
||||||
`Package "${parsed.packageName}" is already installed`,
|
`Package "${parsed.packageName}" is already installed`,
|
||||||
'To update it, click the corresponding button in the UI',
|
'To update it, click the corresponding button in the UI',
|
||||||
|
@ -109,22 +104,19 @@ nodesController.post(
|
||||||
const packageStatus = await checkNpmPackageStatus(name);
|
const packageStatus = await checkNpmPackageStatus(name);
|
||||||
|
|
||||||
if (packageStatus.status !== 'OK') {
|
if (packageStatus.status !== 'OK') {
|
||||||
throw new ResponseHelper.BadRequestError(
|
throw new BadRequestError(`Package "${name}" is banned so it cannot be installed`);
|
||||||
`Package "${name}" is banned so it cannot be installed`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let installedPackage: InstalledPackages;
|
let installedPackage: InstalledPackages;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
installedPackage = await Container.get(LoadNodesAndCredentials).loadNpmModule(
|
installedPackage = await this.loadNodesAndCredentials.installNpmModule(
|
||||||
parsed.packageName,
|
parsed.packageName,
|
||||||
parsed.version,
|
parsed.version,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON;
|
const errorMessage = error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON;
|
||||||
|
|
||||||
void Container.get(InternalHooks).onCommunityPackageInstallFinished({
|
void this.internalHooks.onCommunityPackageInstallFinished({
|
||||||
user: req.user,
|
user: req.user,
|
||||||
input_string: name,
|
input_string: name,
|
||||||
package_name: parsed.packageName,
|
package_name: parsed.packageName,
|
||||||
|
@ -133,26 +125,26 @@ nodesController.post(
|
||||||
failure_reason: errorMessage,
|
failure_reason: errorMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
const message = [`Error loading package "${name}"`, errorMessage].join(':');
|
let message = [`Error loading package "${name}" `, errorMessage].join(':');
|
||||||
|
if (error instanceof Error && error.cause instanceof Error) {
|
||||||
|
message += `\nCause: ${error.cause.message}`;
|
||||||
|
}
|
||||||
|
|
||||||
const clientError = error instanceof Error ? isClientError(error) : false;
|
const clientError = error instanceof Error ? isClientError(error) : false;
|
||||||
|
throw new (clientError ? BadRequestError : InternalServerError)(message);
|
||||||
throw new ResponseHelper[clientError ? 'BadRequestError' : 'InternalServerError'](message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasLoaded) removePackageFromMissingList(name);
|
if (!hasLoaded) removePackageFromMissingList(name);
|
||||||
|
|
||||||
const pushInstance = Container.get(Push);
|
|
||||||
|
|
||||||
// broadcast to connected frontends that node list has been updated
|
// broadcast to connected frontends that node list has been updated
|
||||||
installedPackage.installedNodes.forEach((node) => {
|
installedPackage.installedNodes.forEach((node) => {
|
||||||
pushInstance.send('reloadNodeType', {
|
this.push.send('reloadNodeType', {
|
||||||
name: node.type,
|
name: node.type,
|
||||||
version: node.latestVersion,
|
version: node.latestVersion,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
void Container.get(InternalHooks).onCommunityPackageInstallFinished({
|
void this.internalHooks.onCommunityPackageInstallFinished({
|
||||||
user: req.user,
|
user: req.user,
|
||||||
input_string: name,
|
input_string: name,
|
||||||
package_name: parsed.packageName,
|
package_name: parsed.packageName,
|
||||||
|
@ -164,17 +156,10 @@ nodesController.post(
|
||||||
});
|
});
|
||||||
|
|
||||||
return installedPackage;
|
return installedPackage;
|
||||||
}),
|
}
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
@Get('/')
|
||||||
* GET /nodes
|
async getInstalledPackages() {
|
||||||
*
|
|
||||||
* Retrieve list of installed n8n community packages
|
|
||||||
*/
|
|
||||||
nodesController.get(
|
|
||||||
'/',
|
|
||||||
ResponseHelper.send(async (): Promise<PublicInstalledPackage[]> => {
|
|
||||||
const installedPackages = await getAllInstalledPackages();
|
const installedPackages = await getAllInstalledPackages();
|
||||||
|
|
||||||
if (installedPackages.length === 0) return [];
|
if (installedPackages.length === 0) return [];
|
||||||
|
@ -188,7 +173,6 @@ nodesController.get(
|
||||||
// when there are updates, npm exits with code 1
|
// when there are updates, npm exits with code 1
|
||||||
// when there are no updates, command succeeds
|
// when there are no updates, command succeeds
|
||||||
// https://github.com/npm/rfcs/issues/473
|
// https://github.com/npm/rfcs/issues/473
|
||||||
|
|
||||||
if (isNpmError(error) && error.code === 1) {
|
if (isNpmError(error) && error.code === 1) {
|
||||||
pendingUpdates = JSON.parse(error.stdout) as CommunityPackages.AvailableUpdates;
|
pendingUpdates = JSON.parse(error.stdout) as CommunityPackages.AvailableUpdates;
|
||||||
}
|
}
|
||||||
|
@ -197,31 +181,21 @@ nodesController.get(
|
||||||
let hydratedPackages = matchPackagesWithUpdates(installedPackages, pendingUpdates);
|
let hydratedPackages = matchPackagesWithUpdates(installedPackages, pendingUpdates);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const missingPackages = config.get('nodes.packagesMissing') as string | undefined;
|
const missingPackages = this.config.get('nodes.packagesMissing') as string | undefined;
|
||||||
|
|
||||||
if (missingPackages) {
|
if (missingPackages) {
|
||||||
hydratedPackages = matchMissingPackages(hydratedPackages, missingPackages);
|
hydratedPackages = matchMissingPackages(hydratedPackages, missingPackages);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {}
|
||||||
// Do nothing if setting is missing
|
|
||||||
}
|
|
||||||
|
|
||||||
return hydratedPackages;
|
return hydratedPackages;
|
||||||
}),
|
}
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
@Delete('/')
|
||||||
* DELETE /nodes
|
async uninstallPackage(req: NodeRequest.Delete) {
|
||||||
*
|
|
||||||
* Uninstall an installed n8n community package
|
|
||||||
*/
|
|
||||||
nodesController.delete(
|
|
||||||
'/',
|
|
||||||
ResponseHelper.send(async (req: NodeRequest.Delete) => {
|
|
||||||
const { name } = req.query;
|
const { name } = req.query;
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
throw new ResponseHelper.BadRequestError(PACKAGE_NAME_NOT_PROVIDED);
|
throw new BadRequestError(PACKAGE_NAME_NOT_PROVIDED);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -229,37 +203,35 @@ nodesController.delete(
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON;
|
const message = error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON;
|
||||||
|
|
||||||
throw new ResponseHelper.BadRequestError(message);
|
throw new BadRequestError(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
const installedPackage = await findInstalledPackage(name);
|
const installedPackage = await findInstalledPackage(name);
|
||||||
|
|
||||||
if (!installedPackage) {
|
if (!installedPackage) {
|
||||||
throw new ResponseHelper.BadRequestError(PACKAGE_NOT_INSTALLED);
|
throw new BadRequestError(PACKAGE_NOT_INSTALLED);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Container.get(LoadNodesAndCredentials).removeNpmModule(name, installedPackage);
|
await this.loadNodesAndCredentials.removeNpmModule(name, installedPackage);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = [
|
const message = [
|
||||||
`Error removing package "${name}"`,
|
`Error removing package "${name}"`,
|
||||||
error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON,
|
error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON,
|
||||||
].join(':');
|
].join(':');
|
||||||
|
|
||||||
throw new ResponseHelper.InternalServerError(message);
|
throw new InternalServerError(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pushInstance = Container.get(Push);
|
|
||||||
|
|
||||||
// broadcast to connected frontends that node list has been updated
|
// broadcast to connected frontends that node list has been updated
|
||||||
installedPackage.installedNodes.forEach((node) => {
|
installedPackage.installedNodes.forEach((node) => {
|
||||||
pushInstance.send('removeNodeType', {
|
this.push.send('removeNodeType', {
|
||||||
name: node.type,
|
name: node.type,
|
||||||
version: node.latestVersion,
|
version: node.latestVersion,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
void Container.get(InternalHooks).onCommunityPackageDeleteFinished({
|
void this.internalHooks.onCommunityPackageDeleteFinished({
|
||||||
user: req.user,
|
user: req.user,
|
||||||
package_name: name,
|
package_name: name,
|
||||||
package_version: installedPackage.installedVersion,
|
package_version: installedPackage.installedVersion,
|
||||||
|
@ -267,53 +239,44 @@ nodesController.delete(
|
||||||
package_author: installedPackage.authorName,
|
package_author: installedPackage.authorName,
|
||||||
package_author_email: installedPackage.authorEmail,
|
package_author_email: installedPackage.authorEmail,
|
||||||
});
|
});
|
||||||
}),
|
}
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
@Patch('/')
|
||||||
* PATCH /nodes
|
async updatePackage(req: NodeRequest.Update) {
|
||||||
*
|
|
||||||
* Update an installed n8n community package
|
|
||||||
*/
|
|
||||||
nodesController.patch(
|
|
||||||
'/',
|
|
||||||
ResponseHelper.send(async (req: NodeRequest.Update) => {
|
|
||||||
const { name } = req.body;
|
const { name } = req.body;
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
throw new ResponseHelper.BadRequestError(PACKAGE_NAME_NOT_PROVIDED);
|
throw new BadRequestError(PACKAGE_NAME_NOT_PROVIDED);
|
||||||
}
|
}
|
||||||
|
|
||||||
const previouslyInstalledPackage = await findInstalledPackage(name);
|
const previouslyInstalledPackage = await findInstalledPackage(name);
|
||||||
|
|
||||||
if (!previouslyInstalledPackage) {
|
if (!previouslyInstalledPackage) {
|
||||||
throw new ResponseHelper.BadRequestError(PACKAGE_NOT_INSTALLED);
|
throw new BadRequestError(PACKAGE_NOT_INSTALLED);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newInstalledPackage = await Container.get(LoadNodesAndCredentials).updateNpmModule(
|
const newInstalledPackage = await this.loadNodesAndCredentials.updateNpmModule(
|
||||||
parseNpmPackageName(name).packageName,
|
parseNpmPackageName(name).packageName,
|
||||||
previouslyInstalledPackage,
|
previouslyInstalledPackage,
|
||||||
);
|
);
|
||||||
|
|
||||||
const pushInstance = Container.get(Push);
|
|
||||||
|
|
||||||
// broadcast to connected frontends that node list has been updated
|
// broadcast to connected frontends that node list has been updated
|
||||||
previouslyInstalledPackage.installedNodes.forEach((node) => {
|
previouslyInstalledPackage.installedNodes.forEach((node) => {
|
||||||
pushInstance.send('removeNodeType', {
|
this.push.send('removeNodeType', {
|
||||||
name: node.type,
|
name: node.type,
|
||||||
version: node.latestVersion,
|
version: node.latestVersion,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
newInstalledPackage.installedNodes.forEach((node) => {
|
newInstalledPackage.installedNodes.forEach((node) => {
|
||||||
pushInstance.send('reloadNodeType', {
|
this.push.send('reloadNodeType', {
|
||||||
name: node.name,
|
name: node.name,
|
||||||
version: node.latestVersion,
|
version: node.latestVersion,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
void Container.get(InternalHooks).onCommunityPackageUpdateFinished({
|
void this.internalHooks.onCommunityPackageUpdateFinished({
|
||||||
user: req.user,
|
user: req.user,
|
||||||
package_name: name,
|
package_name: name,
|
||||||
package_version_current: previouslyInstalledPackage.installedVersion,
|
package_version_current: previouslyInstalledPackage.installedVersion,
|
||||||
|
@ -326,8 +289,7 @@ nodesController.patch(
|
||||||
return newInstalledPackage;
|
return newInstalledPackage;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
previouslyInstalledPackage.installedNodes.forEach((node) => {
|
previouslyInstalledPackage.installedNodes.forEach((node) => {
|
||||||
const pushInstance = Container.get(Push);
|
this.push.send('removeNodeType', {
|
||||||
pushInstance.send('removeNodeType', {
|
|
||||||
name: node.type,
|
name: node.type,
|
||||||
version: node.latestVersion,
|
version: node.latestVersion,
|
||||||
});
|
});
|
||||||
|
@ -338,7 +300,7 @@ nodesController.patch(
|
||||||
error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON,
|
error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON,
|
||||||
].join(':');
|
].join(':');
|
||||||
|
|
||||||
throw new ResponseHelper.InternalServerError(message);
|
throw new InternalServerError(message);
|
||||||
}
|
}
|
||||||
}),
|
}
|
||||||
);
|
}
|
|
@ -14,7 +14,7 @@ import {
|
||||||
hashPassword,
|
hashPassword,
|
||||||
validatePassword,
|
validatePassword,
|
||||||
} from '@/UserManagement/UserManagementHelper';
|
} from '@/UserManagement/UserManagementHelper';
|
||||||
import * as UserManagementMailer from '@/UserManagement/email';
|
import type { UserManagementMailer } from '@/UserManagement/email';
|
||||||
|
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import type { ILogger } from 'n8n-workflow';
|
import type { ILogger } from 'n8n-workflow';
|
||||||
|
@ -35,6 +35,8 @@ export class PasswordResetController {
|
||||||
|
|
||||||
private readonly internalHooks: IInternalHooksClass;
|
private readonly internalHooks: IInternalHooksClass;
|
||||||
|
|
||||||
|
private readonly mailer: UserManagementMailer;
|
||||||
|
|
||||||
private readonly userRepository: Repository<User>;
|
private readonly userRepository: Repository<User>;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
|
@ -42,18 +44,21 @@ export class PasswordResetController {
|
||||||
logger,
|
logger,
|
||||||
externalHooks,
|
externalHooks,
|
||||||
internalHooks,
|
internalHooks,
|
||||||
|
mailer,
|
||||||
repositories,
|
repositories,
|
||||||
}: {
|
}: {
|
||||||
config: Config;
|
config: Config;
|
||||||
logger: ILogger;
|
logger: ILogger;
|
||||||
externalHooks: IExternalHooksClass;
|
externalHooks: IExternalHooksClass;
|
||||||
internalHooks: IInternalHooksClass;
|
internalHooks: IInternalHooksClass;
|
||||||
|
mailer: UserManagementMailer;
|
||||||
repositories: Pick<IDatabaseCollections, 'User'>;
|
repositories: Pick<IDatabaseCollections, 'User'>;
|
||||||
}) {
|
}) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.externalHooks = externalHooks;
|
this.externalHooks = externalHooks;
|
||||||
this.internalHooks = internalHooks;
|
this.internalHooks = internalHooks;
|
||||||
|
this.mailer = mailer;
|
||||||
this.userRepository = repositories.User;
|
this.userRepository = repositories.User;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,8 +131,7 @@ export class PasswordResetController {
|
||||||
url.searchParams.append('token', resetPasswordToken);
|
url.searchParams.append('token', resetPasswordToken);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const mailer = UserManagementMailer.getInstance();
|
await this.mailer.passwordReset({
|
||||||
await mailer.passwordReset({
|
|
||||||
email,
|
email,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
|
|
102
packages/cli/src/controllers/tags.controller.ts
Normal file
102
packages/cli/src/controllers/tags.controller.ts
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import type { Repository } from 'typeorm';
|
||||||
|
import type { Config } from '@/config';
|
||||||
|
import { Delete, Get, Middleware, Patch, Post, RestController } from '@/decorators';
|
||||||
|
import type { IDatabaseCollections, IExternalHooksClass, ITagWithCountDb } from '@/Interfaces';
|
||||||
|
import { TagEntity } from '@db/entities/TagEntity';
|
||||||
|
import { getTagsWithCountDb } from '@/TagHelpers';
|
||||||
|
import { validateEntity } from '@/GenericHelpers';
|
||||||
|
import { BadRequestError, UnauthorizedError } from '@/ResponseHelper';
|
||||||
|
import { TagsRequest } from '@/requests';
|
||||||
|
|
||||||
|
@RestController('/tags')
|
||||||
|
export class TagsController {
|
||||||
|
private config: Config;
|
||||||
|
|
||||||
|
private externalHooks: IExternalHooksClass;
|
||||||
|
|
||||||
|
private tagsRepository: Repository<TagEntity>;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
config,
|
||||||
|
externalHooks,
|
||||||
|
repositories,
|
||||||
|
}: {
|
||||||
|
config: Config;
|
||||||
|
externalHooks: IExternalHooksClass;
|
||||||
|
repositories: Pick<IDatabaseCollections, 'Tag'>;
|
||||||
|
}) {
|
||||||
|
this.config = config;
|
||||||
|
this.externalHooks = externalHooks;
|
||||||
|
this.tagsRepository = repositories.Tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: move this into a new decorator `@IfEnabled('workflowTagsDisabled')`
|
||||||
|
@Middleware()
|
||||||
|
workflowsEnabledMiddleware(req: Request, res: Response, next: NextFunction) {
|
||||||
|
if (this.config.getEnv('workflowTagsDisabled'))
|
||||||
|
throw new BadRequestError('Workflow tags are disabled');
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieves all tags, with or without usage count
|
||||||
|
@Get('/')
|
||||||
|
async getAll(req: TagsRequest.GetAll): Promise<TagEntity[] | ITagWithCountDb[]> {
|
||||||
|
const { withUsageCount } = req.query;
|
||||||
|
if (withUsageCount === 'true') {
|
||||||
|
const tablePrefix = this.config.getEnv('database.tablePrefix');
|
||||||
|
return getTagsWithCountDb(tablePrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.tagsRepository.find({ select: ['id', 'name', 'createdAt', 'updatedAt'] });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a tag
|
||||||
|
@Post('/')
|
||||||
|
async createTag(req: TagsRequest.Create): Promise<TagEntity> {
|
||||||
|
const newTag = new TagEntity();
|
||||||
|
newTag.name = req.body.name.trim();
|
||||||
|
|
||||||
|
await this.externalHooks.run('tag.beforeCreate', [newTag]);
|
||||||
|
await validateEntity(newTag);
|
||||||
|
|
||||||
|
const tag = await this.tagsRepository.save(newTag);
|
||||||
|
await this.externalHooks.run('tag.afterCreate', [tag]);
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates a tag
|
||||||
|
@Patch('/:id(\\d+)')
|
||||||
|
async updateTag(req: TagsRequest.Update): Promise<TagEntity> {
|
||||||
|
const { name } = req.body;
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const newTag = new TagEntity();
|
||||||
|
newTag.id = id;
|
||||||
|
newTag.name = name.trim();
|
||||||
|
|
||||||
|
await this.externalHooks.run('tag.beforeUpdate', [newTag]);
|
||||||
|
await validateEntity(newTag);
|
||||||
|
|
||||||
|
const tag = await this.tagsRepository.save(newTag);
|
||||||
|
await this.externalHooks.run('tag.afterUpdate', [tag]);
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('/:id(\\d+)')
|
||||||
|
async deleteTag(req: TagsRequest.Delete) {
|
||||||
|
const isInstanceOwnerSetUp = this.config.getEnv('userManagement.isInstanceOwnerSetUp');
|
||||||
|
if (isInstanceOwnerSetUp && req.user.globalRole.name !== 'owner') {
|
||||||
|
throw new UnauthorizedError(
|
||||||
|
'You are not allowed to perform this action',
|
||||||
|
'Only owners can remove tags',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const { id } = req.params;
|
||||||
|
await this.externalHooks.run('tag.beforeDelete', [id]);
|
||||||
|
|
||||||
|
await this.tagsRepository.delete({ id });
|
||||||
|
await this.externalHooks.run('tag.afterDelete', [id]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,7 +13,6 @@ import {
|
||||||
getInstanceBaseUrl,
|
getInstanceBaseUrl,
|
||||||
hashPassword,
|
hashPassword,
|
||||||
isEmailSetUp,
|
isEmailSetUp,
|
||||||
isUserManagementEnabled,
|
|
||||||
sanitizeUser,
|
sanitizeUser,
|
||||||
validatePassword,
|
validatePassword,
|
||||||
withFeatureFlags,
|
withFeatureFlags,
|
||||||
|
@ -35,6 +34,8 @@ import type {
|
||||||
import type { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
|
import type { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
|
||||||
import { AuthIdentity } from '@db/entities/AuthIdentity';
|
import { AuthIdentity } from '@db/entities/AuthIdentity';
|
||||||
import type { PostHogClient } from '@/posthog';
|
import type { PostHogClient } from '@/posthog';
|
||||||
|
import { userManagementEnabledMiddleware } from '../middlewares/userManagementEnabled';
|
||||||
|
import { isSamlLicensedAndEnabled } from '../sso/saml/samlHelpers';
|
||||||
|
|
||||||
@RestController('/users')
|
@RestController('/users')
|
||||||
export class UsersController {
|
export class UsersController {
|
||||||
|
@ -98,14 +99,15 @@ export class UsersController {
|
||||||
/**
|
/**
|
||||||
* Send email invite(s) to one or multiple users and create user shell(s).
|
* Send email invite(s) to one or multiple users and create user shell(s).
|
||||||
*/
|
*/
|
||||||
@Post('/')
|
@Post('/', { middlewares: [userManagementEnabledMiddleware] })
|
||||||
async sendEmailInvites(req: UserRequest.Invite) {
|
async sendEmailInvites(req: UserRequest.Invite) {
|
||||||
// TODO: this should be checked in the middleware rather than here
|
if (isSamlLicensedAndEnabled()) {
|
||||||
if (!isUserManagementEnabled()) {
|
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
'Request to send email invite(s) to user(s) failed because user management is disabled',
|
'SAML is enabled, so users are managed by the Identity Provider and cannot be added through invites',
|
||||||
|
);
|
||||||
|
throw new BadRequestError(
|
||||||
|
'SAML is enabled, so users are managed by the Identity Provider and cannot be added through invites',
|
||||||
);
|
);
|
||||||
throw new BadRequestError('User management is disabled');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.config.getEnv('userManagement.isInstanceOwnerSetUp')) {
|
if (!this.config.getEnv('userManagement.isInstanceOwnerSetUp')) {
|
||||||
|
|
|
@ -11,7 +11,6 @@ import type {
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
INodeCredentialsDetails,
|
INodeCredentialsDetails,
|
||||||
ICredentialsEncrypted,
|
ICredentialsEncrypted,
|
||||||
IDataObject,
|
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { LoggerProxy } from 'n8n-workflow';
|
import { LoggerProxy } from 'n8n-workflow';
|
||||||
import { resolve as pathResolve } from 'path';
|
import { resolve as pathResolve } from 'path';
|
||||||
|
@ -112,7 +111,7 @@ oauth2CredentialController.get(
|
||||||
);
|
);
|
||||||
|
|
||||||
const token = new Csrf();
|
const token = new Csrf();
|
||||||
// Generate a CSRF prevention token and send it as a OAuth2 state stringma/ERR
|
// Generate a CSRF prevention token and send it as an OAuth2 state string
|
||||||
const csrfSecret = token.secretSync();
|
const csrfSecret = token.secretSync();
|
||||||
const state = {
|
const state = {
|
||||||
token: token.create(csrfSecret),
|
token: token.create(csrfSecret),
|
||||||
|
@ -174,6 +173,9 @@ oauth2CredentialController.get(
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const renderCallbackError = (res: express.Response, errorMessage: string) =>
|
||||||
|
res.render('oauth-error-callback', { error: { message: errorMessage } });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /oauth2-credential/callback
|
* GET /oauth2-credential/callback
|
||||||
*
|
*
|
||||||
|
@ -188,12 +190,12 @@ oauth2CredentialController.get(
|
||||||
const { code, state: stateEncoded } = req.query;
|
const { code, state: stateEncoded } = req.query;
|
||||||
|
|
||||||
if (!code || !stateEncoded) {
|
if (!code || !stateEncoded) {
|
||||||
const errorResponse = new ResponseHelper.ServiceUnavailableError(
|
return renderCallbackError(
|
||||||
|
res,
|
||||||
`Insufficient parameters for OAuth2 callback. Received following query parameters: ${JSON.stringify(
|
`Insufficient parameters for OAuth2 callback. Received following query parameters: ${JSON.stringify(
|
||||||
req.query,
|
req.query,
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let state;
|
let state;
|
||||||
|
@ -203,31 +205,21 @@ oauth2CredentialController.get(
|
||||||
token: string;
|
token: string;
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorResponse = new ResponseHelper.ServiceUnavailableError(
|
return renderCallbackError(res, 'Invalid state format returned');
|
||||||
'Invalid state format returned',
|
|
||||||
);
|
|
||||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const credential = await getCredentialWithoutUser(state.cid);
|
const credential = await getCredentialWithoutUser(state.cid);
|
||||||
|
|
||||||
if (!credential) {
|
if (!credential) {
|
||||||
LoggerProxy.error('OAuth2 callback failed because of insufficient permissions', {
|
const errorMessage = 'OAuth2 callback failed because of insufficient permissions';
|
||||||
|
LoggerProxy.error(errorMessage, {
|
||||||
userId: req.user?.id,
|
userId: req.user?.id,
|
||||||
credentialId: state.cid,
|
credentialId: state.cid,
|
||||||
});
|
});
|
||||||
const errorResponse = new ResponseHelper.NotFoundError(
|
return renderCallbackError(res, errorMessage);
|
||||||
RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL,
|
|
||||||
);
|
|
||||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let encryptionKey: string;
|
const encryptionKey = await UserSettings.getEncryptionKey();
|
||||||
try {
|
|
||||||
encryptionKey = await UserSettings.getEncryptionKey();
|
|
||||||
} catch (error) {
|
|
||||||
throw new ResponseHelper.InternalServerError((error as IDataObject).message as string);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mode: WorkflowExecuteMode = 'internal';
|
const mode: WorkflowExecuteMode = 'internal';
|
||||||
const timezone = config.getEnv('generic.timezone');
|
const timezone = config.getEnv('generic.timezone');
|
||||||
|
@ -251,14 +243,12 @@ oauth2CredentialController.get(
|
||||||
decryptedDataOriginal.csrfSecret === undefined ||
|
decryptedDataOriginal.csrfSecret === undefined ||
|
||||||
!token.verify(decryptedDataOriginal.csrfSecret as string, state.token)
|
!token.verify(decryptedDataOriginal.csrfSecret as string, state.token)
|
||||||
) {
|
) {
|
||||||
LoggerProxy.debug('OAuth2 callback state is invalid', {
|
const errorMessage = 'The OAuth2 callback state is invalid!';
|
||||||
|
LoggerProxy.debug(errorMessage, {
|
||||||
userId: req.user?.id,
|
userId: req.user?.id,
|
||||||
credentialId: state.cid,
|
credentialId: state.cid,
|
||||||
});
|
});
|
||||||
const errorResponse = new ResponseHelper.NotFoundError(
|
return renderCallbackError(res, errorMessage);
|
||||||
'The OAuth2 callback state is invalid!',
|
|
||||||
);
|
|
||||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let options = {};
|
let options = {};
|
||||||
|
@ -298,12 +288,12 @@ oauth2CredentialController.get(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oauthToken === undefined) {
|
if (oauthToken === undefined) {
|
||||||
LoggerProxy.error('OAuth2 callback failed: unable to get access tokens', {
|
const errorMessage = 'Unable to get OAuth2 access tokens!';
|
||||||
|
LoggerProxy.error(errorMessage, {
|
||||||
userId: req.user?.id,
|
userId: req.user?.id,
|
||||||
credentialId: state.cid,
|
credentialId: state.cid,
|
||||||
});
|
});
|
||||||
const errorResponse = new ResponseHelper.NotFoundError('Unable to get access tokens!');
|
return renderCallbackError(res, errorMessage);
|
||||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (decryptedDataOriginal.oauthTokenData) {
|
if (decryptedDataOriginal.oauthTokenData) {
|
||||||
|
@ -336,9 +326,7 @@ oauth2CredentialController.get(
|
||||||
|
|
||||||
return res.sendFile(pathResolve(TEMPLATES_DIR, 'oauth-callback.html'));
|
return res.sendFile(pathResolve(TEMPLATES_DIR, 'oauth-callback.html'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error response
|
return renderCallbackError(res, (error as Error).message);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
||||||
return ResponseHelper.sendErrorResponse(res, error);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { ExecutionStatus, WorkflowExecuteMode } from 'n8n-workflow';
|
import { ExecutionStatus, WorkflowExecuteMode } from 'n8n-workflow';
|
||||||
import { Column, Entity, Generated, Index, PrimaryColumn } from 'typeorm';
|
import { Column, Entity, Generated, Index, OneToMany, PrimaryColumn } from 'typeorm';
|
||||||
import { datetimeColumnType, jsonColumnType } from './AbstractEntity';
|
import { datetimeColumnType, jsonColumnType } from './AbstractEntity';
|
||||||
import { IWorkflowDb } from '@/Interfaces';
|
import { IWorkflowDb } from '@/Interfaces';
|
||||||
import type { IExecutionFlattedDb } from '@/Interfaces';
|
import type { IExecutionFlattedDb } from '@/Interfaces';
|
||||||
import { idStringifier } from '../utils/transformers';
|
import { idStringifier } from '../utils/transformers';
|
||||||
|
import type { ExecutionMetadata } from './ExecutionMetadata';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
@Index(['workflowId', 'id'])
|
@Index(['workflowId', 'id'])
|
||||||
|
@ -49,4 +50,7 @@ export class ExecutionEntity implements IExecutionFlattedDb {
|
||||||
|
|
||||||
@Column({ type: datetimeColumnType, nullable: true })
|
@Column({ type: datetimeColumnType, nullable: true })
|
||||||
waitTill: Date;
|
waitTill: Date;
|
||||||
|
|
||||||
|
@OneToMany('ExecutionMetadata', 'execution')
|
||||||
|
metadata: ExecutionMetadata[];
|
||||||
}
|
}
|
||||||
|
|
22
packages/cli/src/databases/entities/ExecutionMetadata.ts
Normal file
22
packages/cli/src/databases/entities/ExecutionMetadata.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, RelationId } from 'typeorm';
|
||||||
|
import { ExecutionEntity } from './ExecutionEntity';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class ExecutionMetadata {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@ManyToOne('ExecutionEntity', 'metadata', {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
execution: ExecutionEntity;
|
||||||
|
|
||||||
|
@RelationId((executionMetadata: ExecutionMetadata) => executionMetadata.execution)
|
||||||
|
executionId: number;
|
||||||
|
|
||||||
|
@Column('text')
|
||||||
|
key: string;
|
||||||
|
|
||||||
|
@Column('text')
|
||||||
|
value: string;
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ import { User } from './User';
|
||||||
import { WebhookEntity } from './WebhookEntity';
|
import { WebhookEntity } from './WebhookEntity';
|
||||||
import { WorkflowEntity } from './WorkflowEntity';
|
import { WorkflowEntity } from './WorkflowEntity';
|
||||||
import { WorkflowStatistics } from './WorkflowStatistics';
|
import { WorkflowStatistics } from './WorkflowStatistics';
|
||||||
|
import { ExecutionMetadata } from './ExecutionMetadata';
|
||||||
|
|
||||||
export const entities = {
|
export const entities = {
|
||||||
AuthIdentity,
|
AuthIdentity,
|
||||||
|
@ -33,4 +34,5 @@ export const entities = {
|
||||||
WebhookEntity,
|
WebhookEntity,
|
||||||
WorkflowEntity,
|
WorkflowEntity,
|
||||||
WorkflowStatistics,
|
WorkflowStatistics,
|
||||||
|
ExecutionMetadata,
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { MigrationInterface, QueryRunner, Table } from 'typeorm';
|
||||||
|
import { getTablePrefix, logMigrationEnd, logMigrationStart } from '@db/utils/migrationHelpers';
|
||||||
|
|
||||||
|
export class CreateExecutionMetadataTable1679416281779 implements MigrationInterface {
|
||||||
|
name = 'CreateExecutionMetadataTable1679416281779';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
logMigrationStart(this.name);
|
||||||
|
const tablePrefix = getTablePrefix();
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE ${tablePrefix}execution_metadata (
|
||||||
|
id int(11) auto_increment NOT NULL PRIMARY KEY,
|
||||||
|
executionId int(11) NOT NULL,
|
||||||
|
\`key\` TEXT NOT NULL,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
CONSTRAINT \`${tablePrefix}execution_metadata_FK\` FOREIGN KEY (\`executionId\`) REFERENCES \`${tablePrefix}execution_entity\` (\`id\`) ON DELETE CASCADE,
|
||||||
|
INDEX \`IDX_${tablePrefix}6d44376da6c1058b5e81ed8a154e1fee106046eb\` (\`executionId\` ASC)
|
||||||
|
)
|
||||||
|
ENGINE=InnoDB`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove indices that are no longer needed since the addition of the status column
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP INDEX \`IDX_${tablePrefix}06da892aaf92a48e7d3e400003\` ON \`${tablePrefix}execution_entity\``,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP INDEX \`IDX_${tablePrefix}78d62b89dc1433192b86dce18a\` ON \`${tablePrefix}execution_entity\``,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP INDEX \`IDX_${tablePrefix}1688846335d274033e15c846a4\` ON \`${tablePrefix}execution_entity\``,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP INDEX \`IDX_${tablePrefix}cefb067df2402f6aed0638a6c1\` ON \`${tablePrefix}execution_entity\``,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add index to the new status column
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX \`IDX_${tablePrefix}8b6f3f9ae234f137d707b98f3bf43584\` ON \`${tablePrefix}execution_entity\` (\`status\`, \`workflowId\`)`,
|
||||||
|
);
|
||||||
|
|
||||||
|
logMigrationEnd(this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
const tablePrefix = getTablePrefix();
|
||||||
|
|
||||||
|
await queryRunner.query(`DROP TABLE "${tablePrefix}execution_metadata"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX \`IDX_${tablePrefix}06da892aaf92a48e7d3e400003\` ON \`${tablePrefix}execution_entity\` (\`workflowId\`, \`waitTill\`, \`id\`)`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX \`IDX_${tablePrefix}78d62b89dc1433192b86dce18a\` ON \`${tablePrefix}execution_entity\` (\`workflowId\`, \`finished\`, \`id\`)`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX \`IDX_${tablePrefix}1688846335d274033e15c846a4\` ON \`${tablePrefix}execution_entity\` (\`finished\`, \`id\`)`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
'CREATE INDEX `IDX_' +
|
||||||
|
tablePrefix +
|
||||||
|
'cefb067df2402f6aed0638a6c1` ON `' +
|
||||||
|
tablePrefix +
|
||||||
|
'execution_entity` (`stoppedAt`)',
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP INDEX \`IDX_${tablePrefix}8b6f3f9ae234f137d707b98f3bf43584\` ON \`${tablePrefix}execution_entity\``,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,6 +34,7 @@ import { PurgeInvalidWorkflowConnections1675940580449 } from './1675940580449-Pu
|
||||||
import { AddStatusToExecutions1674138566000 } from './1674138566000-AddStatusToExecutions';
|
import { AddStatusToExecutions1674138566000 } from './1674138566000-AddStatusToExecutions';
|
||||||
import { MigrateExecutionStatus1676996103000 } from './1676996103000-MigrateExecutionStatus';
|
import { MigrateExecutionStatus1676996103000 } from './1676996103000-MigrateExecutionStatus';
|
||||||
import { UpdateRunningExecutionStatus1677236788851 } from './1677236788851-UpdateRunningExecutionStatus';
|
import { UpdateRunningExecutionStatus1677236788851 } from './1677236788851-UpdateRunningExecutionStatus';
|
||||||
|
import { CreateExecutionMetadataTable1679416281779 } from './1679416281779-CreateExecutionMetadataTable';
|
||||||
|
|
||||||
export const mysqlMigrations = [
|
export const mysqlMigrations = [
|
||||||
InitialMigration1588157391238,
|
InitialMigration1588157391238,
|
||||||
|
@ -72,4 +73,5 @@ export const mysqlMigrations = [
|
||||||
AddStatusToExecutions1674138566000,
|
AddStatusToExecutions1674138566000,
|
||||||
MigrateExecutionStatus1676996103000,
|
MigrateExecutionStatus1676996103000,
|
||||||
UpdateRunningExecutionStatus1677236788851,
|
UpdateRunningExecutionStatus1677236788851,
|
||||||
|
CreateExecutionMetadataTable1679416281779,
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { MigrationInterface, QueryRunner, Table } from 'typeorm';
|
||||||
|
import { getTablePrefix, logMigrationEnd, logMigrationStart } from '@db/utils/migrationHelpers';
|
||||||
|
|
||||||
|
export class CreateExecutionMetadataTable1679416281778 implements MigrationInterface {
|
||||||
|
name = 'CreateExecutionMetadataTable1679416281778';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
logMigrationStart(this.name);
|
||||||
|
const tablePrefix = getTablePrefix();
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE ${tablePrefix}execution_metadata (
|
||||||
|
"id" serial4 NOT NULL PRIMARY KEY,
|
||||||
|
"executionId" int4 NOT NULL,
|
||||||
|
"key" text NOT NULL,
|
||||||
|
"value" text NOT NULL,
|
||||||
|
CONSTRAINT ${tablePrefix}execution_metadata_fk FOREIGN KEY ("executionId") REFERENCES ${tablePrefix}execution_entity(id) ON DELETE CASCADE
|
||||||
|
)`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_${tablePrefix}6d44376da6c1058b5e81ed8a154e1fee106046eb" ON "${tablePrefix}execution_metadata" ("executionId");`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove indices that are no longer needed since the addition of the status column
|
||||||
|
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_${tablePrefix}33228da131bb1112247cf52a42"`);
|
||||||
|
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_${tablePrefix}72ffaaab9f04c2c1f1ea86e662"`);
|
||||||
|
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_${tablePrefix}58154df94c686818c99fb754ce"`);
|
||||||
|
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_${tablePrefix}4f474ac92be81610439aaad61e"`);
|
||||||
|
|
||||||
|
// Create new index for status
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_${tablePrefix}8b6f3f9ae234f137d707b98f3bf43584" ON "${tablePrefix}execution_entity" ("status", "workflowId");`,
|
||||||
|
);
|
||||||
|
|
||||||
|
logMigrationEnd(this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
const tablePrefix = getTablePrefix();
|
||||||
|
|
||||||
|
// Re-add removed indices
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX IF NOT EXISTS "IDX_${tablePrefix}33228da131bb1112247cf52a42" ON ${tablePrefix}execution_entity ("stoppedAt") `,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX IF NOT EXISTS "IDX_${tablePrefix}72ffaaab9f04c2c1f1ea86e662" ON ${tablePrefix}execution_entity ("finished", "id") `,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX IF NOT EXISTS "IDX_${tablePrefix}58154df94c686818c99fb754ce" ON ${tablePrefix}execution_entity ("workflowId", "waitTill", "id") `,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX IF NOT EXISTS "IDX_${tablePrefix}4f474ac92be81610439aaad61e" ON ${tablePrefix}execution_entity ("workflowId", "finished", "id") `,
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP INDEX IF EXISTS "IDX_${tablePrefix}8b6f3f9ae234f137d707b98f3bf43584"`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(`DROP TABLE "${tablePrefix}execution_metadata"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,6 +32,7 @@ import { PurgeInvalidWorkflowConnections1675940580449 } from './1675940580449-Pu
|
||||||
import { AddStatusToExecutions1674138566000 } from './1674138566000-AddStatusToExecutions';
|
import { AddStatusToExecutions1674138566000 } from './1674138566000-AddStatusToExecutions';
|
||||||
import { MigrateExecutionStatus1676996103000 } from './1676996103000-MigrateExecutionStatus';
|
import { MigrateExecutionStatus1676996103000 } from './1676996103000-MigrateExecutionStatus';
|
||||||
import { UpdateRunningExecutionStatus1677236854063 } from './1677236854063-UpdateRunningExecutionStatus';
|
import { UpdateRunningExecutionStatus1677236854063 } from './1677236854063-UpdateRunningExecutionStatus';
|
||||||
|
import { CreateExecutionMetadataTable1679416281778 } from './1679416281778-CreateExecutionMetadataTable';
|
||||||
|
|
||||||
export const postgresMigrations = [
|
export const postgresMigrations = [
|
||||||
InitialMigration1587669153312,
|
InitialMigration1587669153312,
|
||||||
|
@ -68,4 +69,5 @@ export const postgresMigrations = [
|
||||||
AddStatusToExecutions1674138566000,
|
AddStatusToExecutions1674138566000,
|
||||||
MigrateExecutionStatus1676996103000,
|
MigrateExecutionStatus1676996103000,
|
||||||
UpdateRunningExecutionStatus1677236854063,
|
UpdateRunningExecutionStatus1677236854063,
|
||||||
|
CreateExecutionMetadataTable1679416281778,
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { MigrationInterface, QueryRunner, Table } from 'typeorm';
|
||||||
|
import { getTablePrefix, logMigrationEnd, logMigrationStart } from '@db/utils/migrationHelpers';
|
||||||
|
|
||||||
|
export class CreateExecutionMetadataTable1679416281777 implements MigrationInterface {
|
||||||
|
name = 'CreateExecutionMetadataTable1679416281777';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
logMigrationStart(this.name);
|
||||||
|
const tablePrefix = getTablePrefix();
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "${tablePrefix}execution_metadata" (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
executionId INTEGER NOT NULL,
|
||||||
|
"key" TEXT NOT NULL,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
CONSTRAINT ${tablePrefix}execution_metadata_entity_FK FOREIGN KEY (executionId) REFERENCES ${tablePrefix}execution_entity(id) ON DELETE CASCADE
|
||||||
|
)`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX IF NOT EXISTS "IDX_${tablePrefix}6d44376da6c1058b5e81ed8a154e1fee106046eb" ON "${tablePrefix}execution_metadata" ("executionId");`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Re add some lost indices from migration DeleteExecutionsWithWorkflows.ts
|
||||||
|
// that were part of AddExecutionEntityIndexes.ts
|
||||||
|
// not all were needed since we added the `status` column to execution_entity
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX IF NOT EXISTS 'IDX_${tablePrefix}b94b45ce2c73ce46c54f20b5f9' ON '${tablePrefix}execution_entity' ('waitTill', 'id') `,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX IF NOT EXISTS 'IDX_${tablePrefix}81fc04c8a17de15835713505e4' ON '${tablePrefix}execution_entity' ('workflowId', 'id') `,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Also add index to the new status column
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX IF NOT EXISTS 'IDX_${tablePrefix}8b6f3f9ae234f137d707b98f3bf43584' ON '${tablePrefix}execution_entity' ('status', 'workflowId') `,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove no longer needed index to waitTill since it's already covered by the index b94b45ce2c73ce46c54f20b5f9 above
|
||||||
|
await queryRunner.query(`DROP INDEX IF EXISTS 'IDX_${tablePrefix}ca4a71b47f28ac6ea88293a8e2'`);
|
||||||
|
// Remove index for stoppedAt since it's not used anymore
|
||||||
|
await queryRunner.query(`DROP INDEX IF EXISTS 'IDX_${tablePrefix}cefb067df2402f6aed0638a6c1'`);
|
||||||
|
|
||||||
|
logMigrationEnd(this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
const tablePrefix = getTablePrefix();
|
||||||
|
|
||||||
|
await queryRunner.query(`DROP TABLE "${tablePrefix}execution_metadata"`);
|
||||||
|
await queryRunner.query(`DROP INDEX IF EXISTS 'IDX_${tablePrefix}b94b45ce2c73ce46c54f20b5f9'`);
|
||||||
|
await queryRunner.query(`DROP INDEX IF EXISTS 'IDX_${tablePrefix}81fc04c8a17de15835713505e4'`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP INDEX IF EXISTS 'IDX_${tablePrefix}8b6f3f9ae234f137d707b98f3bf43584'`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_${tablePrefix}ca4a71b47f28ac6ea88293a8e2" ON "${tablePrefix}execution_entity" ("waitTill")`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_${tablePrefix}cefb067df2402f6aed0638a6c1" ON "${tablePrefix}execution_entity" ("stoppedAt")`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,6 +31,7 @@ import { PurgeInvalidWorkflowConnections1675940580449 } from './1675940580449-Pu
|
||||||
import { AddStatusToExecutions1674138566000 } from './1674138566000-AddStatusToExecutions';
|
import { AddStatusToExecutions1674138566000 } from './1674138566000-AddStatusToExecutions';
|
||||||
import { MigrateExecutionStatus1676996103000 } from './1676996103000-MigrateExecutionStatus';
|
import { MigrateExecutionStatus1676996103000 } from './1676996103000-MigrateExecutionStatus';
|
||||||
import { UpdateRunningExecutionStatus1677237073720 } from './1677237073720-UpdateRunningExecutionStatus';
|
import { UpdateRunningExecutionStatus1677237073720 } from './1677237073720-UpdateRunningExecutionStatus';
|
||||||
|
import { CreateExecutionMetadataTable1679416281777 } from './1679416281777-CreateExecutionMetadataTable';
|
||||||
|
|
||||||
const sqliteMigrations = [
|
const sqliteMigrations = [
|
||||||
InitialMigration1588102412422,
|
InitialMigration1588102412422,
|
||||||
|
@ -66,6 +67,7 @@ const sqliteMigrations = [
|
||||||
AddStatusToExecutions1674138566000,
|
AddStatusToExecutions1674138566000,
|
||||||
MigrateExecutionStatus1676996103000,
|
MigrateExecutionStatus1676996103000,
|
||||||
UpdateRunningExecutionStatus1677237073720,
|
UpdateRunningExecutionStatus1677237073720,
|
||||||
|
CreateExecutionMetadataTable1679416281777,
|
||||||
];
|
];
|
||||||
|
|
||||||
export { sqliteMigrations };
|
export { sqliteMigrations };
|
||||||
|
|
11
packages/cli/src/decorators/Middleware.ts
Normal file
11
packages/cli/src/decorators/Middleware.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { CONTROLLER_MIDDLEWARES } from './constants';
|
||||||
|
import type { MiddlewareMetadata } from './types';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
export const Middleware = (): MethodDecorator => (target, handlerName) => {
|
||||||
|
const controllerClass = target.constructor;
|
||||||
|
const middlewares = (Reflect.getMetadata(CONTROLLER_MIDDLEWARES, controllerClass) ??
|
||||||
|
[]) as MiddlewareMetadata[];
|
||||||
|
middlewares.push({ handlerName: String(handlerName) });
|
||||||
|
Reflect.defineMetadata(CONTROLLER_MIDDLEWARES, middlewares, controllerClass);
|
||||||
|
};
|
|
@ -1,19 +1,30 @@
|
||||||
|
import type { RequestHandler } from 'express';
|
||||||
import { CONTROLLER_ROUTES } from './constants';
|
import { CONTROLLER_ROUTES } from './constants';
|
||||||
import type { Method, RouteMetadata } from './types';
|
import type { Method, RouteMetadata } from './types';
|
||||||
|
|
||||||
|
interface RouteOptions {
|
||||||
|
middlewares?: RequestHandler[];
|
||||||
|
}
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
const RouteFactory =
|
const RouteFactory =
|
||||||
(method: Method) =>
|
(method: Method) =>
|
||||||
(path: `/${string}`): MethodDecorator =>
|
(path: `/${string}`, options: RouteOptions = {}): MethodDecorator =>
|
||||||
(target, handlerName) => {
|
(target, handlerName) => {
|
||||||
const controllerClass = target.constructor;
|
const controllerClass = target.constructor;
|
||||||
const routes = (Reflect.getMetadata(CONTROLLER_ROUTES, controllerClass) ??
|
const routes = (Reflect.getMetadata(CONTROLLER_ROUTES, controllerClass) ??
|
||||||
[]) as RouteMetadata[];
|
[]) as RouteMetadata[];
|
||||||
routes.push({ method, path, handlerName: String(handlerName) });
|
routes.push({
|
||||||
|
method,
|
||||||
|
path,
|
||||||
|
middlewares: options.middlewares ?? [],
|
||||||
|
handlerName: String(handlerName),
|
||||||
|
});
|
||||||
Reflect.defineMetadata(CONTROLLER_ROUTES, routes, controllerClass);
|
Reflect.defineMetadata(CONTROLLER_ROUTES, routes, controllerClass);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Get = RouteFactory('get');
|
export const Get = RouteFactory('get');
|
||||||
export const Post = RouteFactory('post');
|
export const Post = RouteFactory('post');
|
||||||
|
export const Put = RouteFactory('put');
|
||||||
export const Patch = RouteFactory('patch');
|
export const Patch = RouteFactory('patch');
|
||||||
export const Delete = RouteFactory('delete');
|
export const Delete = RouteFactory('delete');
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
export const CONTROLLER_ROUTES = 'CONTROLLER_ROUTES';
|
export const CONTROLLER_ROUTES = 'CONTROLLER_ROUTES';
|
||||||
export const CONTROLLER_BASE_PATH = 'CONTROLLER_BASE_PATH';
|
export const CONTROLLER_BASE_PATH = 'CONTROLLER_BASE_PATH';
|
||||||
|
export const CONTROLLER_MIDDLEWARES = 'CONTROLLER_MIDDLEWARES';
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue