diff --git a/.github/workflows/check-pr-title.yml b/.github/workflows/check-pr-title.yml index add6e858a3..761cc5b8c7 100644 --- a/.github/workflows/check-pr-title.yml +++ b/.github/workflows/check-pr-title.yml @@ -7,8 +7,7 @@ on: - edited - synchronize branches: - - '**' - - '!release/*' + - 'master' jobs: check-pr-title: diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index bfd43e9257..d2ebc05320 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: pull_request_review: types: [submitted] - branch: + branches: - 'master' paths: - packages/design-system/** diff --git a/.github/workflows/ci-postgres-mysql.yml b/.github/workflows/ci-postgres-mysql.yml index d5abf1fa26..1d645ae774 100644 --- a/.github/workflows/ci-postgres-mysql.yml +++ b/.github/workflows/ci-postgres-mysql.yml @@ -8,6 +8,10 @@ on: paths: - packages/cli/src/databases/** - .github/workflows/ci-postgres-mysql.yml + pull_request_review: + types: [submitted] + branches: + - 'release/*' concurrency: group: db-${{ github.event.pull_request.number || github.ref }} diff --git a/.github/workflows/ci-pull-requests.yml b/.github/workflows/ci-pull-requests.yml index 78a5388950..d9938983b1 100644 --- a/.github/workflows/ci-pull-requests.yml +++ b/.github/workflows/ci-pull-requests.yml @@ -1,6 +1,10 @@ name: Build, unit test and lint branch -on: [pull_request] +on: + pull_request: + branches: + - '**' + - '!release/*' jobs: install-and-build: @@ -9,7 +13,6 @@ jobs: steps: - uses: actions/checkout@v4.1.1 with: - repository: n8n-io/n8n ref: refs/pull/${{ github.event.pull_request.number }}/merge - run: corepack enable diff --git a/.github/workflows/docker-images-benchmark.yml b/.github/workflows/docker-images-benchmark.yml new file mode 100644 index 0000000000..cf9d7359a5 --- /dev/null +++ b/.github/workflows/docker-images-benchmark.yml @@ -0,0 +1,43 @@ +name: Benchmark Docker Image CI + +on: + workflow_dispatch: + push: + branches: + - main + paths: + - 'packages/@n8n/benchmark/**' + - 'pnpm-lock.yaml' + - 'pnpm-workspace.yaml' + - '.github/workflows/docker-images-benchmark.yml' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4.1.1 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3.0.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.0.0 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3.0.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build + uses: docker/build-push-action@v5.1.0 + with: + context: . + file: ./packages/@n8n/benchmark/Dockerfile + platforms: linux/amd64 + provenance: false + push: true + tags: | + ghcr.io/${{ github.repository_owner }}/n8n-benchmark:latest diff --git a/.github/workflows/docker-images-nightly.yml b/.github/workflows/docker-images-nightly.yml index b701392e8b..34950d7ac2 100644 --- a/.github/workflows/docker-images-nightly.yml +++ b/.github/workflows/docker-images-nightly.yml @@ -6,10 +6,6 @@ on: - cron: '0 1 * * *' workflow_dispatch: inputs: - repository: - description: 'GitHub repository to create image off.' - required: true - default: 'n8n-io/n8n' branch: description: 'GitHub branch to create image off.' required: true @@ -49,7 +45,6 @@ jobs: - name: Checkout uses: actions/checkout@v4.1.1 with: - repository: ${{ github.event.inputs.repository || 'n8n-io/n8n' }} ref: ${{ github.event.inputs.branch || 'master' }} - name: Set up QEMU diff --git a/.github/workflows/docker-images.yml b/.github/workflows/docker-images.yml deleted file mode 100644 index fe5770252b..0000000000 --- a/.github/workflows/docker-images.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Docker Image CI - -on: - push: - tags: - - 'n8n@*' - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4.1.1 - - - name: Get the version - id: vars - run: echo ::set-output name=tag::$(echo ${GITHUB_REF:14}) - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3.0.0 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.0.0 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3.0.0 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Login to DockerHub - uses: docker/login-action@v3.0.0 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Build - uses: docker/build-push-action@v5.1.0 - with: - context: ./docker/images/n8n - build-args: | - N8N_VERSION=${{ steps.vars.outputs.tag }} - platforms: linux/amd64,linux/arm64 - provenance: false - push: true - tags: | - ${{ secrets.DOCKER_USERNAME }}/n8n:${{ steps.vars.outputs.tag }} - ghcr.io/${{ github.repository_owner }}/n8n:${{ steps.vars.outputs.tag }} diff --git a/.github/workflows/e2e-reusable.yml b/.github/workflows/e2e-reusable.yml index beea3bca27..ab88930a3a 100644 --- a/.github/workflows/e2e-reusable.yml +++ b/.github/workflows/e2e-reusable.yml @@ -22,11 +22,6 @@ on: required: false default: 'browsers:node18.12.0-chrome107' type: string - cache-key: - description: 'Cache key for modules and build artifacts.' - required: false - default: ${{ github.sha }}-${{ inputs.run-env }}-e2e-modules - type: string record: description: 'Record test run.' required: false @@ -78,7 +73,6 @@ jobs: steps: - uses: actions/checkout@v4.1.1 with: - repository: n8n-io/n8n ref: ${{ inputs.branch }} - name: Checkout PR @@ -111,7 +105,7 @@ jobs: /github/home/.cache /github/home/.pnpm-store ./packages/**/dist - key: ${{ inputs.cache-key }} + key: ${{ github.sha }}-e2e testing: runs-on: ubuntu-latest @@ -128,7 +122,6 @@ jobs: steps: - uses: actions/checkout@v4.1.1 with: - repository: n8n-io/n8n ref: ${{ inputs.branch }} - name: Checkout PR @@ -146,7 +139,7 @@ jobs: /github/home/.cache /github/home/.pnpm-store ./packages/**/dist - key: ${{ inputs.cache-key }} + key: ${{ github.sha }}-e2e - name: Install dependencies run: pnpm install --frozen-lockfile diff --git a/.github/workflows/e2e-tests-pr.yml b/.github/workflows/e2e-tests-pr.yml index f845dbb062..3d2f122638 100644 --- a/.github/workflows/e2e-tests-pr.yml +++ b/.github/workflows/e2e-tests-pr.yml @@ -3,8 +3,9 @@ name: PR E2E on: pull_request_review: types: [submitted] - branch: + branches: - 'master' + - 'release/*' concurrency: group: e2e-${{ github.event.pull_request.number || github.ref }} @@ -18,7 +19,6 @@ jobs: with: pr_number: ${{ github.event.pull_request.number }} user: ${{ github.event.pull_request.user.login || 'PR User' }} - spec: 'e2e/*' secrets: CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} diff --git a/.github/workflows/linting-reusable.yml b/.github/workflows/linting-reusable.yml index 2650622bd0..ed8d234940 100644 --- a/.github/workflows/linting-reusable.yml +++ b/.github/workflows/linting-reusable.yml @@ -21,7 +21,6 @@ jobs: steps: - uses: actions/checkout@v4.1.1 with: - repository: n8n-io/n8n ref: ${{ inputs.ref }} - run: corepack enable diff --git a/.github/workflows/release-create-pr.yml b/.github/workflows/release-create-pr.yml index a17fa1bf89..03572e541c 100644 --- a/.github/workflows/release-create-pr.yml +++ b/.github/workflows/release-create-pr.yml @@ -56,12 +56,12 @@ jobs: git push -f origin refs/remotes/origin/${{ github.event.inputs.base-branch }}:refs/heads/release/${{ env.NEXT_RELEASE }} - name: Push the release branch, and Create the PR - uses: peter-evans/create-pull-request@v5 + uses: peter-evans/create-pull-request@v6 with: base: 'release/${{ env.NEXT_RELEASE }}' - branch: '${{ env.NEXT_RELEASE }}-pr' + branch: 'release-pr/${{ env.NEXT_RELEASE }}' commit-message: ':rocket: Release ${{ env.NEXT_RELEASE }}' delete-branch: true - labels: 'release' + labels: release,release:${{ github.event.inputs.release-type }} title: ':rocket: Release ${{ env.NEXT_RELEASE }}' body-path: 'CHANGELOG-${{ env.NEXT_RELEASE }}.md' diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 239c18b512..522d47ff0f 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -8,18 +8,15 @@ on: - 'release/*' jobs: - publish-release: - if: github.event.pull_request.merged == true + publish-to-npm: + name: Publish to NPM runs-on: ubuntu-latest - - permissions: - contents: write - id-token: write - - timeout-minutes: 60 + if: github.event.pull_request.merged == true + timeout-minutes: 10 env: NPM_CONFIG_PROVENANCE: true - + outputs: + release: ${{ steps.set-release.outputs.release }} steps: - name: Checkout uses: actions/checkout@v4.1.1 @@ -51,25 +48,97 @@ jobs: pnpm publish -r --publish-branch ${{github.event.pull_request.base.ref}} --access public --tag rc --no-git-checks npm dist-tag rm n8n rc + - id: set-release + run: echo "release=${{ env.RELEASE }}" >> $GITHUB_OUTPUT + + publish-to-docker-hub: + name: Publish to DockerHub + needs: [publish-to-npm] + runs-on: ubuntu-latest + if: github.event.pull_request.merged == true + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@v4.1.1 + with: + fetch-depth: 0 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3.0.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.0.0 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3.0.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to DockerHub + uses: docker/login-action@v3.0.0 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build + uses: docker/build-push-action@v5.1.0 + with: + context: ./docker/images/n8n + build-args: | + N8N_VERSION=${{ needs.publish-to-npm.outputs.release }} + platforms: linux/amd64,linux/arm64 + provenance: false + push: true + tags: | + ${{ secrets.DOCKER_USERNAME }}/n8n:${{ needs.publish-to-npm.outputs.release }} + ghcr.io/${{ github.repository_owner }}/n8n:${{ needs.publish-to-npm.outputs.release }} + + create-github-release: + name: Create a GitHub Release + needs: [publish-to-npm, publish-to-docker-hub] + runs-on: ubuntu-latest + if: github.event.pull_request.merged == true + timeout-minutes: 5 + + permissions: + contents: write + id-token: write + + steps: - name: Create a Release on GitHub uses: ncipollo/release-action@v1 with: commit: ${{github.event.pull_request.base.ref}} - tag: 'n8n@${{env.RELEASE}}' + tag: 'n8n@${{ needs.publish-to-npm.outputs.release }}' prerelease: true makeLatest: false body: ${{github.event.pull_request.body}} + trigger-release-note: + name: Trigger a release note + needs: [publish-to-npm, create-github-release] + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + steps: - name: Trigger a release note - continue-on-error: true - run: curl -u docsWorkflows:${{ secrets.N8N_WEBHOOK_DOCS_PASSWORD }} --request GET 'https://internal.users.n8n.cloud/webhook/trigger-release-note' --header 'Content-Type:application/json' --data '{"version":"${{env.RELEASE}}"}' + run: curl -u docsWorkflows:${{ secrets.N8N_WEBHOOK_DOCS_PASSWORD }} --request GET 'https://internal.users.n8n.cloud/webhook/trigger-release-note' --header 'Content-Type:application/json' --data '{"version":"${{ needs.publish-to-npm.outputs.release }}"}' - # - name: Merge Release into 'master' - # run: | - # git fetch origin - # git checkout --track origin/master - # git config user.name "Jan Oberhauser" - # git config user.email jan.oberhauser@gmail.com - # git merge --ff n8n@${{env.RELEASE}} - # git push origin master - # git push origin :${{github.event.pull_request.base.ref}} + merge-back-into-master: + name: Merge back into master + needs: [publish-to-npm, create-github-release] + if: ${{ github.event.pull_request.merged == true && !contains(github.event.pull_request.labels.*.name, 'release:patch') }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 0 + - run: | + git checkout --track origin/master + git config user.name "github-actions[bot]" + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + git merge --ff n8n@${{ needs.publish-to-npm.outputs.release }} + git push origin master + git push origin :${{github.event.pull_request.base.ref}} diff --git a/.github/workflows/units-tests-reusable.yml b/.github/workflows/units-tests-reusable.yml index 1f64b2e27c..60bf593e82 100644 --- a/.github/workflows/units-tests-reusable.yml +++ b/.github/workflows/units-tests-reusable.yml @@ -36,7 +36,6 @@ jobs: steps: - uses: actions/checkout@v4.1.1 with: - repository: n8n-io/n8n ref: ${{ inputs.ref }} - run: corepack enable diff --git a/CHANGELOG.md b/CHANGELOG.md index 06ded0c1ca..08aadc87da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,45 @@ +# [1.56.0](https://github.com/n8n-io/n8n/compare/n8n@1.55.0...n8n@1.56.0) (2024-08-21) + + +### Bug Fixes + +* Better errors in Switch, If and Filter nodes ([#10457](https://github.com/n8n-io/n8n/issues/10457)) ([aea82cb](https://github.com/n8n-io/n8n/commit/aea82cb74421d516919742127daf669808b57604)) +* **Calendly Trigger Node:** Fix issue with webhook url matching ([#10378](https://github.com/n8n-io/n8n/issues/10378)) ([09c3a8b](https://github.com/n8n-io/n8n/commit/09c3a8b36733a9634ef5948922d6aa7a19bbb592)) +* **core:** Fix payload property in `workflow-post-execute` event ([#10413](https://github.com/n8n-io/n8n/issues/10413)) ([d98e29e](https://github.com/n8n-io/n8n/commit/d98e29e3d53de87aec276260615fa60473a2692f)) +* **core:** Fix XSS validation and separate URL validation ([#10424](https://github.com/n8n-io/n8n/issues/10424)) ([91467ab](https://github.com/n8n-io/n8n/commit/91467ab325e4c71c20c522f3143246d270101626)) +* **core:** Replace `sanitize-html` with `xss` in XSS validator constraint ([#10479](https://github.com/n8n-io/n8n/issues/10479)) ([5dea51a](https://github.com/n8n-io/n8n/commit/5dea51aad7d9e7ffc676d16f4bbbdecce5876f0b)) +* **core:** Use class-validator with XSS check for survey answers ([#10490](https://github.com/n8n-io/n8n/issues/10490)) ([547a606](https://github.com/n8n-io/n8n/commit/547a60642ce9e54819d4e600c822d87dabd59b2e)) +* **core:** Use explicit types in configs to ensure valid decorator metadata ([#10433](https://github.com/n8n-io/n8n/issues/10433)) ([2043daa](https://github.com/n8n-io/n8n/commit/2043daa2570bc04b0b8d41f277901a8cc8a7b98f)) +* **editor:** Add workflow scopes when initializing workflow ([#10455](https://github.com/n8n-io/n8n/issues/10455)) ([b857c2c](https://github.com/n8n-io/n8n/commit/b857c2cda0a9e4386a540d5e1e741570d9453588)) +* **editor:** Buffer json chunks in stream response ([#10439](https://github.com/n8n-io/n8n/issues/10439)) ([37797f3](https://github.com/n8n-io/n8n/commit/37797f38d81b12d030ba85034baeb49192ea575c)) +* **editor:** Fix flaky mapping tests ([#10453](https://github.com/n8n-io/n8n/issues/10453)) ([fc6d413](https://github.com/n8n-io/n8n/commit/fc6d4138d58282f676b32f1a6011b1b6d0184bf2)) +* **editor:** Fix overflow in AI Assistant chat messages ([#10491](https://github.com/n8n-io/n8n/issues/10491)) ([4a6ca63](https://github.com/n8n-io/n8n/commit/4a6ca632100731f85875c639f2164bf1ef415009)) +* **editor:** Highlight matching type in filter component ([#10425](https://github.com/n8n-io/n8n/issues/10425)) ([6bca879](https://github.com/n8n-io/n8n/commit/6bca879d4ae30c7f9a35e8d6672de42cf93be727)) +* **editor:** Show item count in output panel schema view ([#10426](https://github.com/n8n-io/n8n/issues/10426)) ([4dee7cc](https://github.com/n8n-io/n8n/commit/4dee7cc36e5f7768d0b71095b194bf357c92e941)) +* **editor:** Truncate long data pill labels in schema view ([#10427](https://github.com/n8n-io/n8n/issues/10427)) ([1bf2f4f](https://github.com/n8n-io/n8n/commit/1bf2f4f6171d666391bb3a3a312468bc083446e3)) +* Filter component - improve errors ([#10456](https://github.com/n8n-io/n8n/issues/10456)) ([61ac0c7](https://github.com/n8n-io/n8n/commit/61ac0c77755210f570b887951fe6bbec1a323811)) +* **Google Sheets Node:** Better error when column to match on is empty ([#10442](https://github.com/n8n-io/n8n/issues/10442)) ([ce46bf5](https://github.com/n8n-io/n8n/commit/ce46bf516a86d9779f37dd75b0c680d26d88e15d)) +* **Google Sheets Node:** Update name and hint for useAppend option ([#10443](https://github.com/n8n-io/n8n/issues/10443)) ([c5a0c04](https://github.com/n8n-io/n8n/commit/c5a0c049eaf44419c690d151de42fb0c10bd406e)) +* **Google Sheets Node:** Update to returnAllMatches option ([#10440](https://github.com/n8n-io/n8n/issues/10440)) ([f7fb02e](https://github.com/n8n-io/n8n/commit/f7fb02e92a756781f8e35bbbfc25d71c12cb70af)) +* **Invoice Ninja Node:** Fix payment types ([#10462](https://github.com/n8n-io/n8n/issues/10462)) ([129245d](https://github.com/n8n-io/n8n/commit/129245da10be1d645f61e929e40b128bd7813f17)) +* **n8n Form Trigger Node:** Show basic authentication modal on wrong credentials ([#10423](https://github.com/n8n-io/n8n/issues/10423)) ([0dc3e99](https://github.com/n8n-io/n8n/commit/0dc3e99b26bec45e747d83f383cfe5169d89e6b7)) +* **OpenAI Node:** Throw node operations error in case of openAi client error ([#10448](https://github.com/n8n-io/n8n/issues/10448)) ([0d3ed46](https://github.com/n8n-io/n8n/commit/0d3ed461996bbad06015c455f133baab6506437f)) +* Project Viewer always seeing a connection error when testing credentials ([#10417](https://github.com/n8n-io/n8n/issues/10417)) ([613cdd2](https://github.com/n8n-io/n8n/commit/613cdd2ba2c0f224c4857a5fc3eea36dbd683049)) +* Remove unimplemented Postgres credentials options ([#10461](https://github.com/n8n-io/n8n/issues/10461)) ([17ac784](https://github.com/n8n-io/n8n/commit/17ac7844f29d819b91dfaf90b9fe386d98060c42)) +* Rename Assistant back ([#10481](https://github.com/n8n-io/n8n/issues/10481)) ([c410aed](https://github.com/n8n-io/n8n/commit/c410aed4c22182943dc80ede63acda00b7898e10)) +* Require mfa code to change email ([#10354](https://github.com/n8n-io/n8n/issues/10354)) ([39c8e50](https://github.com/n8n-io/n8n/commit/39c8e50ad0513649f5a8cef911b7d6cdd61c2372)) +* **Respond to Webhook Node:** Fix issue preventing the chat trigger from working ([#9886](https://github.com/n8n-io/n8n/issues/9886)) ([9d6ad88](https://github.com/n8n-io/n8n/commit/9d6ad88c14a88fd0dfcb4f9981e38d19cf5f3067)) +* Show input names when node has multiple inputs ([#10434](https://github.com/n8n-io/n8n/issues/10434)) ([973956c](https://github.com/n8n-io/n8n/commit/973956cc26c78c329ff6eb6934d4f0a24060c87c)) +* **Toggl Trigger Node:** Update API version ([#10207](https://github.com/n8n-io/n8n/issues/10207)) ([9bdb1d6](https://github.com/n8n-io/n8n/commit/9bdb1d6dca43fe491c5eb96f093b7eec4509eaff)) + + +### Features + +* **core:** Support bidirectional communication between specific mains and specific workers ([#10377](https://github.com/n8n-io/n8n/issues/10377)) ([d0fc9de](https://github.com/n8n-io/n8n/commit/d0fc9dee0e17211c1ed130b19286e9573c9ebfbd)) +* **Facebook Graph API Node:** Update node to support API v18 - v20 ([#10419](https://github.com/n8n-io/n8n/issues/10419)) ([e7ee10f](https://github.com/n8n-io/n8n/commit/e7ee10f243663d899d32e61bc6264b4df444e2af)) + + + # [1.55.0](https://github.com/n8n-io/n8n/compare/n8n@1.54.0...n8n@1.55.0) (2024-08-14) diff --git a/cypress/constants.ts b/cypress/constants.ts index 6f7e7b978d..cbbf838530 100644 --- a/cypress/constants.ts +++ b/cypress/constants.ts @@ -61,6 +61,7 @@ export const AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME = 'OpenAI Chat Model' export const AI_MEMORY_POSTGRES_NODE_NAME = 'Postgres Chat Memory'; export const AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME = 'Auto-fixing Output Parser'; export const WEBHOOK_NODE_NAME = 'Webhook'; +export const EXECUTE_WORKFLOW_NODE_NAME = 'Execute Workflow'; export const META_KEY = Cypress.platform === 'darwin' ? 'meta' : 'ctrl'; diff --git a/cypress/e2e/11-inline-expression-editor.cy.ts b/cypress/e2e/11-inline-expression-editor.cy.ts index 143648ce1b..945c62821b 100644 --- a/cypress/e2e/11-inline-expression-editor.cy.ts +++ b/cypress/e2e/11-inline-expression-editor.cy.ts @@ -11,6 +11,21 @@ describe('Inline expression editor', () => { cy.on('uncaught:exception', (error) => error.name !== 'ExpressionError'); }); + describe('Basic UI functionality', () => { + it('should open and close inline expression preview', () => { + WorkflowPage.actions.zoomToFit(); + WorkflowPage.actions.openNode('Schedule'); + WorkflowPage.actions.openInlineExpressionEditor(); + WorkflowPage.getters.inlineExpressionEditorInput().clear(); + WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); + WorkflowPage.getters.inlineExpressionEditorInput().type('123'); + WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^123$/); + // click outside to close + ndv.getters.outputPanel().click(); + WorkflowPage.getters.inlineExpressionEditorOutput().should('not.exist'); + }); + }); + describe('Static data', () => { beforeEach(() => { WorkflowPage.actions.addNodeToCanvas('Hacker News'); diff --git a/cypress/e2e/14-mapping.cy.ts b/cypress/e2e/14-mapping.cy.ts index 2e405e69e8..43103415e3 100644 --- a/cypress/e2e/14-mapping.cy.ts +++ b/cypress/e2e/14-mapping.cy.ts @@ -40,6 +40,8 @@ describe('Data mapping', () => { ndv.actions.mapDataFromHeader(1, 'value'); ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.timestamp }}'); + ndv.getters.inlineExpressionEditorInput().type('{esc}'); + ndv.getters.parameterExpressionPreview('value').should('include.text', '2024'); ndv.actions.mapDataFromHeader(2, 'value'); ndv.getters @@ -133,6 +135,7 @@ describe('Data mapping', () => { ndv.actions.mapToParameter('value'); ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }}'); + ndv.getters.inlineExpressionEditorInput().type('{esc}'); ndv.getters.parameterExpressionPreview('value').should('include.text', '0'); ndv.getters @@ -146,6 +149,7 @@ describe('Data mapping', () => { ndv.getters .inlineExpressionEditorInput() .should('have.text', '{{ $json.input }}{{ $json.input[0].count }}'); + ndv.getters.inlineExpressionEditorInput().type('{esc}'); ndv.actions.validateExpressionPreview('value', '[object Object]0'); }); @@ -163,6 +167,7 @@ describe('Data mapping', () => { ndv.actions.mapToParameter('value'); ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }}'); + ndv.getters.inlineExpressionEditorInput().type('{esc}'); ndv.actions.validateExpressionPreview('value', '0'); ndv.getters.inputDataContainer().find('span').contains('input').realMouseDown(); @@ -192,6 +197,7 @@ describe('Data mapping', () => { ndv.getters .inlineExpressionEditorInput() .should('have.text', `{{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input[0].count }}`); + ndv.getters.inlineExpressionEditorInput().type('{esc}'); ndv.actions.switchInputMode('Table'); ndv.actions.selectInputNode(SCHEDULE_TRIGGER_NODE_NAME); @@ -271,12 +277,12 @@ describe('Data mapping', () => { ndv.actions.typeIntoParameterInput('value', 'fun'); ndv.actions.clearParameterInput('value'); // keep focus on param - cy.wait(300); ndv.getters.inputDataContainer().should('exist').find('span').contains('count').realMouseDown(); ndv.actions.mapToParameter('value'); ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }}'); + ndv.getters.inlineExpressionEditorInput().type('{esc}'); ndv.actions.validateExpressionPreview('value', '0'); ndv.getters.inputDataContainer().find('span').contains('input').realMouseDown(); @@ -350,19 +356,23 @@ describe('Data mapping', () => { workflowPage.actions.zoomToFit(); workflowPage.actions.openNode('Set'); - ndv.actions.clearParameterInput('value'); ndv.actions.typeIntoParameterInput('value', '='); - ndv.actions.typeIntoParameterInput('value', 'hello world{enter}{enter}newline'); + ndv.getters.inlineExpressionEditorInput().find('.cm-content').paste('hello world\n\nnewline'); + ndv.getters.inlineExpressionEditorInput().type('{esc}'); ndv.getters.inputDataContainer().should('exist').find('span').contains('count').realMouseDown(); - ndv.actions.mapToParameter('value'); + ndv.getters + .inlineExpressionEditorInput() + .should('have.text', '{{ $json.input[0].count }}hello worldnewline'); + ndv.getters.inlineExpressionEditorInput().type('{esc}'); + ndv.actions.validateExpressionPreview('value', '0hello world\n\nnewline'); ndv.getters.inputDataContainer().find('span').contains('input').realMouseDown(); - ndv.actions.mapToParameter('value', 'bottom'); + ndv.actions.mapToParameter('value', 'center'); ndv.getters .inlineExpressionEditorInput() - .should('have.text', '{{ $json.input[0].count }}hello worldnewline{{ $json.input }}'); + .should('have.text', '{{ $json.input[0].count }}hello world{{ $json.input }}newline'); }); }); diff --git a/cypress/e2e/17-workflow-tags.cy.ts b/cypress/e2e/17-workflow-tags.cy.ts index fc889aead2..26ea7cbe2c 100644 --- a/cypress/e2e/17-workflow-tags.cy.ts +++ b/cypress/e2e/17-workflow-tags.cy.ts @@ -65,7 +65,7 @@ describe('Workflow tags', () => { it('should detach a tag inline by clicking on dropdown list item', () => { wf.getters.createTagButton().click(); wf.actions.addTags(TEST_TAGS); - wf.getters.nthTagPill(1).click(); + wf.getters.workflowTagsContainer().click(); wf.getters.tagsInDropdown().filter('.selected').first().click(); cy.get('body').click(0, 0); wf.getters.workflowTags().click(); @@ -79,7 +79,7 @@ describe('Workflow tags', () => { wf.actions.addTags(TEST_TAGS); cy.get('body').click(0, 0); wf.getters.workflowTags().click(); - wf.getters.tagsDropdown().find('input:focus').type(NON_EXISTING_TAG); + wf.getters.workflowTagsInput().type(NON_EXISTING_TAG); getVisibleSelect() .find('li') diff --git a/cypress/e2e/1858-PAY-can-use-context-menu.ts b/cypress/e2e/1858-PAY-can-use-context-menu.ts new file mode 100644 index 0000000000..6727df4166 --- /dev/null +++ b/cypress/e2e/1858-PAY-can-use-context-menu.ts @@ -0,0 +1,21 @@ +import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; + +const WorkflowPage = new WorkflowPageClass(); + +describe('PAY-1858 context menu', () => { + it('can use context menu on saved workflow', () => { + WorkflowPage.actions.visit(); + cy.createFixtureWorkflow('Test_workflow_filter.json', 'test'); + + WorkflowPage.getters.canvasNodes().should('have.length', 5); + WorkflowPage.actions.deleteNodeFromContextMenu('Then'); + WorkflowPage.getters.canvasNodes().should('have.length', 4); + + WorkflowPage.actions.hitSaveWorkflow(); + + cy.reload(); + WorkflowPage.getters.canvasNodes().should('have.length', 4); + WorkflowPage.actions.deleteNodeFromContextMenu('Code'); + WorkflowPage.getters.canvasNodes().should('have.length', 3); + }); +}); diff --git a/cypress/e2e/2-credentials.cy.ts b/cypress/e2e/2-credentials.cy.ts index ffecd51959..9dbe6c6b5d 100644 --- a/cypress/e2e/2-credentials.cy.ts +++ b/cypress/e2e/2-credentials.cy.ts @@ -112,13 +112,13 @@ describe('Credentials', () => { workflowPage.getters.nodeCredentialsSelect().should('have.length', 2); workflowPage.getters.nodeCredentialsSelect().first().click(); - getVisibleSelect().find('li').last().click(); + getVisibleSelect().find('li').contains('Create New Credential').click(); // This one should show auth type selector credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2); cy.get('body').type('{esc}'); workflowPage.getters.nodeCredentialsSelect().last().click(); - getVisibleSelect().find('li').last().click(); + getVisibleSelect().find('li').contains('Create New Credential').click(); // This one should not show auth type selector credentialsModal.getters.credentialsAuthTypeSelector().should('not.exist'); }); diff --git a/cypress/e2e/33-settings-personal.cy.ts b/cypress/e2e/33-settings-personal.cy.ts index 73dc7476b8..e3cc572c9e 100644 --- a/cypress/e2e/33-settings-personal.cy.ts +++ b/cypress/e2e/33-settings-personal.cy.ts @@ -35,13 +35,14 @@ describe('Personal Settings', () => { successToast().find('.el-notification__closeBtn').click(); }); }); + // eslint-disable-next-line n8n-local-rules/no-skipped-tests it('not allow malicious values for personal data', () => { cy.visit('/settings/personal'); INVALID_NAMES.forEach((name) => { cy.getByTestId('personal-data-form').find('input[name="firstName"]').clear().type(name); cy.getByTestId('personal-data-form').find('input[name="lastName"]').clear().type(name); cy.getByTestId('save-settings-button').click(); - errorToast().should('contain', 'Malicious firstName | Malicious lastName'); + errorToast().should('contain', 'Potentially malicious string | Potentially malicious string'); errorToast().find('.el-notification__closeBtn').click(); }); }); diff --git a/cypress/e2e/45-ai-assistant.cy.ts b/cypress/e2e/45-ai-assistant.cy.ts new file mode 100644 index 0000000000..4431007df0 --- /dev/null +++ b/cypress/e2e/45-ai-assistant.cy.ts @@ -0,0 +1,247 @@ +import { NDV, WorkflowPage } from '../pages'; +import { AIAssistant } from '../pages/features/ai-assistant'; + +const wf = new WorkflowPage(); +const ndv = new NDV(); +const aiAssistant = new AIAssistant(); + +describe('AI Assistant::disabled', () => { + beforeEach(() => { + aiAssistant.actions.disableAssistant(); + wf.actions.visit(); + }); + + it('does not show assistant button if feature is disabled', () => { + aiAssistant.getters.askAssistantFloatingButton().should('not.exist'); + }); +}); + +describe('AI Assistant::enabled', () => { + beforeEach(() => { + aiAssistant.actions.enableAssistant(); + wf.actions.visit(); + }); + + after(() => { + aiAssistant.actions.disableAssistant(); + }); + + it('renders placeholder UI', () => { + aiAssistant.getters.askAssistantFloatingButton().should('be.visible'); + aiAssistant.getters.askAssistantFloatingButton().click(); + aiAssistant.getters.askAssistantChat().should('be.visible'); + aiAssistant.getters.placeholderMessage().should('be.visible'); + aiAssistant.getters.chatInputWrapper().should('not.exist'); + aiAssistant.getters.closeChatButton().should('be.visible'); + aiAssistant.getters.closeChatButton().click(); + aiAssistant.getters.askAssistantChat().should('not.exist'); + }); + + it('should resize assistant chat up', () => { + aiAssistant.getters.askAssistantFloatingButton().click(); + aiAssistant.getters.askAssistantSidebarResizer().should('be.visible'); + aiAssistant.getters.askAssistantChat().then((element) => { + const { width, left } = element[0].getBoundingClientRect(); + cy.drag(aiAssistant.getters.askAssistantSidebarResizer(), [left - 10, 0], { + abs: true, + clickToFinish: true, + }); + aiAssistant.getters.askAssistantChat().then((newElement) => { + const newWidth = newElement[0].getBoundingClientRect().width; + expect(newWidth).to.be.greaterThan(width); + }); + }); + }); + + it('should resize assistant chat down', () => { + aiAssistant.getters.askAssistantFloatingButton().click(); + aiAssistant.getters.askAssistantSidebarResizer().should('be.visible'); + aiAssistant.getters.askAssistantChat().then((element) => { + const { width, left } = element[0].getBoundingClientRect(); + cy.drag(aiAssistant.getters.askAssistantSidebarResizer(), [left + 10, 0], { + abs: true, + clickToFinish: true, + }); + aiAssistant.getters.askAssistantChat().then((newElement) => { + const newWidth = newElement[0].getBoundingClientRect().width; + expect(newWidth).to.be.lessThan(width); + }); + }); + }); + + it('should start chat session from node error view', () => { + cy.intercept('POST', '/rest/ai-assistant/chat', { + statusCode: 200, + fixture: 'aiAssistant/simple_message_response.json', + }).as('chatRequest'); + cy.createFixtureWorkflow('aiAssistant/test_workflow.json'); + wf.actions.openNode('Stop and Error'); + ndv.getters.nodeExecuteButton().click(); + aiAssistant.getters.nodeErrorViewAssistantButton().click(); + cy.wait('@chatRequest'); + aiAssistant.getters.chatMessagesAll().should('have.length', 1); + aiAssistant.getters + .chatMessagesAll() + .eq(0) + .should('contain.text', 'Hey, this is an assistant message'); + aiAssistant.getters.nodeErrorViewAssistantButton().should('be.disabled'); + }); + + it('should render chat input correctly', () => { + cy.intercept('POST', '/rest/ai-assistant/chat', { + statusCode: 200, + fixture: 'aiAssistant/simple_message_response.json', + }).as('chatRequest'); + cy.createFixtureWorkflow('aiAssistant/test_workflow.json'); + wf.actions.openNode('Stop and Error'); + ndv.getters.nodeExecuteButton().click(); + aiAssistant.getters.nodeErrorViewAssistantButton().click(); + cy.wait('@chatRequest'); + // Send button should be disabled when input is empty + aiAssistant.getters.sendMessageButton().should('be.disabled'); + aiAssistant.getters.chatInput().type('Yo '); + aiAssistant.getters.sendMessageButton().should('not.be.disabled'); + aiAssistant.getters.chatInput().then((element) => { + const { height } = element[0].getBoundingClientRect(); + // Shift + Enter should add a new line + aiAssistant.getters.chatInput().type('Hello{shift+enter}there'); + aiAssistant.getters.chatInput().then((newElement) => { + const newHeight = newElement[0].getBoundingClientRect().height; + // Chat input should grow as user adds new lines + expect(newHeight).to.be.greaterThan(height); + aiAssistant.getters.sendMessageButton().click(); + cy.wait('@chatRequest'); + // New lines should be rendered as
in the chat + aiAssistant.getters.chatMessagesUser().should('have.length', 1); + aiAssistant.getters.chatMessagesUser().eq(0).find('br').should('have.length', 1); + // Chat input should be cleared now + aiAssistant.getters.chatInput().should('have.value', ''); + }); + }); + }); + + it('should render and handle quick replies', () => { + cy.intercept('POST', '/rest/ai-assistant/chat', { + statusCode: 200, + fixture: 'aiAssistant/quick_reply_message_response.json', + }).as('chatRequest'); + cy.createFixtureWorkflow('aiAssistant/test_workflow.json'); + wf.actions.openNode('Stop and Error'); + ndv.getters.nodeExecuteButton().click(); + aiAssistant.getters.nodeErrorViewAssistantButton().click(); + cy.wait('@chatRequest'); + aiAssistant.getters.quickReplies().should('have.length', 2); + aiAssistant.getters.quickReplies().eq(0).click(); + cy.wait('@chatRequest'); + aiAssistant.getters.chatMessagesUser().should('have.length', 1); + aiAssistant.getters.chatMessagesUser().eq(0).should('contain.text', "Sure, let's do it"); + }); + + it('should send message to assistant when node is executed', () => { + cy.intercept('POST', '/rest/ai-assistant/chat', { + statusCode: 200, + fixture: 'aiAssistant/simple_message_response.json', + }).as('chatRequest'); + cy.createFixtureWorkflow('aiAssistant/test_workflow.json'); + wf.actions.openNode('Edit Fields'); + ndv.getters.nodeExecuteButton().click(); + aiAssistant.getters.nodeErrorViewAssistantButton().click(); + cy.wait('@chatRequest'); + aiAssistant.getters.chatMessagesAssistant().should('have.length', 1); + // Executing the same node should sende a new message to the assistant automatically + ndv.getters.nodeExecuteButton().click(); + cy.wait('@chatRequest'); + aiAssistant.getters.chatMessagesAssistant().should('have.length', 2); + }); + + it('should warn before starting a new session', () => { + cy.intercept('POST', '/rest/ai-assistant/chat', { + statusCode: 200, + fixture: 'aiAssistant/simple_message_response.json', + }).as('chatRequest'); + cy.createFixtureWorkflow('aiAssistant/test_workflow.json'); + wf.actions.openNode('Edit Fields'); + ndv.getters.nodeExecuteButton().click(); + aiAssistant.getters.nodeErrorViewAssistantButton().click(); + cy.wait('@chatRequest'); + aiAssistant.getters.closeChatButton().click(); + ndv.getters.backToCanvas().click(); + wf.actions.openNode('Stop and Error'); + ndv.getters.nodeExecuteButton().click(); + aiAssistant.getters.nodeErrorViewAssistantButton().click(); + // Since we already have an active session, a warning should be shown + aiAssistant.getters.newAssistantSessionModal().should('be.visible'); + aiAssistant.getters + .newAssistantSessionModal() + .find('button') + .contains('Start new session') + .click(); + cy.wait('@chatRequest'); + // New session should start with initial assistant message + aiAssistant.getters.chatMessagesAll().should('have.length', 1); + }); + + it('should apply code diff to code node', () => { + cy.intercept('POST', '/rest/ai-assistant/chat', { + statusCode: 200, + fixture: 'aiAssistant/code_diff_suggestion_response.json', + }).as('chatRequest'); + cy.intercept('POST', '/rest/ai-assistant/chat/apply-suggestion', { + statusCode: 200, + fixture: 'aiAssistant/apply_code_diff_response.json', + }).as('applySuggestion'); + cy.createFixtureWorkflow('aiAssistant/test_workflow.json'); + wf.actions.openNode('Code'); + ndv.getters.nodeExecuteButton().click(); + aiAssistant.getters.nodeErrorViewAssistantButton().click({ force: true }); + cy.wait('@chatRequest'); + // Should have two assistant messages + aiAssistant.getters.chatMessagesAll().should('have.length', 2); + aiAssistant.getters.codeDiffs().should('have.length', 1); + aiAssistant.getters.applyCodeDiffButtons().should('have.length', 1); + aiAssistant.getters.applyCodeDiffButtons().first().click(); + cy.wait('@applySuggestion'); + aiAssistant.getters.applyCodeDiffButtons().should('have.length', 0); + aiAssistant.getters.undoReplaceCodeButtons().should('have.length', 1); + aiAssistant.getters.codeReplacedMessage().should('be.visible'); + ndv.getters + .parameterInput('jsCode') + .get('.cm-content') + .should('contain.text', 'item.json.myNewField = 1'); + // Clicking undo should revert the code back but not call the assistant + aiAssistant.getters.undoReplaceCodeButtons().first().click(); + aiAssistant.getters.applyCodeDiffButtons().should('have.length', 1); + aiAssistant.getters.codeReplacedMessage().should('not.exist'); + cy.get('@applySuggestion.all').then((interceptions) => { + expect(interceptions).to.have.length(1); + }); + ndv.getters + .parameterInput('jsCode') + .get('.cm-content') + .should('contain.text', 'item.json.myNewField = 1aaa'); + // Replacing the code again should also not call the assistant + cy.get('@applySuggestion.all').then((interceptions) => { + expect(interceptions).to.have.length(1); + }); + aiAssistant.getters.applyCodeDiffButtons().should('have.length', 1); + aiAssistant.getters.applyCodeDiffButtons().first().click(); + ndv.getters + .parameterInput('jsCode') + .get('.cm-content') + .should('contain.text', 'item.json.myNewField = 1'); + }); + + it('should end chat session when `end_session` event is received', () => { + cy.intercept('POST', '/rest/ai-assistant/chat', { + statusCode: 200, + fixture: 'aiAssistant/end_session_response.json', + }).as('chatRequest'); + cy.createFixtureWorkflow('aiAssistant/test_workflow.json'); + wf.actions.openNode('Stop and Error'); + ndv.getters.nodeExecuteButton().click(); + aiAssistant.getters.nodeErrorViewAssistantButton().click(); + cy.wait('@chatRequest'); + aiAssistant.getters.chatMessagesSystem().should('have.length', 1); + aiAssistant.getters.chatMessagesSystem().first().should('contain.text', 'session has ended'); + }); +}); diff --git a/cypress/e2e/45-workflow-selector-parameter.cy.ts b/cypress/e2e/45-workflow-selector-parameter.cy.ts new file mode 100644 index 0000000000..a6dc23e6c2 --- /dev/null +++ b/cypress/e2e/45-workflow-selector-parameter.cy.ts @@ -0,0 +1,82 @@ +import { EXECUTE_WORKFLOW_NODE_NAME } from '../constants'; +import { WorkflowPage as WorkflowPageClass, NDV } from '../pages'; +import { getVisiblePopper } from '../utils'; + +const workflowPage = new WorkflowPageClass(); +const ndv = new NDV(); + +describe('Workflow Selector Parameter', () => { + beforeEach(() => { + cy.resetDatabase(); + cy.signinAsOwner(); + ['Get_Weather', 'Search_DB'].forEach((workflowName) => { + workflowPage.actions.visit(); + cy.createFixtureWorkflow(`Test_Subworkflow_${workflowName}.json`, workflowName); + workflowPage.actions.saveWorkflowOnButtonClick(); + }); + workflowPage.actions.visit(); + workflowPage.actions.addInitialNodeToCanvas(EXECUTE_WORKFLOW_NODE_NAME, { + keepNdvOpen: true, + action: 'Call Another Workflow', + }); + }); + it('should render sub-workflows list', () => { + ndv.getters.resourceLocator('workflowId').should('be.visible'); + ndv.getters.resourceLocatorInput('workflowId').click(); + + getVisiblePopper() + .should('have.length', 1) + .findChildByTestId('rlc-item') + .should('have.length', 2); + }); + + it('should show required parameter warning', () => { + ndv.getters.resourceLocator('workflowId').should('be.visible'); + ndv.getters.resourceLocatorInput('workflowId').click(); + ndv.getters.parameterInputIssues('workflowId').should('exist'); + }); + + it('should filter sub-workflows list', () => { + ndv.getters.resourceLocator('workflowId').should('be.visible'); + ndv.getters.resourceLocatorInput('workflowId').click(); + ndv.getters.resourceLocatorSearch('workflowId').type('Weather'); + + getVisiblePopper() + .should('have.length', 1) + .findChildByTestId('rlc-item') + .should('have.length', 1) + .click(); + + ndv.getters + .resourceLocatorInput('workflowId') + .find('input') + .should('have.value', 'Get_Weather'); + }); + + it('should render sub-workflow links correctly', () => { + ndv.getters.resourceLocator('workflowId').should('be.visible'); + ndv.getters.resourceLocatorInput('workflowId').click(); + + getVisiblePopper().findChildByTestId('rlc-item').first().click(); + + ndv.getters.resourceLocatorInput('workflowId').find('a').should('exist'); + cy.getByTestId('radio-button-expression').eq(1).click(); + ndv.getters.resourceLocatorInput('workflowId').find('a').should('not.exist'); + }); + + it('should switch to ID mode on expression', () => { + ndv.getters.resourceLocator('workflowId').should('be.visible'); + ndv.getters.resourceLocatorInput('workflowId').click(); + + getVisiblePopper().findChildByTestId('rlc-item').first().click(); + ndv.getters + .resourceLocatorModeSelector('workflowId') + .find('input') + .should('have.value', 'From list'); + cy.getByTestId('radio-button-expression').eq(1).click(); + ndv.getters + .resourceLocatorModeSelector('workflowId') + .find('input') + .should('have.value', 'By ID'); + }); +}); diff --git a/cypress/fixtures/Test_Subworkflow_Get_Weather.json b/cypress/fixtures/Test_Subworkflow_Get_Weather.json new file mode 100644 index 0000000000..3829aca879 --- /dev/null +++ b/cypress/fixtures/Test_Subworkflow_Get_Weather.json @@ -0,0 +1,53 @@ +{ + "name": "Get Weather", + "nodes": [ + { + "parameters": {}, + "id": "82eed1ba-179b-4f8f-8a85-b45f0d4e5857", + "name": "Execute Workflow Trigger", + "type": "n8n-nodes-base.executeWorkflowTrigger", + "typeVersion": 1, + "position": [ + 560, + 340 + ] + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "6ad8dc55-20f3-45af-a724-c7ecac90d338", + "name": "response", + "value": "Weather is sunny", + "type": "string" + } + ] + }, + "options": {} + }, + "id": "8f3e00f6-fc92-4aba-817b-93d206158bda", + "name": "Edit Fields", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 780, + 340 + ] + } + ], + "pinData": {}, + "connections": { + "Execute Workflow Trigger": { + "main": [ + [ + { + "node": "Edit Fields", + "type": "main", + "index": 0 + } + ] + ] + } + } +} diff --git a/cypress/fixtures/Test_Subworkflow_Search_DB.json b/cypress/fixtures/Test_Subworkflow_Search_DB.json new file mode 100644 index 0000000000..990aee120d --- /dev/null +++ b/cypress/fixtures/Test_Subworkflow_Search_DB.json @@ -0,0 +1,64 @@ +{ + "name": "Search DB", + "nodes": [ + { + "parameters": {}, + "id": "64465f9b-63de-43f9-8d90-b5b2eb7a2dc7", + "name": "Execute Workflow Trigger", + "type": "n8n-nodes-base.executeWorkflowTrigger", + "typeVersion": 1, + "position": [ + 640, + 380 + ] + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "6ad8dc55-20f3-45af-a724-c7ecac90d338", + "name": "response", + "value": "10 results found", + "type": "string" + } + ] + }, + "options": {} + }, + "id": "b580fd2b-00c8-4a52-8acb-024f204c0947", + "name": "Edit Fields", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 860, + 380 + ] + } + ], + "pinData": {}, + "connections": { + "Execute Workflow Trigger": { + "main": [ + [ + { + "node": "Edit Fields", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "6026f7a4-f5dc-4c27-9f83-3a02fc6e33ae", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4" + }, + "id": "BFFhCdBZmNSkx4qf", + "tags": [] +} \ No newline at end of file diff --git a/cypress/fixtures/aiAssistant/apply_code_diff_response.json b/cypress/fixtures/aiAssistant/apply_code_diff_response.json new file mode 100644 index 0000000000..8d7ada0b40 --- /dev/null +++ b/cypress/fixtures/aiAssistant/apply_code_diff_response.json @@ -0,0 +1,8 @@ +{ + "data": { + "sessionId": "f9130bd7-c078-4862-a38a-369b27b0ff20-e96eb9f7-d581-4684-b6a9-fd3dfe9fe1fb-emTezIGat7bQsDdtIlbti", + "parameters": { + "jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.myNewField = 1;\n}\n\nreturn $input.all();" + } + } +} diff --git a/cypress/fixtures/aiAssistant/code_diff_suggestion_response.json b/cypress/fixtures/aiAssistant/code_diff_suggestion_response.json new file mode 100644 index 0000000000..8ee5d647fd --- /dev/null +++ b/cypress/fixtures/aiAssistant/code_diff_suggestion_response.json @@ -0,0 +1,23 @@ +{ + "sessionId": "1", + "messages": [ + { + "role": "assistant", + "type": "message", + "text": "Hi there! Here is my top solution to fix the error in your **Code** node 👇" + }, + { + "type": "code-diff", + "description": "Fix the syntax error by changing '1asd' to a valid value. In this case, it seems like '1' was intended.", + "suggestionId": "1", + "codeDiff": "@@ -2,2 +2,2 @@\n item.json.myNewField = 1asd;\n+ item.json.myNewField = 1;\n", + "role": "assistant", + "quickReplies": [ + { + "text": "Give me another solution", + "type": "new-suggestion" + } + ] + } + ] +} diff --git a/cypress/fixtures/aiAssistant/end_session_response.json b/cypress/fixtures/aiAssistant/end_session_response.json new file mode 100644 index 0000000000..c53574d93a --- /dev/null +++ b/cypress/fixtures/aiAssistant/end_session_response.json @@ -0,0 +1,16 @@ +{ + "sessionId": "f9130bd7-c078-4862-a38a-369b27b0ff20-e96eb9f7-d581-4684-b6a9-fd3dfe9fe1fb-XCldJLlusGrEVku5I9cYT", + "messages": [ + { + "role": "assistant", + "type": "agent-suggestion", + "title": "Glad to Help", + "text": "I'm glad I could help. If you have any more questions or need further assistance with your n8n workflows, feel free to ask!" + }, + { + "role": "assistant", + "type": "event", + "eventName": "end-session" + } + ] +} diff --git a/cypress/fixtures/aiAssistant/quick_reply_message_response.json b/cypress/fixtures/aiAssistant/quick_reply_message_response.json new file mode 100644 index 0000000000..a3c1b958c4 --- /dev/null +++ b/cypress/fixtures/aiAssistant/quick_reply_message_response.json @@ -0,0 +1,20 @@ +{ + "sessionId": "1", + "messages": [ + { + "role": "assistant", + "type": "message", + "text": "Hey, this is an assistant message", + "quickReplies": [ + { + "text": "Sure, let's do it", + "type": "yes" + }, + { + "text": "Nah, doesn't sound good", + "type": "no" + } + ] + } + ] +} diff --git a/cypress/fixtures/aiAssistant/simple_message_response.json b/cypress/fixtures/aiAssistant/simple_message_response.json new file mode 100644 index 0000000000..11299b91f9 --- /dev/null +++ b/cypress/fixtures/aiAssistant/simple_message_response.json @@ -0,0 +1,10 @@ +{ + "sessionId": "1", + "messages": [ + { + "role": "assistant", + "type": "message", + "text": "Hey, this is an assistant message" + } + ] +} diff --git a/cypress/fixtures/aiAssistant/test_workflow.json b/cypress/fixtures/aiAssistant/test_workflow.json new file mode 100644 index 0000000000..da930ea489 --- /dev/null +++ b/cypress/fixtures/aiAssistant/test_workflow.json @@ -0,0 +1,88 @@ +{ + "nodes": [ + { + "parameters": {}, + "id": "ebfced75-2ce1-4c41-a971-6c3b83522c4d", + "name": "When clicking ‘Test workflow’", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 360, + 220 + ] + }, + { + "parameters": { + "errorMessage": "This is an error message" + }, + "id": "f2e60459-401a-49d5-acfc-7b2b31cfdcf7", + "name": "Stop and Error", + "type": "n8n-nodes-base.stopAndError", + "typeVersion": 1, + "position": [ + 1020, + 220 + ] + }, + { + "parameters": { + "jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.myNewField = 1aaa;\n}\n\nreturn $input.all();" + }, + "id": "b54d4db9-b257-41a8-862f-26d293115bad", + "name": "Code", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 840, + 320 + ] + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "053ada73-f7db-4e6a-8cc8-85756cc6ca4e", + "name": "age", + "value": "={{ 32sad }}", + "type": "number" + } + ] + }, + "options": {} + }, + "id": "5fd89612-a871-4679-b7b0-d659e09c6a0e", + "name": "Edit Fields", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 600, + 100 + ] + } + ], + "connections": { + "When clicking ‘Test workflow’": { + "main": [ + [ + { + "node": "Stop and Error", + "type": "main", + "index": 0 + }, + { + "node": "Code", + "type": "main", + "index": 0 + }, + { + "node": "Edit Fields", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": {} +} diff --git a/cypress/package.json b/cypress/package.json index 0e1ce734c0..5084e0b10c 100644 --- a/cypress/package.json +++ b/cypress/package.json @@ -26,6 +26,6 @@ "cypress-real-events": "^1.12.0", "lodash": "catalog:", "nanoid": "catalog:", - "start-server-and-test": "^2.0.3" + "start-server-and-test": "^2.0.5" } } diff --git a/cypress/pages/features/ai-assistant.ts b/cypress/pages/features/ai-assistant.ts new file mode 100644 index 0000000000..abca07fbbe --- /dev/null +++ b/cypress/pages/features/ai-assistant.ts @@ -0,0 +1,49 @@ +import { overrideFeatureFlag } from '../../composables/featureFlags'; +import { BasePage } from '../base'; + +const AI_ASSISTANT_FEATURE = { + name: 'aiAssistant', + experimentName: '021_ai_debug_helper', + enabledFor: 'variant', + disabledFor: 'control', +}; + +export class AIAssistant extends BasePage { + url = '/workflows/new'; + + getters = { + askAssistantFloatingButton: () => cy.getByTestId('ask-assistant-floating-button'), + askAssistantSidebar: () => cy.getByTestId('ask-assistant-sidebar'), + askAssistantSidebarResizer: () => + this.getters.askAssistantSidebar().find('[class^=_resizer][data-dir=left]').first(), + askAssistantChat: () => cy.getByTestId('ask-assistant-chat'), + placeholderMessage: () => cy.getByTestId('placeholder-message'), + closeChatButton: () => cy.getByTestId('close-chat-button'), + chatInputWrapper: () => cy.getByTestId('chat-input-wrapper'), + chatInput: () => cy.getByTestId('chat-input'), + sendMessageButton: () => cy.getByTestId('send-message-button'), + chatMessagesAll: () => cy.get('[data-test-id^=chat-message]'), + chatMessagesAssistant: () => cy.getByTestId('chat-message-assistant'), + chatMessagesUser: () => cy.getByTestId('chat-message-user'), + chatMessagesSystem: () => cy.getByTestId('chat-message-system'), + quickReplies: () => cy.getByTestId('quick-replies').find('button'), + newAssistantSessionModal: () => cy.getByTestId('new-assistant-session-modal'), + codeDiffs: () => cy.getByTestId('code-diff-suggestion'), + applyCodeDiffButtons: () => cy.getByTestId('replace-code-button'), + undoReplaceCodeButtons: () => cy.getByTestId('undo-replace-button'), + codeReplacedMessage: () => cy.getByTestId('code-replaced-message'), + nodeErrorViewAssistantButton: () => + cy.getByTestId('node-error-view-ask-assistant-button').find('button').first(), + }; + + actions = { + enableAssistant(): void { + overrideFeatureFlag(AI_ASSISTANT_FEATURE.experimentName, AI_ASSISTANT_FEATURE.enabledFor); + cy.enableFeature(AI_ASSISTANT_FEATURE.name); + }, + disableAssistant(): void { + overrideFeatureFlag(AI_ASSISTANT_FEATURE.experimentName, AI_ASSISTANT_FEATURE.disabledFor); + cy.disableFeature(AI_ASSISTANT_FEATURE.name); + }, + }; +} diff --git a/cypress/pages/ndv.ts b/cypress/pages/ndv.ts index 1a80d79707..8bd7ccf95f 100644 --- a/cypress/pages/ndv.ts +++ b/cypress/pages/ndv.ts @@ -138,6 +138,8 @@ export class NDV extends BasePage { cy.getByTestId(`fixed-collection-${paramName}`), schemaViewNode: () => cy.getByTestId('run-data-schema-node'), schemaViewNodeName: () => cy.getByTestId('run-data-schema-node-name'), + expressionExpanders: () => cy.getByTestId('expander'), + expressionModalOutput: () => cy.getByTestId('expression-modal-output'), }; actions = { @@ -175,7 +177,7 @@ export class NDV extends BasePage { this.getters.editPinnedDataButton().click(); this.getters.pinnedDataEditor().click(); - this.getters.pinnedDataEditor().type('{selectall}{backspace}').paste(JSON.stringify(data)); + this.getters.pinnedDataEditor().invoke('text', '').paste(JSON.stringify(data)); this.actions.savePinnedData(); }, diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index 3968a09b5b..4d5d7a7f9a 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -17,6 +17,8 @@ beforeEach(() => { cy.window().then((win): void => { win.localStorage.setItem('N8N_THEME', 'light'); + win.localStorage.setItem('N8N_AUTOCOMPLETE_ONBOARDED', 'true'); + win.localStorage.setItem('N8N_MAPPING_ONBOARDED', 'true'); }); cy.intercept('GET', '/rest/settings', (req) => { diff --git a/cypress/utils/popper.ts b/cypress/utils/popper.ts index 5743c70f3e..43ef2997cf 100644 --- a/cypress/utils/popper.ts +++ b/cypress/utils/popper.ts @@ -3,7 +3,7 @@ export function getPopper() { } export function getVisiblePopper() { - return getPopper().filter(':visible'); + return getPopper().filter('[aria-hidden="false"]'); } export function getVisibleSelect() { diff --git a/package.json b/package.json index e7d55c6445..545a7c851f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n-monorepo", - "version": "1.55.0", + "version": "1.56.0", "private": true, "engines": { "node": ">=20.15", @@ -63,7 +63,6 @@ ], "overrides": { "@types/node": "^18.16.16", - "axios": "1.7.3", "chokidar": "3.5.2", "esbuild": "^0.20.2", "formidable": "3.5.1", diff --git a/packages/@n8n/benchmark/Dockerfile b/packages/@n8n/benchmark/Dockerfile new file mode 100644 index 0000000000..5fa1aeae93 --- /dev/null +++ b/packages/@n8n/benchmark/Dockerfile @@ -0,0 +1,62 @@ +# syntax=docker/dockerfile:1 +FROM node:20.16.0 AS base + +# Install required dependencies +RUN apt-get update && apt-get install -y gnupg2 curl + +# Add k6 GPG key and repository +RUN mkdir -p /etc/apt/keyrings && \ + curl -sS https://dl.k6.io/key.gpg | gpg --dearmor --yes -o /etc/apt/keyrings/k6.gpg && \ + chmod a+x /etc/apt/keyrings/k6.gpg && \ + echo "deb [signed-by=/etc/apt/keyrings/k6.gpg] https://dl.k6.io/deb stable main" | tee /etc/apt/sources.list.d/k6.list + +# Update and install k6 +RUN apt-get update && \ + apt-get install -y k6 tini && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable + +# +# Builder +FROM base AS builder + +WORKDIR /app + +COPY --chown=node:node ./pnpm-lock.yaml /app/pnpm-lock.yaml +COPY --chown=node:node ./pnpm-workspace.yaml /app/pnpm-workspace.yaml +COPY --chown=node:node ./package.json /app/package.json +COPY --chown=node:node ./packages/@n8n/benchmark/package.json /app/packages/@n8n/benchmark/package.json +COPY --chown=node:node ./patches /app/patches +COPY --chown=node:node ./scripts /app/scripts + +RUN pnpm install --frozen-lockfile + +# TS config files +COPY --chown=node:node ./tsconfig.json /app/tsconfig.json +COPY --chown=node:node ./tsconfig.build.json /app/tsconfig.build.json +COPY --chown=node:node ./tsconfig.backend.json /app/tsconfig.backend.json +COPY --chown=node:node ./packages/@n8n/benchmark/tsconfig.json /app/packages/@n8n/benchmark/tsconfig.json +COPY --chown=node:node ./packages/@n8n/benchmark/tsconfig.build.json /app/packages/@n8n/benchmark/tsconfig.build.json + +# Source files +COPY --chown=node:node ./packages/@n8n/benchmark/src /app/packages/@n8n/benchmark/src +COPY --chown=node:node ./packages/@n8n/benchmark/bin /app/packages/@n8n/benchmark/bin +COPY --chown=node:node ./packages/@n8n/benchmark/scenarios /app/packages/@n8n/benchmark/scenarios + +WORKDIR /app/packages/@n8n/benchmark +RUN pnpm build + +# +# Runner +FROM base AS runner + +COPY --from=builder /app /app + +WORKDIR /app/packages/@n8n/benchmark +USER node + +ENTRYPOINT [ "/app/packages/@n8n/benchmark/bin/n8n-benchmark" ] diff --git a/packages/@n8n/benchmark/README.md b/packages/@n8n/benchmark/README.md new file mode 100644 index 0000000000..569bcf897f --- /dev/null +++ b/packages/@n8n/benchmark/README.md @@ -0,0 +1,55 @@ +# n8n benchmarking tool + +Tool for executing benchmarks against an n8n instance. + +## Running locally with Docker + +Build the Docker image: + +```sh +# Must be run in the repository root +# k6 doesn't have an arm64 build available for linux, we need to build against amd64 +docker build --platform linux/amd64 -t n8n-benchmark -f packages/@n8n/benchmark/Dockerfile . +``` + +Run the image + +```sh +docker run \ + -e N8N_USER_EMAIL=user@n8n.io \ + -e N8N_USER_PASSWORD=password \ + # For macos, n8n running outside docker + -e N8N_BASE_URL=http://host.docker.internal:5678 \ + n8n-benchmark +``` + +## Running locally without Docker + +Requirements: + +- [k6](https://grafana.com/docs/k6/latest/set-up/install-k6/) +- Node.js v20 or higher + +```sh +pnpm build + +# Run tests against http://localhost:5678 with specified email and password +N8N_USER_EMAIL=user@n8n.io N8N_USER_PASSWORD=password ./bin/n8n-benchmark run + +# If you installed k6 using brew, you might have to specify it explicitly +K6_PATH=/opt/homebrew/bin/k6 N8N_USER_EMAIL=user@n8n.io N8N_USER_PASSWORD=password ./bin/n8n-benchmark run +``` + +## Configuration + +The configuration options the cli accepts can be seen from [config.ts](./src/config/config.ts) + +## Benchmark scenarios + +A benchmark scenario defines one or multiple steps to execute and measure. It consists of: + +- Manifest file which describes and configures the scenario +- Any test data that is imported before the scenario is run +- A [`k6`](https://grafana.com/docs/k6/latest/using-k6/http-requests/) script which executes the steps and receives `API_BASE_URL` environment variable in runtime. + +Available scenarios are located in [`./scenarios`](./scenarios/). diff --git a/packages/@n8n/benchmark/bin/n8n-benchmark b/packages/@n8n/benchmark/bin/n8n-benchmark new file mode 100755 index 0000000000..c7f0996f09 --- /dev/null +++ b/packages/@n8n/benchmark/bin/n8n-benchmark @@ -0,0 +1,13 @@ +#!/usr/bin/env node + +// Check if version should be displayed +const versionFlags = ['-v', '-V', '--version']; +if (versionFlags.includes(process.argv.slice(-1)[0])) { + console.log(require('../package').version); + process.exit(0); +} + +(async () => { + const oclif = require('@oclif/core'); + await oclif.execute({ dir: __dirname }); +})(); diff --git a/packages/@n8n/benchmark/package.json b/packages/@n8n/benchmark/package.json new file mode 100644 index 0000000000..3a7afb50a3 --- /dev/null +++ b/packages/@n8n/benchmark/package.json @@ -0,0 +1,48 @@ +{ + "name": "@n8n/n8n-benchmark", + "version": "1.0.0", + "description": "Cli for running benchmark tests for n8n", + "main": "dist/index", + "scripts": { + "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json", + "start": "./bin/n8n-benchmark", + "test": "echo \"Error: no test specified\" && exit 1", + "typecheck": "tsc --noEmit", + "watch": "concurrently \"tsc -w -p tsconfig.build.json\" \"tsc-alias -w -p tsconfig.build.json\"" + }, + "engines": { + "node": ">=20.10" + }, + "keywords": [ + "automate", + "automation", + "IaaS", + "iPaaS", + "n8n", + "workflow", + "benchmark", + "performance" + ], + "dependencies": { + "@oclif/core": "4.0.7", + "axios": "catalog:", + "convict": "6.2.4", + "dotenv": "8.6.0", + "zx": "^8.1.4" + }, + "devDependencies": { + "@types/convict": "^6.1.1", + "@types/k6": "^0.52.0", + "@types/node": "^20.14.8", + "tsc-alias": "^1.8.7", + "typescript": "^5.5.2" + }, + "bin": { + "n8n-benchmark": "./bin/n8n-benchmark" + }, + "oclif": { + "bin": "n8n-benchmark", + "commands": "./dist/commands", + "topicSeparator": " " + } +} diff --git a/packages/@n8n/benchmark/scenarios/scenario.schema.json b/packages/@n8n/benchmark/scenarios/scenario.schema.json new file mode 100644 index 0000000000..661fc054b6 --- /dev/null +++ b/packages/@n8n/benchmark/scenarios/scenario.schema.json @@ -0,0 +1,42 @@ +{ + "definitions": { + "ScenarioData": { + "type": "object", + "properties": { + "workflowFiles": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [], + "additionalProperties": false + } + }, + "type": "object", + "properties": { + "$schema": { + "type": "string", + "description": "The JSON schema to validate this file" + }, + "name": { + "type": "string", + "description": "The name of the scenario" + }, + "description": { + "type": "string", + "description": "A longer description of the scenario" + }, + "scriptPath": { + "type": "string", + "description": "Relative path to the k6 test script" + }, + "scenarioData": { + "$ref": "#/definitions/ScenarioData", + "description": "Data to import before running the scenario" + } + }, + "required": ["name", "description", "scriptPath", "scenarioData"], + "additionalProperties": false +} diff --git a/packages/@n8n/benchmark/scenarios/singleWebhook/singleWebhook.json b/packages/@n8n/benchmark/scenarios/singleWebhook/singleWebhook.json new file mode 100644 index 0000000000..cba1aa5832 --- /dev/null +++ b/packages/@n8n/benchmark/scenarios/singleWebhook/singleWebhook.json @@ -0,0 +1,25 @@ +{ + "createdAt": "2024-08-06T12:19:51.268Z", + "updatedAt": "2024-08-06T12:20:45.000Z", + "name": "Single Webhook", + "active": true, + "nodes": [ + { + "parameters": { "path": "single-webhook", "options": {} }, + "id": "7587ab0e-cc15-424f-83c0-c887a0eb97fb", + "name": "Webhook", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "position": [760, 400], + "webhookId": "fa563fc2-c73f-4631-99a1-39c16f1f858f" + } + ], + "connections": {}, + "settings": { "executionOrder": "v1" }, + "staticData": null, + "meta": { "templateCredsSetupCompleted": true, "responseMode": "lastNode", "options": {} }, + "pinData": {}, + "versionId": "840a38a1-ba37-433d-9f20-de73f5131a2b", + "triggerCount": 1, + "tags": [] +} diff --git a/packages/@n8n/benchmark/scenarios/singleWebhook/singleWebhook.manifest.json b/packages/@n8n/benchmark/scenarios/singleWebhook/singleWebhook.manifest.json new file mode 100644 index 0000000000..e9b4664a96 --- /dev/null +++ b/packages/@n8n/benchmark/scenarios/singleWebhook/singleWebhook.manifest.json @@ -0,0 +1,7 @@ +{ + "$schema": "../scenario.schema.json", + "name": "SingleWebhook", + "description": "A single webhook trigger that responds with a 200 status code", + "scenarioData": { "workflowFiles": ["singleWebhook.json"] }, + "scriptPath": "singleWebhook.script.ts" +} diff --git a/packages/@n8n/benchmark/scenarios/singleWebhook/singleWebhook.script.ts b/packages/@n8n/benchmark/scenarios/singleWebhook/singleWebhook.script.ts new file mode 100644 index 0000000000..72e2563cbe --- /dev/null +++ b/packages/@n8n/benchmark/scenarios/singleWebhook/singleWebhook.script.ts @@ -0,0 +1,11 @@ +import http from 'k6/http'; +import { check } from 'k6'; + +const apiBaseUrl = __ENV.API_BASE_URL; + +export default function () { + const res = http.get(`${apiBaseUrl}/webhook/single-webhook`); + check(res, { + 'is status 200': (r) => r.status === 200, + }); +} diff --git a/packages/@n8n/benchmark/src/commands/list.ts b/packages/@n8n/benchmark/src/commands/list.ts new file mode 100644 index 0000000000..fcc60b1b81 --- /dev/null +++ b/packages/@n8n/benchmark/src/commands/list.ts @@ -0,0 +1,21 @@ +import { Command } from '@oclif/core'; +import { ScenarioLoader } from '@/scenario/scenarioLoader'; +import { loadConfig } from '@/config/config'; + +export default class ListCommand extends Command { + static description = 'List all available scenarios'; + + async run() { + const config = loadConfig(); + const scenarioLoader = new ScenarioLoader(); + + const allScenarios = scenarioLoader.loadAll(config.get('testScenariosPath')); + + console.log('Available test scenarios:'); + console.log(''); + + for (const scenario of allScenarios) { + console.log('\t', scenario.name, ':', scenario.description); + } + } +} diff --git a/packages/@n8n/benchmark/src/commands/run.ts b/packages/@n8n/benchmark/src/commands/run.ts new file mode 100644 index 0000000000..d69b4a54d4 --- /dev/null +++ b/packages/@n8n/benchmark/src/commands/run.ts @@ -0,0 +1,39 @@ +import { Command, Flags } from '@oclif/core'; +import { loadConfig } from '@/config/config'; +import { ScenarioLoader } from '@/scenario/scenarioLoader'; +import { ScenarioRunner } from '@/testExecution/scenarioRunner'; +import { N8nApiClient } from '@/n8nApiClient/n8nApiClient'; +import { ScenarioDataFileLoader } from '@/scenario/scenarioDataLoader'; +import { K6Executor } from '@/testExecution/k6Executor'; + +export default class RunCommand extends Command { + static description = 'Run all (default) or specified test scenarios'; + + // TODO: Add support for filtering scenarios + static flags = { + scenarios: Flags.string({ + char: 't', + description: 'Comma-separated list of test scenarios to run', + required: false, + }), + }; + + async run() { + const config = loadConfig(); + const scenarioLoader = new ScenarioLoader(); + + const scenarioRunner = new ScenarioRunner( + new N8nApiClient(config.get('n8n.baseUrl')), + new ScenarioDataFileLoader(), + new K6Executor(config.get('k6ExecutablePath'), config.get('n8n.baseUrl')), + { + email: config.get('n8n.user.email'), + password: config.get('n8n.user.password'), + }, + ); + + const allScenarios = scenarioLoader.loadAll(config.get('testScenariosPath')); + + await scenarioRunner.runManyScenarios(allScenarios); + } +} diff --git a/packages/@n8n/benchmark/src/config/config.ts b/packages/@n8n/benchmark/src/config/config.ts new file mode 100644 index 0000000000..896ecc9296 --- /dev/null +++ b/packages/@n8n/benchmark/src/config/config.ts @@ -0,0 +1,50 @@ +import convict from 'convict'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const configSchema = { + testScenariosPath: { + doc: 'The path to the scenarios', + format: String, + default: 'scenarios', + }, + n8n: { + baseUrl: { + doc: 'The base URL for the n8n instance', + format: String, + default: 'http://localhost:5678', + env: 'N8N_BASE_URL', + }, + user: { + email: { + doc: 'The email address of the n8n user', + format: String, + default: 'benchmark-user@n8n.io', + env: 'N8N_USER_EMAIL', + }, + password: { + doc: 'The password of the n8n user', + format: String, + default: 'VerySecret!123', + env: 'N8N_USER_PASSWORD', + }, + }, + }, + k6ExecutablePath: { + doc: 'The path to the k6 binary', + format: String, + default: 'k6', + env: 'K6_PATH', + }, +}; + +export type Config = ReturnType; + +export function loadConfig() { + const config = convict(configSchema); + + config.validate({ allowed: 'strict' }); + + return config; +} diff --git a/packages/@n8n/benchmark/src/n8nApiClient/authenticatedN8nApiClient.ts b/packages/@n8n/benchmark/src/n8nApiClient/authenticatedN8nApiClient.ts new file mode 100644 index 0000000000..93cd767347 --- /dev/null +++ b/packages/@n8n/benchmark/src/n8nApiClient/authenticatedN8nApiClient.ts @@ -0,0 +1,67 @@ +import { strict as assert } from 'node:assert'; +import { N8nApiClient } from './n8nApiClient'; +import { AxiosRequestConfig } from 'axios'; + +export class AuthenticatedN8nApiClient extends N8nApiClient { + constructor( + apiBaseUrl: string, + private readonly authCookie: string, + ) { + super(apiBaseUrl); + } + + static async createUsingUsernameAndPassword( + apiClient: N8nApiClient, + loginDetails: { + email: string; + password: string; + }, + ) { + const response = await apiClient.restApiRequest('/login', { + method: 'POST', + data: loginDetails, + }); + + const cookieHeader = response.headers['set-cookie']; + const authCookie = Array.isArray(cookieHeader) ? cookieHeader.join('; ') : cookieHeader; + assert(authCookie); + + return new AuthenticatedN8nApiClient(apiClient.apiBaseUrl, authCookie); + } + + async get(endpoint: string) { + return await this.authenticatedRequest(endpoint, { + method: 'GET', + }); + } + + async post(endpoint: string, data: unknown) { + return await this.authenticatedRequest(endpoint, { + method: 'POST', + data, + }); + } + + async patch(endpoint: string, data: unknown) { + return await this.authenticatedRequest(endpoint, { + method: 'PATCH', + data, + }); + } + + async delete(endpoint: string) { + return await this.authenticatedRequest(endpoint, { + method: 'DELETE', + }); + } + + protected async authenticatedRequest(endpoint: string, init: Omit) { + return await this.restApiRequest(endpoint, { + ...init, + headers: { + ...init.headers, + cookie: this.authCookie, + }, + }); + } +} diff --git a/packages/@n8n/benchmark/src/n8nApiClient/n8nApiClient.ts b/packages/@n8n/benchmark/src/n8nApiClient/n8nApiClient.ts new file mode 100644 index 0000000000..86ca52aff8 --- /dev/null +++ b/packages/@n8n/benchmark/src/n8nApiClient/n8nApiClient.ts @@ -0,0 +1,78 @@ +import axios, { AxiosError, AxiosRequestConfig } from 'axios'; + +export class N8nApiClient { + constructor(public readonly apiBaseUrl: string) {} + + async waitForInstanceToBecomeOnline(): Promise { + const HEALTH_ENDPOINT = 'healthz'; + const START_TIME = Date.now(); + const INTERVAL_MS = 1000; + const TIMEOUT_MS = 60_000; + + while (Date.now() - START_TIME < TIMEOUT_MS) { + try { + const response = await axios.request({ + url: `${this.apiBaseUrl}/${HEALTH_ENDPOINT}`, + method: 'GET', + }); + + if (response.status === 200 && response.data.status === 'ok') { + return; + } + } catch {} + + console.log(`n8n instance not online yet, retrying in ${INTERVAL_MS / 1000} seconds...`); + await this.delay(INTERVAL_MS); + } + + throw new Error(`n8n instance did not come online within ${TIMEOUT_MS / 1000} seconds`); + } + + async setupOwnerIfNeeded(loginDetails: { email: string; password: string }) { + const response = await this.restApiRequest<{ message: string }>('/owner/setup', { + method: 'POST', + data: { + email: loginDetails.email, + password: loginDetails.password, + firstName: 'Test', + lastName: 'User', + }, + // Don't throw on non-2xx responses + validateStatus: () => true, + }); + + const responsePayload = response.data; + + if (response.status === 200) { + console.log('Owner setup successful'); + } else if (response.status === 400) { + if (responsePayload.message === 'Instance owner already setup') + console.log('Owner already set up'); + } else { + throw new Error( + `Owner setup failed with status ${response.status}: ${responsePayload.message}`, + ); + } + } + + async restApiRequest(endpoint: string, init: Omit) { + try { + return await axios.request({ + ...init, + url: this.getRestEndpointUrl(endpoint), + }); + } catch (e) { + const error = e as AxiosError; + console.error(`[ERROR] Request failed ${init.method} ${endpoint}`, error?.response?.data); + throw error; + } + } + + protected getRestEndpointUrl(endpoint: string) { + return `${this.apiBaseUrl}/rest${endpoint}`; + } + + private delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } +} diff --git a/packages/@n8n/benchmark/src/n8nApiClient/n8nApiClient.types.ts b/packages/@n8n/benchmark/src/n8nApiClient/n8nApiClient.types.ts new file mode 100644 index 0000000000..ff6aa6930b --- /dev/null +++ b/packages/@n8n/benchmark/src/n8nApiClient/n8nApiClient.types.ts @@ -0,0 +1,8 @@ +/** + * n8n workflow. This is a simplified version of the actual workflow object. + */ +export type Workflow = { + id: string; + name: string; + tags?: string[]; +}; diff --git a/packages/@n8n/benchmark/src/n8nApiClient/workflowsApiClient.ts b/packages/@n8n/benchmark/src/n8nApiClient/workflowsApiClient.ts new file mode 100644 index 0000000000..18f2ecbcda --- /dev/null +++ b/packages/@n8n/benchmark/src/n8nApiClient/workflowsApiClient.ts @@ -0,0 +1,31 @@ +import { Workflow } from '@/n8nApiClient/n8nApiClient.types'; +import { AuthenticatedN8nApiClient } from './authenticatedN8nApiClient'; + +export class WorkflowApiClient { + constructor(private readonly apiClient: AuthenticatedN8nApiClient) {} + + async getAllWorkflows(): Promise { + const response = await this.apiClient.get<{ count: number; data: Workflow[] }>('/workflows'); + + return response.data.data; + } + + async createWorkflow(workflow: unknown): Promise { + const response = await this.apiClient.post<{ data: Workflow }>('/workflows', workflow); + + return response.data.data; + } + + async activateWorkflow(workflow: Workflow): Promise { + const response = await this.apiClient.patch<{ data: Workflow }>(`/workflows/${workflow.id}`, { + ...workflow, + active: true, + }); + + return response.data.data; + } + + async deleteWorkflow(workflowId: Workflow['id']): Promise { + await this.apiClient.delete(`/workflows/${workflowId}`); + } +} diff --git a/packages/@n8n/benchmark/src/scenario/scenarioDataLoader.ts b/packages/@n8n/benchmark/src/scenario/scenarioDataLoader.ts new file mode 100644 index 0000000000..43638a2e00 --- /dev/null +++ b/packages/@n8n/benchmark/src/scenario/scenarioDataLoader.ts @@ -0,0 +1,35 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { Scenario } from '@/types/scenario'; +import { Workflow } from '@/n8nApiClient/n8nApiClient.types'; + +/** + * Loads scenario data files from FS + */ +export class ScenarioDataFileLoader { + async loadDataForScenario(scenario: Scenario): Promise<{ + workflows: Workflow[]; + }> { + const workflows = await Promise.all( + scenario.scenarioData.workflowFiles?.map((workflowFilePath) => + this.loadSingleWorkflowFromFile(path.join(scenario.scenarioDirPath, workflowFilePath)), + ) ?? [], + ); + + return { + workflows, + }; + } + + private loadSingleWorkflowFromFile(workflowFilePath: string): Workflow { + const fileContent = fs.readFileSync(workflowFilePath, 'utf8'); + + try { + return JSON.parse(fileContent); + } catch (error) { + throw new Error( + `Failed to parse workflow file ${workflowFilePath}: ${error instanceof Error ? error.message : error}`, + ); + } + } +} diff --git a/packages/@n8n/benchmark/src/scenario/scenarioLoader.ts b/packages/@n8n/benchmark/src/scenario/scenarioLoader.ts new file mode 100644 index 0000000000..475ce495ac --- /dev/null +++ b/packages/@n8n/benchmark/src/scenario/scenarioLoader.ts @@ -0,0 +1,67 @@ +import * as fs from 'node:fs'; +import * as path from 'path'; +import { createHash } from 'node:crypto'; +import type { Scenario, ScenarioManifest } from '@/types/scenario'; + +export class ScenarioLoader { + /** + * Loads all scenarios from the given path + */ + loadAll(pathToScenarios: string): Scenario[] { + pathToScenarios = path.resolve(pathToScenarios); + const scenarioFolders = fs + .readdirSync(pathToScenarios, { withFileTypes: true }) + .filter((dirent) => dirent.isDirectory()) + .map((dirent) => dirent.name); + + const scenarios: Scenario[] = []; + + for (const folder of scenarioFolders) { + const scenarioPath = path.join(pathToScenarios, folder); + const manifestFileName = `${folder}.manifest.json`; + const scenarioManifestPath = path.join(pathToScenarios, folder, manifestFileName); + if (!fs.existsSync(scenarioManifestPath)) { + console.warn(`Scenario at ${scenarioPath} is missing the ${manifestFileName} file`); + continue; + } + + // Load the scenario manifest file + const [scenario, validationErrors] = + this.loadAndValidateScenarioManifest(scenarioManifestPath); + if (validationErrors) { + console.warn( + `Scenario at ${scenarioPath} has the following validation errors: ${validationErrors.join(', ')}`, + ); + continue; + } + + scenarios.push({ + ...scenario, + id: this.formScenarioId(scenarioPath), + scenarioDirPath: scenarioPath, + }); + } + + return scenarios; + } + + private loadAndValidateScenarioManifest( + scenarioManifestPath: string, + ): [ScenarioManifest, null] | [null, string[]] { + const scenario = JSON.parse(fs.readFileSync(scenarioManifestPath, 'utf8')); + const validationErrors: string[] = []; + + if (!scenario.name) { + validationErrors.push(`Scenario at ${scenarioManifestPath} is missing a name`); + } + if (!scenario.description) { + validationErrors.push(`Scenario at ${scenarioManifestPath} is missing a description`); + } + + return validationErrors.length === 0 ? [scenario, null] : [null, validationErrors]; + } + + private formScenarioId(scenarioPath: string): string { + return createHash('sha256').update(scenarioPath).digest('hex'); + } +} diff --git a/packages/@n8n/benchmark/src/testExecution/k6Executor.ts b/packages/@n8n/benchmark/src/testExecution/k6Executor.ts new file mode 100644 index 0000000000..903d06ca74 --- /dev/null +++ b/packages/@n8n/benchmark/src/testExecution/k6Executor.ts @@ -0,0 +1,28 @@ +import { $ } from 'zx'; +import { Scenario } from '@/types/scenario'; + +/** + * Executes test scenarios using k6 + */ +export class K6Executor { + constructor( + private readonly k6ExecutablePath: string, + private readonly n8nApiBaseUrl: string, + ) {} + + async executeTestScenario(scenario: Scenario) { + // For 1 min with 5 virtual users + const stage = '1m:5'; + + const processPromise = $({ + cwd: scenario.scenarioDirPath, + env: { + API_BASE_URL: this.n8nApiBaseUrl, + }, + })`${this.k6ExecutablePath} run --quiet --stage ${stage} ${scenario.scriptPath}`; + + for await (const chunk of processPromise.stdout) { + console.log(chunk.toString()); + } + } +} diff --git a/packages/@n8n/benchmark/src/testExecution/scenarioDataImporter.ts b/packages/@n8n/benchmark/src/testExecution/scenarioDataImporter.ts new file mode 100644 index 0000000000..1c1ec3777e --- /dev/null +++ b/packages/@n8n/benchmark/src/testExecution/scenarioDataImporter.ts @@ -0,0 +1,56 @@ +import { AuthenticatedN8nApiClient } from '@/n8nApiClient/authenticatedN8nApiClient'; +import { Workflow } from '@/n8nApiClient/n8nApiClient.types'; +import { WorkflowApiClient } from '@/n8nApiClient/workflowsApiClient'; + +/** + * Imports scenario data into an n8n instance + */ +export class ScenarioDataImporter { + private readonly workflowApiClient: WorkflowApiClient; + + constructor(n8nApiClient: AuthenticatedN8nApiClient) { + this.workflowApiClient = new WorkflowApiClient(n8nApiClient); + } + + async importTestScenarioData(workflows: Workflow[]) { + const existingWorkflows = await this.workflowApiClient.getAllWorkflows(); + + for (const workflow of workflows) { + await this.importWorkflow({ existingWorkflows, workflow }); + } + } + + /** + * Imports a single workflow into n8n removing any existing workflows with the same name + */ + private async importWorkflow(opts: { existingWorkflows: Workflow[]; workflow: Workflow }) { + const existingWorkflows = this.findExistingWorkflows(opts.existingWorkflows, opts.workflow); + if (existingWorkflows.length > 0) { + for (const toDelete of existingWorkflows) { + await this.workflowApiClient.deleteWorkflow(toDelete.id); + } + } + + const createdWorkflow = await this.workflowApiClient.createWorkflow({ + ...opts.workflow, + name: this.getBenchmarkWorkflowName(opts.workflow), + }); + + return await this.workflowApiClient.activateWorkflow(createdWorkflow); + } + + private findExistingWorkflows( + existingWorkflows: Workflow[], + workflowToImport: Workflow, + ): Workflow[] { + const benchmarkWorkflowName = this.getBenchmarkWorkflowName(workflowToImport); + + return existingWorkflows.filter( + (existingWorkflow) => existingWorkflow.name === benchmarkWorkflowName, + ); + } + + private getBenchmarkWorkflowName(workflow: Workflow) { + return `[BENCHMARK] ${workflow.name}`; + } +} diff --git a/packages/@n8n/benchmark/src/testExecution/scenarioRunner.ts b/packages/@n8n/benchmark/src/testExecution/scenarioRunner.ts new file mode 100644 index 0000000000..83e1077680 --- /dev/null +++ b/packages/@n8n/benchmark/src/testExecution/scenarioRunner.ts @@ -0,0 +1,50 @@ +import { Scenario } from '@/types/scenario'; +import { N8nApiClient } from '@/n8nApiClient/n8nApiClient'; +import { ScenarioDataFileLoader } from '@/scenario/scenarioDataLoader'; +import { K6Executor } from './k6Executor'; +import { ScenarioDataImporter } from '@/testExecution/scenarioDataImporter'; +import { AuthenticatedN8nApiClient } from '@/n8nApiClient/authenticatedN8nApiClient'; + +/** + * Runs scenarios + */ +export class ScenarioRunner { + constructor( + private readonly n8nClient: N8nApiClient, + private readonly dataLoader: ScenarioDataFileLoader, + private readonly k6Executor: K6Executor, + private readonly ownerConfig: { + email: string; + password: string; + }, + ) {} + + async runManyScenarios(scenarios: Scenario[]) { + console.log(`Waiting for n8n ${this.n8nClient.apiBaseUrl} to become online`); + await this.n8nClient.waitForInstanceToBecomeOnline(); + + console.log('Setting up owner'); + await this.n8nClient.setupOwnerIfNeeded(this.ownerConfig); + + const authenticatedN8nClient = await AuthenticatedN8nApiClient.createUsingUsernameAndPassword( + this.n8nClient, + this.ownerConfig, + ); + const testDataImporter = new ScenarioDataImporter(authenticatedN8nClient); + + for (const scenario of scenarios) { + await this.runSingleTestScenario(testDataImporter, scenario); + } + } + + private async runSingleTestScenario(testDataImporter: ScenarioDataImporter, scenario: Scenario) { + console.log('Running scenario:', scenario.name); + + console.log('Loading and importing data'); + const testData = await this.dataLoader.loadDataForScenario(scenario); + await testDataImporter.importTestScenarioData(testData.workflows); + + console.log('Executing scenario script'); + await this.k6Executor.executeTestScenario(scenario); + } +} diff --git a/packages/@n8n/benchmark/src/types/scenario.ts b/packages/@n8n/benchmark/src/types/scenario.ts new file mode 100644 index 0000000000..19c52fd45b --- /dev/null +++ b/packages/@n8n/benchmark/src/types/scenario.ts @@ -0,0 +1,27 @@ +export type ScenarioData = { + /** Relative paths to the workflow files */ + workflowFiles?: string[]; +}; + +/** + * Configuration that defines the benchmark scenario + */ +export type ScenarioManifest = { + /** The name of the scenario */ + name: string; + /** A longer description of the scenario */ + description: string; + /** Relative path to the k6 script */ + scriptPath: string; + /** Data to import before running the scenario */ + scenarioData: ScenarioData; +}; + +/** + * Scenario with additional metadata + */ +export type Scenario = ScenarioManifest & { + id: string; + /** Path to the directory containing the scenario */ + scenarioDirPath: string; +}; diff --git a/packages/@n8n/benchmark/tsconfig.build.json b/packages/@n8n/benchmark/tsconfig.build.json new file mode 100644 index 0000000000..b91db37a4a --- /dev/null +++ b/packages/@n8n/benchmark/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": ["./tsconfig.json", "../../../tsconfig.build.json"], + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "dist/build.tsbuildinfo" + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/@n8n/benchmark/tsconfig.json b/packages/@n8n/benchmark/tsconfig.json new file mode 100644 index 0000000000..58a1b48f65 --- /dev/null +++ b/packages/@n8n/benchmark/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": ["../../../tsconfig.json", "../../../tsconfig.backend.json"], + "compilerOptions": { + "rootDir": ".", + "baseUrl": "src", + "paths": { + "@/*": ["./*"] + } + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/@n8n/config/.eslintrc.js b/packages/@n8n/config/.eslintrc.js index 032e99b09e..e5a8f3f0f9 100644 --- a/packages/@n8n/config/.eslintrc.js +++ b/packages/@n8n/config/.eslintrc.js @@ -7,4 +7,13 @@ module.exports = { extends: ['@n8n_io/eslint-config/node'], ...sharedOptions(__dirname), + + overrides: [ + { + files: ['**/*.config.ts'], + rules: { + 'n8n-local-rules/no-untyped-config-class-field': 'error', + }, + }, + ], }; diff --git a/packages/@n8n/config/package.json b/packages/@n8n/config/package.json index 1ee3fb9160..36d280a71f 100644 --- a/packages/@n8n/config/package.json +++ b/packages/@n8n/config/package.json @@ -1,6 +1,6 @@ { "name": "@n8n/config", - "version": "1.5.0", + "version": "1.6.0", "scripts": { "clean": "rimraf dist .turbo", "dev": "pnpm watch", diff --git a/packages/@n8n/config/src/configs/cache.config.ts b/packages/@n8n/config/src/configs/cache.config.ts index 8a24bdc18b..fa1295d583 100644 --- a/packages/@n8n/config/src/configs/cache.config.ts +++ b/packages/@n8n/config/src/configs/cache.config.ts @@ -4,22 +4,22 @@ import { Config, Env, Nested } from '../decorators'; class MemoryConfig { /** Max size of memory cache in bytes */ @Env('N8N_CACHE_MEMORY_MAX_SIZE') - maxSize = 3 * 1024 * 1024; // 3 MiB + maxSize: number = 3 * 1024 * 1024; // 3 MiB /** Time to live (in milliseconds) for data cached in memory. */ @Env('N8N_CACHE_MEMORY_TTL') - ttl = 3600 * 1000; // 1 hour + ttl: number = 3600 * 1000; // 1 hour } @Config class RedisConfig { /** Prefix for cache keys in Redis. */ @Env('N8N_CACHE_REDIS_KEY_PREFIX') - prefix = 'redis'; + prefix: string = 'redis'; /** Time to live (in milliseconds) for data cached in Redis. 0 for no TTL. */ @Env('N8N_CACHE_REDIS_TTL') - ttl = 3600 * 1000; // 1 hour + ttl: number = 3600 * 1000; // 1 hour } @Config diff --git a/packages/@n8n/config/src/configs/credentials.config.ts b/packages/@n8n/config/src/configs/credentials.config.ts index ee5f78a681..f40e6b9f15 100644 --- a/packages/@n8n/config/src/configs/credentials.config.ts +++ b/packages/@n8n/config/src/configs/credentials.config.ts @@ -7,18 +7,18 @@ class CredentialsOverwrite { * Format: { CREDENTIAL_NAME: { PARAMETER: VALUE }} */ @Env('CREDENTIALS_OVERWRITE_DATA') - data = '{}'; + data: string = '{}'; /** Internal API endpoint to fetch overwritten credential types from. */ @Env('CREDENTIALS_OVERWRITE_ENDPOINT') - endpoint = ''; + endpoint: string = ''; } @Config export class CredentialsConfig { /** Default name for credentials */ @Env('CREDENTIALS_DEFAULT_NAME') - defaultName = 'My credentials'; + defaultName: string = 'My credentials'; @Nested overwrite: CredentialsOverwrite; diff --git a/packages/@n8n/config/src/configs/database.config.ts b/packages/@n8n/config/src/configs/database.config.ts index 06a3f85465..9da83958e6 100644 --- a/packages/@n8n/config/src/configs/database.config.ts +++ b/packages/@n8n/config/src/configs/database.config.ts @@ -4,7 +4,7 @@ import { Config, Env, Nested } from '../decorators'; class LoggingConfig { /** Whether database logging is enabled. */ @Env('DB_LOGGING_ENABLED') - enabled = false; + enabled: boolean = false; /** * Database logging level. Requires `DB_LOGGING_MAX_EXECUTION_TIME` to be higher than `0`. @@ -16,7 +16,7 @@ class LoggingConfig { * Only queries that exceed this time (ms) will be logged. Set `0` to disable. */ @Env('DB_LOGGING_MAX_EXECUTION_TIME') - maxQueryExecutionTime = 0; + maxQueryExecutionTime: number = 0; } @Config @@ -26,38 +26,38 @@ class PostgresSSLConfig { * If `DB_POSTGRESDB_SSL_CA`, `DB_POSTGRESDB_SSL_CERT`, or `DB_POSTGRESDB_SSL_KEY` are defined, `DB_POSTGRESDB_SSL_ENABLED` defaults to `true`. */ @Env('DB_POSTGRESDB_SSL_ENABLED') - enabled = false; + enabled: boolean = false; /** SSL certificate authority */ @Env('DB_POSTGRESDB_SSL_CA') - ca = ''; + ca: string = ''; /** SSL certificate */ @Env('DB_POSTGRESDB_SSL_CERT') - cert = ''; + cert: string = ''; /** SSL key */ @Env('DB_POSTGRESDB_SSL_KEY') - key = ''; + key: string = ''; /** If unauthorized SSL connections should be rejected */ @Env('DB_POSTGRESDB_SSL_REJECT_UNAUTHORIZED') - rejectUnauthorized = true; + rejectUnauthorized: boolean = true; } @Config class PostgresConfig { /** Postgres database name */ @Env('DB_POSTGRESDB_DATABASE') - database = 'n8n'; + database: string = 'n8n'; /** Postgres database host */ @Env('DB_POSTGRESDB_HOST') - host = 'localhost'; + host: string = 'localhost'; /** Postgres database password */ @Env('DB_POSTGRESDB_PASSWORD') - password = ''; + password: string = ''; /** Postgres database port */ @Env('DB_POSTGRESDB_PORT') @@ -65,15 +65,15 @@ class PostgresConfig { /** Postgres database user */ @Env('DB_POSTGRESDB_USER') - user = 'postgres'; + user: string = 'postgres'; /** Postgres database schema */ @Env('DB_POSTGRESDB_SCHEMA') - schema = 'public'; + schema: string = 'public'; /** Postgres database pool size */ @Env('DB_POSTGRESDB_POOL_SIZE') - poolSize = 2; + poolSize: number = 2; @Nested ssl: PostgresSSLConfig; @@ -83,15 +83,15 @@ class PostgresConfig { class MysqlConfig { /** @deprecated MySQL database name */ @Env('DB_MYSQLDB_DATABASE') - database = 'n8n'; + database: string = 'n8n'; /** MySQL database host */ @Env('DB_MYSQLDB_HOST') - host = 'localhost'; + host: string = 'localhost'; /** MySQL database password */ @Env('DB_MYSQLDB_PASSWORD') - password = ''; + password: string = ''; /** MySQL database port */ @Env('DB_MYSQLDB_PORT') @@ -99,14 +99,14 @@ class MysqlConfig { /** MySQL database user */ @Env('DB_MYSQLDB_USER') - user = 'root'; + user: string = 'root'; } @Config class SqliteConfig { /** SQLite database file name */ @Env('DB_SQLITE_DATABASE') - database = 'database.sqlite'; + database: string = 'database.sqlite'; /** SQLite database pool size. Set to `0` to disable pooling. */ @Env('DB_SQLITE_POOL_SIZE') @@ -116,7 +116,7 @@ class SqliteConfig { * Enable SQLite WAL mode. */ @Env('DB_SQLITE_ENABLE_WAL') - enableWAL = this.poolSize > 1; + enableWAL: boolean = this.poolSize > 1; /** * Run `VACUUM` on startup to rebuild the database, reducing file size and optimizing indexes. @@ -124,7 +124,7 @@ class SqliteConfig { * @warning Long-running blocking operation that will increase startup time. */ @Env('DB_SQLITE_VACUUM_ON_STARTUP') - executeVacuumOnStartup = false; + executeVacuumOnStartup: boolean = false; } @Config @@ -135,7 +135,7 @@ export class DatabaseConfig { /** Prefix for table names */ @Env('DB_TABLE_PREFIX') - tablePrefix = ''; + tablePrefix: string = ''; @Nested logging: LoggingConfig; diff --git a/packages/@n8n/config/src/configs/endpoints.config.ts b/packages/@n8n/config/src/configs/endpoints.config.ts index 4957c5afa5..88efa01d26 100644 --- a/packages/@n8n/config/src/configs/endpoints.config.ts +++ b/packages/@n8n/config/src/configs/endpoints.config.ts @@ -4,51 +4,51 @@ import { Config, Env, Nested } from '../decorators'; class PrometheusMetricsConfig { /** Whether to enable the `/metrics` endpoint to expose Prometheus metrics. */ @Env('N8N_METRICS') - enable = false; + enable: boolean = false; /** Prefix for Prometheus metric names. */ @Env('N8N_METRICS_PREFIX') - prefix = 'n8n_'; + prefix: string = 'n8n_'; /** Whether to expose system and Node.js metrics. See: https://www.npmjs.com/package/prom-client */ @Env('N8N_METRICS_INCLUDE_DEFAULT_METRICS') - includeDefaultMetrics = true; + includeDefaultMetrics: boolean = true; /** Whether to include a label for workflow ID on workflow metrics. */ @Env('N8N_METRICS_INCLUDE_WORKFLOW_ID_LABEL') - includeWorkflowIdLabel = false; + includeWorkflowIdLabel: boolean = false; /** Whether to include a label for node type on node metrics. */ @Env('N8N_METRICS_INCLUDE_NODE_TYPE_LABEL') - includeNodeTypeLabel = false; + includeNodeTypeLabel: boolean = false; /** Whether to include a label for credential type on credential metrics. */ @Env('N8N_METRICS_INCLUDE_CREDENTIAL_TYPE_LABEL') - includeCredentialTypeLabel = false; + includeCredentialTypeLabel: boolean = false; /** Whether to expose metrics for API endpoints. See: https://www.npmjs.com/package/express-prom-bundle */ @Env('N8N_METRICS_INCLUDE_API_ENDPOINTS') - includeApiEndpoints = false; + includeApiEndpoints: boolean = false; /** Whether to include a label for the path of API endpoint calls. */ @Env('N8N_METRICS_INCLUDE_API_PATH_LABEL') - includeApiPathLabel = false; + includeApiPathLabel: boolean = false; /** Whether to include a label for the HTTP method of API endpoint calls. */ @Env('N8N_METRICS_INCLUDE_API_METHOD_LABEL') - includeApiMethodLabel = false; + includeApiMethodLabel: boolean = false; /** Whether to include a label for the status code of API endpoint calls. */ @Env('N8N_METRICS_INCLUDE_API_STATUS_CODE_LABEL') - includeApiStatusCodeLabel = false; + includeApiStatusCodeLabel: boolean = false; /** Whether to include metrics for cache hits and misses. */ @Env('N8N_METRICS_INCLUDE_CACHE_METRICS') - includeCacheMetrics = false; + includeCacheMetrics: boolean = false; /** Whether to include metrics derived from n8n's internal events */ @Env('N8N_METRICS_INCLUDE_MESSAGE_EVENT_BUS_METRICS') - includeMessageEventBusMetrics = false; + includeMessageEventBusMetrics: boolean = false; } @Config @@ -62,41 +62,41 @@ export class EndpointsConfig { /** Path segment for REST API endpoints. */ @Env('N8N_ENDPOINT_REST') - rest = 'rest'; + rest: string = 'rest'; /** Path segment for form endpoints. */ @Env('N8N_ENDPOINT_FORM') - form = 'form'; + form: string = 'form'; /** Path segment for test form endpoints. */ @Env('N8N_ENDPOINT_FORM_TEST') - formTest = 'form-test'; + formTest: string = 'form-test'; /** Path segment for waiting form endpoints. */ @Env('N8N_ENDPOINT_FORM_WAIT') - formWaiting = 'form-waiting'; + formWaiting: string = 'form-waiting'; /** Path segment for webhook endpoints. */ @Env('N8N_ENDPOINT_WEBHOOK') - webhook = 'webhook'; + webhook: string = 'webhook'; /** Path segment for test webhook endpoints. */ @Env('N8N_ENDPOINT_WEBHOOK_TEST') - webhookTest = 'webhook-test'; + webhookTest: string = 'webhook-test'; /** Path segment for waiting webhook endpoints. */ @Env('N8N_ENDPOINT_WEBHOOK_WAIT') - webhookWaiting = 'webhook-waiting'; + webhookWaiting: string = 'webhook-waiting'; /** Whether to disable n8n's UI (frontend). */ @Env('N8N_DISABLE_UI') - disableUi = false; + disableUi: boolean = false; /** Whether to disable production webhooks on the main process, when using webhook-specific processes. */ @Env('N8N_DISABLE_PRODUCTION_MAIN_PROCESS') - disableProductionWebhooksOnMainProcess = false; + disableProductionWebhooksOnMainProcess: boolean = false; /** Colon-delimited list of additional endpoints to not open the UI on. */ @Env('N8N_ADDITIONAL_NON_UI_ROUTES') - additionalNonUIRoutes = ''; + additionalNonUIRoutes: string = ''; } diff --git a/packages/@n8n/config/src/configs/event-bus.config.ts b/packages/@n8n/config/src/configs/event-bus.config.ts index 87db613e63..b4782555d5 100644 --- a/packages/@n8n/config/src/configs/event-bus.config.ts +++ b/packages/@n8n/config/src/configs/event-bus.config.ts @@ -4,22 +4,22 @@ import { Config, Env, Nested } from '../decorators'; class LogWriterConfig { /* of event log files to keep */ @Env('N8N_EVENTBUS_LOGWRITER_KEEPLOGCOUNT') - keepLogCount = 3; + keepLogCount: number = 3; /** Max size (in KB) of an event log file before a new one is started */ @Env('N8N_EVENTBUS_LOGWRITER_MAXFILESIZEINKB') - maxFileSizeInKB = 10240; // 10 MB + maxFileSizeInKB: number = 10240; // 10 MB /** Basename of event log file */ @Env('N8N_EVENTBUS_LOGWRITER_LOGBASENAME') - logBaseName = 'n8nEventLog'; + logBaseName: string = 'n8nEventLog'; } @Config export class EventBusConfig { /** How often (in ms) to check for unsent event messages. Can in rare cases cause a message to be sent twice. `0` to disable */ @Env('N8N_EVENTBUS_CHECKUNSENTINTERVAL') - checkUnsentInterval = 0; + checkUnsentInterval: number = 0; /** Endpoint to retrieve n8n version information from */ @Nested diff --git a/packages/@n8n/config/src/configs/external-secrets.config.ts b/packages/@n8n/config/src/configs/external-secrets.config.ts index 2e51be87bc..1195adf660 100644 --- a/packages/@n8n/config/src/configs/external-secrets.config.ts +++ b/packages/@n8n/config/src/configs/external-secrets.config.ts @@ -4,9 +4,9 @@ import { Config, Env } from '../decorators'; export class ExternalSecretsConfig { /** How often (in seconds) to check for secret updates */ @Env('N8N_EXTERNAL_SECRETS_UPDATE_INTERVAL') - updateInterval = 300; + updateInterval: number = 300; /** Whether to prefer GET over LIST when fetching secrets from Hashicorp Vault */ @Env('N8N_EXTERNAL_SECRETS_PREFER_GET') - preferGet = false; + preferGet: boolean = false; } diff --git a/packages/@n8n/config/src/configs/external-storage.config.ts b/packages/@n8n/config/src/configs/external-storage.config.ts index 3dd1448b44..6e5fbd64d8 100644 --- a/packages/@n8n/config/src/configs/external-storage.config.ts +++ b/packages/@n8n/config/src/configs/external-storage.config.ts @@ -4,29 +4,29 @@ import { Config, Env, Nested } from '../decorators'; class S3BucketConfig { /** Name of the n8n bucket in S3-compatible external storage */ @Env('N8N_EXTERNAL_STORAGE_S3_BUCKET_NAME') - name = ''; + name: string = ''; /** Region of the n8n bucket in S3-compatible external storage @example "us-east-1" */ @Env('N8N_EXTERNAL_STORAGE_S3_BUCKET_REGION') - region = ''; + region: string = ''; } @Config class S3CredentialsConfig { /** Access key in S3-compatible external storage */ @Env('N8N_EXTERNAL_STORAGE_S3_ACCESS_KEY') - accessKey = ''; + accessKey: string = ''; /** Access secret in S3-compatible external storage */ @Env('N8N_EXTERNAL_STORAGE_S3_ACCESS_SECRET') - accessSecret = ''; + accessSecret: string = ''; } @Config class S3Config { /** Host of the n8n bucket in S3-compatible external storage @example "s3.us-east-1.amazonaws.com" */ @Env('N8N_EXTERNAL_STORAGE_S3_HOST') - host = ''; + host: string = ''; @Nested bucket: S3BucketConfig; diff --git a/packages/@n8n/config/src/configs/nodes.config.ts b/packages/@n8n/config/src/configs/nodes.config.ts index cf8407762c..577c4055ab 100644 --- a/packages/@n8n/config/src/configs/nodes.config.ts +++ b/packages/@n8n/config/src/configs/nodes.config.ts @@ -47,7 +47,7 @@ export class NodesConfig { /** Node type to use as error trigger */ @Env('NODES_ERROR_TRIGGER_TYPE') - errorTriggerType = 'n8n-nodes-base.errorTrigger'; + errorTriggerType: string = 'n8n-nodes-base.errorTrigger'; @Nested communityPackages: CommunityPackagesConfig; diff --git a/packages/@n8n/config/src/configs/public-api.config.ts b/packages/@n8n/config/src/configs/public-api.config.ts index b62cac68c7..340c54fbd7 100644 --- a/packages/@n8n/config/src/configs/public-api.config.ts +++ b/packages/@n8n/config/src/configs/public-api.config.ts @@ -4,13 +4,13 @@ import { Config, Env } from '../decorators'; export class PublicApiConfig { /** Whether to disable the Public API */ @Env('N8N_PUBLIC_API_DISABLED') - disabled = false; + disabled: boolean = false; /** Path segment for the Public API */ @Env('N8N_PUBLIC_API_ENDPOINT') - path = 'api'; + path: string = 'api'; /** Whether to disable the Swagger UI for the Public API */ @Env('N8N_PUBLIC_API_SWAGGERUI_DISABLED') - swaggerUiDisabled = false; + swaggerUiDisabled: boolean = false; } diff --git a/packages/@n8n/config/src/configs/scaling-mode.config.ts b/packages/@n8n/config/src/configs/scaling-mode.config.ts index 6ff331eedd..750de77b07 100644 --- a/packages/@n8n/config/src/configs/scaling-mode.config.ts +++ b/packages/@n8n/config/src/configs/scaling-mode.config.ts @@ -4,83 +4,83 @@ import { Config, Env, Nested } from '../decorators'; class HealthConfig { /** Whether to enable the worker health check endpoint `/healthz`. */ @Env('QUEUE_HEALTH_CHECK_ACTIVE') - active = false; + active: boolean = false; /** Port for worker to respond to health checks requests on, if enabled. */ @Env('QUEUE_HEALTH_CHECK_PORT') - port = 5678; + port: number = 5678; } @Config class RedisConfig { /** Redis database for Bull queue. */ @Env('QUEUE_BULL_REDIS_DB') - db = 0; + db: number = 0; /** Redis host for Bull queue. */ @Env('QUEUE_BULL_REDIS_HOST') - host = 'localhost'; + host: string = 'localhost'; /** Password to authenticate with Redis. */ @Env('QUEUE_BULL_REDIS_PASSWORD') - password = ''; + password: string = ''; /** Port for Redis to listen on. */ @Env('QUEUE_BULL_REDIS_PORT') - port = 6379; + port: number = 6379; /** Max cumulative timeout (in milliseconds) of connection retries before process exit. */ @Env('QUEUE_BULL_REDIS_TIMEOUT_THRESHOLD') - timeoutThreshold = 10_000; + timeoutThreshold: number = 10_000; /** Redis username. Redis 6.0 or higher required. */ @Env('QUEUE_BULL_REDIS_USERNAME') - username = ''; + username: string = ''; /** Redis cluster startup nodes, as comma-separated list of `{host}:{port}` pairs. @example 'redis-1:6379,redis-2:6379' */ @Env('QUEUE_BULL_REDIS_CLUSTER_NODES') - clusterNodes = ''; + clusterNodes: string = ''; /** Whether to enable TLS on Redis connections. */ @Env('QUEUE_BULL_REDIS_TLS') - tls = false; + tls: boolean = false; } @Config class SettingsConfig { /** How long (in milliseconds) is the lease period for a worker processing a job. */ @Env('QUEUE_WORKER_LOCK_DURATION') - lockDuration = 30_000; + lockDuration: number = 30_000; /** How often (in milliseconds) a worker must renew the lease. */ @Env('QUEUE_WORKER_LOCK_RENEW_TIME') - lockRenewTime = 15_000; + lockRenewTime: number = 15_000; /** How often (in milliseconds) Bull must check for stalled jobs. `0` to disable. */ @Env('QUEUE_WORKER_STALLED_INTERVAL') - stalledInterval = 30_000; + stalledInterval: number = 30_000; /** Max number of times a stalled job will be re-processed. See Bull's [documentation](https://docs.bullmq.io/guide/workers/stalled-jobs). */ @Env('QUEUE_WORKER_MAX_STALLED_COUNT') - maxStalledCount = 1; + maxStalledCount: number = 1; } @Config class BullConfig { /** Prefix for Bull keys on Redis. @example 'bull:jobs:23' */ @Env('QUEUE_BULL_PREFIX') - prefix = 'bull'; + prefix: string = 'bull'; @Nested redis: RedisConfig; /** How often (in seconds) to poll the Bull queue to identify executions finished during a Redis crash. `0` to disable. May increase Redis traffic significantly. */ @Env('QUEUE_RECOVERY_INTERVAL') - queueRecoveryInterval = 60; // watchdog interval + queueRecoveryInterval: number = 60; // watchdog interval /** @deprecated How long (in seconds) a worker must wait for active executions to finish before exiting. Use `N8N_GRACEFUL_SHUTDOWN_TIMEOUT` instead */ @Env('QUEUE_WORKER_TIMEOUT') - gracefulShutdownTimeout = 30; + gracefulShutdownTimeout: number = 30; @Nested settings: SettingsConfig; diff --git a/packages/@n8n/config/src/configs/templates.config.ts b/packages/@n8n/config/src/configs/templates.config.ts index 3b05048b36..0707330b32 100644 --- a/packages/@n8n/config/src/configs/templates.config.ts +++ b/packages/@n8n/config/src/configs/templates.config.ts @@ -4,9 +4,9 @@ import { Config, Env } from '../decorators'; export class TemplatesConfig { /** Whether to load workflow templates. */ @Env('N8N_TEMPLATES_ENABLED') - enabled = true; + enabled: boolean = true; /** Host to retrieve workflow templates from endpoints. */ @Env('N8N_TEMPLATES_HOST') - host = 'https://api.n8n.io/api/'; + host: string = 'https://api.n8n.io/api/'; } diff --git a/packages/@n8n/config/src/configs/user-management.config.ts b/packages/@n8n/config/src/configs/user-management.config.ts index 2c603a7148..956bac2b75 100644 --- a/packages/@n8n/config/src/configs/user-management.config.ts +++ b/packages/@n8n/config/src/configs/user-management.config.ts @@ -4,26 +4,26 @@ import { Config, Env, Nested } from '../decorators'; class SmtpAuth { /** SMTP login username */ @Env('N8N_SMTP_USER') - user = ''; + user: string = ''; /** SMTP login password */ @Env('N8N_SMTP_PASS') - pass = ''; + pass: string = ''; /** SMTP OAuth Service Client */ @Env('N8N_SMTP_OAUTH_SERVICE_CLIENT') - serviceClient = ''; + serviceClient: string = ''; /** SMTP OAuth Private Key */ @Env('N8N_SMTP_OAUTH_PRIVATE_KEY') - privateKey = ''; + privateKey: string = ''; } @Config class SmtpConfig { /** SMTP server host */ @Env('N8N_SMTP_HOST') - host = ''; + host: string = ''; /** SMTP server port */ @Env('N8N_SMTP_PORT') @@ -39,7 +39,7 @@ class SmtpConfig { /** How to display sender name */ @Env('N8N_SMTP_SENDER') - sender = ''; + sender: string = ''; @Nested auth: SmtpAuth; @@ -49,19 +49,19 @@ class SmtpConfig { export class TemplateConfig { /** Overrides default HTML template for inviting new people (use full path) */ @Env('N8N_UM_EMAIL_TEMPLATES_INVITE') - invite = ''; + invite: string = ''; /** Overrides default HTML template for resetting password (use full path) */ @Env('N8N_UM_EMAIL_TEMPLATES_PWRESET') - passwordReset = ''; + passwordReset: string = ''; /** Overrides default HTML template for notifying that a workflow was shared (use full path) */ @Env('N8N_UM_EMAIL_TEMPLATES_WORKFLOW_SHARED') - workflowShared = ''; + workflowShared: string = ''; /** Overrides default HTML template for notifying that credentials were shared (use full path) */ @Env('N8N_UM_EMAIL_TEMPLATES_CREDENTIALS_SHARED') - credentialsShared = ''; + credentialsShared: string = ''; } @Config diff --git a/packages/@n8n/config/src/configs/version-notifications.config.ts b/packages/@n8n/config/src/configs/version-notifications.config.ts index 5fe495ed6c..313e264a2d 100644 --- a/packages/@n8n/config/src/configs/version-notifications.config.ts +++ b/packages/@n8n/config/src/configs/version-notifications.config.ts @@ -4,13 +4,13 @@ import { Config, Env } from '../decorators'; export class VersionNotificationsConfig { /** Whether to request notifications about new n8n versions */ @Env('N8N_VERSION_NOTIFICATIONS_ENABLED') - enabled = true; + enabled: boolean = true; /** Endpoint to retrieve n8n version information from */ @Env('N8N_VERSION_NOTIFICATIONS_ENDPOINT') - endpoint = 'https://api.n8n.io/api/versions/'; + endpoint: string = 'https://api.n8n.io/api/versions/'; /** URL for versions panel to page instructing user on how to update n8n instance */ @Env('N8N_VERSION_NOTIFICATIONS_INFO_URL') - infoUrl = 'https://docs.n8n.io/hosting/installation/updating/'; + infoUrl: string = 'https://docs.n8n.io/hosting/installation/updating/'; } diff --git a/packages/@n8n/config/src/configs/workflows.config.ts b/packages/@n8n/config/src/configs/workflows.config.ts index 9ca004c886..3d6eaad12f 100644 --- a/packages/@n8n/config/src/configs/workflows.config.ts +++ b/packages/@n8n/config/src/configs/workflows.config.ts @@ -4,11 +4,11 @@ import { Config, Env } from '../decorators'; export class WorkflowsConfig { /** Default name for workflow */ @Env('WORKFLOWS_DEFAULT_NAME') - defaultName = 'My workflow'; + defaultName: string = 'My workflow'; /** Show onboarding flow in new workflow */ @Env('N8N_ONBOARDING_FLOW_DISABLED') - onboardingFlowDisabled = false; + onboardingFlowDisabled: boolean = false; /** Default option for which workflows may call the current workflow */ @Env('N8N_WORKFLOW_CALLER_POLICY_DEFAULT_OPTION') diff --git a/packages/@n8n/config/src/decorators.ts b/packages/@n8n/config/src/decorators.ts index fd68d44085..c5549a674f 100644 --- a/packages/@n8n/config/src/decorators.ts +++ b/packages/@n8n/config/src/decorators.ts @@ -47,6 +47,11 @@ export const Config: ClassDecorator = (ConfigClass: Class) => { } else { value = value === 'true'; } + } else if (type === Object) { + // eslint-disable-next-line n8n-local-rules/no-plain-errors + throw new Error( + `Invalid decorator metadata on key "${key as string}" on ${ConfigClass.name}\n Please use explicit typing on all config fields`, + ); } else if (type !== String && type !== Object) { value = new (type as Constructable)(value as string); } diff --git a/packages/@n8n/config/src/index.ts b/packages/@n8n/config/src/index.ts index a5b970eab4..fd1dc50c18 100644 --- a/packages/@n8n/config/src/index.ts +++ b/packages/@n8n/config/src/index.ts @@ -51,19 +51,19 @@ export class GlobalConfig { /** Path n8n is deployed to */ @Env('N8N_PATH') - path = '/'; + path: string = '/'; /** Host name n8n can be reached */ @Env('N8N_HOST') - host = 'localhost'; + host: string = 'localhost'; /** HTTP port n8n can be reached */ @Env('N8N_PORT') - port = 5678; + port: number = 5678; /** IP address n8n should listen on */ @Env('N8N_LISTEN_ADDRESS') - listen_address = '0.0.0.0'; + listen_address: string = '0.0.0.0'; /** HTTP Protocol via which n8n can be reached */ @Env('N8N_PROTOCOL') diff --git a/packages/@n8n/config/test/config.test.ts b/packages/@n8n/config/test/config.test.ts index b8e89d0ab7..adecff7f9d 100644 --- a/packages/@n8n/config/test/config.test.ts +++ b/packages/@n8n/config/test/config.test.ts @@ -232,6 +232,7 @@ describe('GlobalConfig', () => { DB_POSTGRESDB_USER: 'n8n', DB_TABLE_PREFIX: 'test_', NODES_INCLUDE: '["n8n-nodes-base.hackerNews"]', + DB_LOGGING_MAX_EXECUTION_TIME: '0', }; const config = Container.get(GlobalConfig); expect(config).toEqual({ diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/Agent.node.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/Agent.node.ts index b7bd13e870..62761742b0 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/Agent.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/Agent.node.ts @@ -9,7 +9,6 @@ import type { INodeTypeDescription, INodeProperties, } from 'n8n-workflow'; -import { getTemplateNoticeField } from '../../../utils/sharedFields'; import { promptTypeOptions, textInput } from '../../../utils/descriptions'; import { conversationalAgentProperties } from './agents/ConversationalAgent/description'; import { conversationalAgentExecute } from './agents/ConversationalAgent/execute'; @@ -83,6 +82,7 @@ function getInputs( filter: { nodes: [ '@n8n/n8n-nodes-langchain.lmChatAnthropic', + '@n8n/n8n-nodes-langchain.lmChatAwsBedrock', '@n8n/n8n-nodes-langchain.lmChatGroq', '@n8n/n8n-nodes-langchain.lmChatOllama', '@n8n/n8n-nodes-langchain.lmChatOpenAi', @@ -305,10 +305,14 @@ export class Agent implements INodeType { ], properties: [ { - ...getTemplateNoticeField(1954), + displayName: + 'Tip: Get a feel for agents with our quick tutorial or see an example of how this node works', + name: 'notice_tip', + type: 'notice', + default: '', displayOptions: { show: { - agent: ['conversationalAgent'], + agent: ['conversationalAgent', 'toolsAgent'], }, }, }, diff --git a/packages/@n8n/nodes-langchain/nodes/retrievers/RetrieverWorkflow/RetrieverWorkflow.node.ts b/packages/@n8n/nodes-langchain/nodes/retrievers/RetrieverWorkflow/RetrieverWorkflow.node.ts index 5b2d852178..6b446149fc 100644 --- a/packages/@n8n/nodes-langchain/nodes/retrievers/RetrieverWorkflow/RetrieverWorkflow.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/retrievers/RetrieverWorkflow/RetrieverWorkflow.node.ts @@ -9,6 +9,7 @@ import type { INodeType, INodeTypeDescription, SupplyData, + INodeParameterResourceLocator, } from 'n8n-workflow'; import { BaseRetriever, type BaseRetrieverInput } from '@langchain/core/retrievers'; @@ -41,7 +42,7 @@ export class RetrieverWorkflow implements INodeType { name: 'retrieverWorkflow', icon: 'fa:box-open', group: ['transform'], - version: 1, + version: [1, 1.1], description: 'Use an n8n Workflow as Retriever', defaults: { name: 'Workflow Retriever', @@ -105,12 +106,26 @@ export class RetrieverWorkflow implements INodeType { displayOptions: { show: { source: ['database'], + '@version': [{ _cnd: { eq: 1 } }], }, }, default: '', required: true, description: 'The workflow to execute', }, + { + displayName: 'Workflow', + name: 'workflowId', + type: 'workflowSelector', + displayOptions: { + show: { + source: ['database'], + '@version': [{ _cnd: { gte: 1.1 } }], + }, + }, + default: '', + required: true, + }, // ---------------------------------- // source:parameter @@ -301,11 +316,21 @@ export class RetrieverWorkflow implements INodeType { const workflowInfo: IExecuteWorkflowInfo = {}; if (source === 'database') { - // Read workflow from database - workflowInfo.id = this.executeFunctions.getNodeParameter( - 'workflowId', - itemIndex, - ) as string; + const nodeVersion = this.executeFunctions.getNode().typeVersion; + if (nodeVersion === 1) { + workflowInfo.id = this.executeFunctions.getNodeParameter( + 'workflowId', + itemIndex, + ) as string; + } else { + const { value } = this.executeFunctions.getNodeParameter( + 'workflowId', + itemIndex, + {}, + ) as INodeParameterResourceLocator; + workflowInfo.id = value as string; + } + baseMetadata.workflowId = workflowInfo.id; } else if (source === 'parameter') { // Read workflow from parameter diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/ToolWorkflow.node.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/ToolWorkflow.node.ts index 5ed96cbd60..0b00e17ac4 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/ToolWorkflow.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/ToolWorkflow.node.ts @@ -8,6 +8,7 @@ import type { SupplyData, ExecutionError, IDataObject, + INodeParameterResourceLocator, } from 'n8n-workflow'; import { NodeConnectionType, NodeOperationError, jsonParse } from 'n8n-workflow'; import type { SetField, SetNodeOptions } from 'n8n-nodes-base/dist/nodes/Set/v2/helpers/interfaces'; @@ -32,7 +33,7 @@ export class ToolWorkflow implements INodeType { name: 'toolWorkflow', icon: 'fa:network-wired', group: ['transform'], - version: [1, 1.1], + version: [1, 1.1, 1.2], description: 'Uses another n8n workflow as a tool. Allows packaging any n8n node(s) as a tool.', defaults: { name: 'Call n8n Workflow Tool', @@ -142,6 +143,7 @@ export class ToolWorkflow implements INodeType { displayOptions: { show: { source: ['database'], + '@version': [{ _cnd: { lte: 1.1 } }], }, }, default: '', @@ -150,6 +152,20 @@ export class ToolWorkflow implements INodeType { hint: 'Can be found in the URL of the workflow', }, + { + displayName: 'Workflow', + name: 'workflowId', + type: 'workflowSelector', + displayOptions: { + show: { + source: ['database'], + '@version': [{ _cnd: { gte: 1.2 } }], + }, + }, + default: '', + required: true, + }, + // ---------------------------------- // source:parameter // ---------------------------------- @@ -368,7 +384,17 @@ export class ToolWorkflow implements INodeType { const workflowInfo: IExecuteWorkflowInfo = {}; if (source === 'database') { // Read workflow from database - workflowInfo.id = this.getNodeParameter('workflowId', itemIndex) as string; + const nodeVersion = this.getNode().typeVersion; + if (nodeVersion <= 1.1) { + workflowInfo.id = this.getNodeParameter('workflowId', itemIndex) as string; + } else { + const { value } = this.getNodeParameter( + 'workflowId', + itemIndex, + {}, + ) as INodeParameterResourceLocator; + workflowInfo.id = value as string; + } } else if (source === 'parameter') { // Read workflow from parameter const workflowJson = this.getNodeParameter('workflowJson', itemIndex) as string; diff --git a/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/actions/assistant/message.operation.ts b/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/actions/assistant/message.operation.ts index 134e8a1167..3ec2e46eea 100644 --- a/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/actions/assistant/message.operation.ts +++ b/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/actions/assistant/message.operation.ts @@ -4,7 +4,12 @@ import { OpenAIAssistantRunnable } from 'langchain/experimental/openai_assistant import type { OpenAIToolType } from 'langchain/dist/experimental/openai_assistant/schema'; import { OpenAI as OpenAIClient } from 'openai'; -import { NodeConnectionType, NodeOperationError, updateDisplayOptions } from 'n8n-workflow'; +import { + ApplicationError, + NodeConnectionType, + NodeOperationError, + updateDisplayOptions, +} from 'n8n-workflow'; import type { IDataObject, IExecuteFunctions, @@ -228,25 +233,36 @@ export async function execute(this: IExecuteFunctions, i: number): Promise= 1.3 && + (assistantTools ?? [])?.length + ) { + await client.beta.assistants.update(assistantId, { + tools: assistantTools, + }); + } + filteredResponse = omit(response, ['signal', 'timeout']) as IDataObject; + } catch (error) { + if (!(error instanceof ApplicationError)) { + throw new NodeOperationError(this.getNode(), error.message, { itemIndex: i }); } } - if ( - options.preserveOriginalTools !== false && - nodeVersion >= 1.3 && - (assistantTools ?? [])?.length - ) { - await client.beta.assistants.update(assistantId, { - tools: assistantTools, - }); - } - const filteredResponse = omit(response, ['signal', 'timeout']); return [{ json: filteredResponse, pairedItem: { item: i } }]; } diff --git a/packages/@n8n/nodes-langchain/package.json b/packages/@n8n/nodes-langchain/package.json index acec252253..28303136c2 100644 --- a/packages/@n8n/nodes-langchain/package.json +++ b/packages/@n8n/nodes-langchain/package.json @@ -1,6 +1,6 @@ { "name": "@n8n/n8n-nodes-langchain", - "version": "1.55.0", + "version": "1.56.0", "description": "", "main": "index.js", "scripts": { diff --git a/packages/@n8n_io/eslint-config/local-rules.js b/packages/@n8n_io/eslint-config/local-rules.js index 09a9c00f0f..92b0f6669e 100644 --- a/packages/@n8n_io/eslint-config/local-rules.js +++ b/packages/@n8n_io/eslint-config/local-rules.js @@ -492,6 +492,29 @@ module.exports = { }; }, }, + + 'no-untyped-config-class-field': { + meta: { + type: 'problem', + docs: { + description: 'Enforce explicit typing of config class fields', + recommended: 'error', + }, + messages: { + noUntypedConfigClassField: + 'Class field must have an explicit type annotation, e.g. `field: type = value`. See: https://github.com/n8n-io/n8n/pull/10433', + }, + }, + create(context) { + return { + PropertyDefinition(node) { + if (!node.typeAnnotation) { + context.report({ node: node.key, messageId: 'noUntypedConfigClassField' }); + } + }, + }; + }, + }, }; const isJsonParseCall = (node) => diff --git a/packages/cli/package.json b/packages/cli/package.json index 54ca084465..c7608d1c34 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "1.55.0", + "version": "1.56.0", "description": "n8n Workflow Automation Tool", "main": "dist/index", "types": "dist/index.d.ts", @@ -93,7 +93,7 @@ "@n8n_io/ai-assistant-sdk": "1.9.4", "@n8n_io/license-sdk": "2.13.1", "@oclif/core": "4.0.7", - "@rudderstack/rudder-sdk-node": "2.0.7", + "@rudderstack/rudder-sdk-node": "2.0.9", "@sentry/integrations": "7.87.0", "@sentry/node": "7.87.0", "aws4": "1.11.0", @@ -171,6 +171,7 @@ "ws": "8.17.1", "xml2js": "catalog:", "xmllint-wasm": "3.0.1", + "xss": "^1.0.14", "yamljs": "0.3.0", "zod": "3.22.4" } diff --git a/packages/cli/scripts/build.mjs b/packages/cli/scripts/build.mjs index b1f5f5bd8f..36076ccfd4 100644 --- a/packages/cli/scripts/build.mjs +++ b/packages/cli/scripts/build.mjs @@ -23,8 +23,8 @@ if (publicApiEnabled) { function copyUserManagementEmailTemplates() { const templates = { - source: path.resolve(ROOT_DIR, 'src', 'UserManagement', 'email', 'templates'), - destination: path.resolve(ROOT_DIR, 'dist', 'UserManagement', 'email'), + source: path.resolve(ROOT_DIR, 'src', 'user-management', 'email', 'templates'), + destination: path.resolve(ROOT_DIR, 'dist', 'user-management', 'email'), }; shell.cp('-r', templates.source, templates.destination); diff --git a/packages/cli/src/GenericHelpers.ts b/packages/cli/src/GenericHelpers.ts deleted file mode 100644 index 300f24f9f9..0000000000 --- a/packages/cli/src/GenericHelpers.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { ValidationError, validate } from 'class-validator'; -import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; -import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; -import type { TagEntity } from '@db/entities/TagEntity'; -import type { User } from '@db/entities/User'; -import type { - UserRoleChangePayload, - UserSettingsUpdatePayload, - UserUpdatePayload, -} from '@/requests'; -import { BadRequestError } from './errors/response-errors/bad-request.error'; -import { NoXss } from './databases/utils/customValidators'; - -export async function validateEntity( - entity: - | WorkflowEntity - | CredentialsEntity - | TagEntity - | User - | UserUpdatePayload - | UserRoleChangePayload - | UserSettingsUpdatePayload, -): Promise { - const errors = await validate(entity); - - const errorMessages = errors - .reduce((acc, cur) => { - if (!cur.constraints) return acc; - acc.push(...Object.values(cur.constraints)); - return acc; - }, []) - .join(' | '); - - if (errorMessages) { - throw new BadRequestError(errorMessages); - } -} - -export const DEFAULT_EXECUTIONS_GET_ALL_LIMIT = 20; - -class StringWithNoXss { - @NoXss() - value: string; - - constructor(value: string) { - this.value = value; - } -} - -// Temporary solution until we implement payload validation middleware -export async function validateRecordNoXss(record: Record) { - const errors: ValidationError[] = []; - - for (const [key, value] of Object.entries(record)) { - const stringWithNoXss = new StringWithNoXss(value); - const validationErrors = await validate(stringWithNoXss); - - if (validationErrors.length > 0) { - const error = new ValidationError(); - error.property = key; - error.constraints = validationErrors[0].constraints; - errors.push(error); - } - } - - if (errors.length > 0) { - const errorMessages = errors - .map((error) => `${error.property}: ${Object.values(error.constraints ?? {}).join(', ')}`) - .join(' | '); - - throw new BadRequestError(errorMessages); - } -} diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index 73b1e99ec6..f6c3f4670f 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -25,7 +25,7 @@ import type { StartNodeData, } from 'n8n-workflow'; -import type { ActiveWorkflowManager } from '@/ActiveWorkflowManager'; +import type { ActiveWorkflowManager } from '@/active-workflow-manager'; import type { WorkflowExecute } from 'n8n-core'; @@ -39,7 +39,7 @@ import type { CredentialsRepository } from '@db/repositories/credentials.reposit import type { SettingsRepository } from '@db/repositories/settings.repository'; import type { UserRepository } from '@db/repositories/user.repository'; import type { WorkflowRepository } from '@db/repositories/workflow.repository'; -import type { ExternalHooks } from './ExternalHooks'; +import type { ExternalHooks } from './external-hooks'; import type { LICENSE_FEATURES, LICENSE_QUOTAS } from './constants'; import type { WorkflowWithSharingsAndCredentials } from './workflows/workflows.types'; import type { RunningJobSummary } from './scaling/types'; diff --git a/packages/cli/src/PublicApi/index.ts b/packages/cli/src/PublicApi/index.ts index af4fc97fc7..35c6d9862e 100644 --- a/packages/cli/src/PublicApi/index.ts +++ b/packages/cli/src/PublicApi/index.ts @@ -10,7 +10,7 @@ import type { HttpError } from 'express-openapi-validator/dist/framework/types'; import type { OpenAPIV3 } from 'openapi-types'; import type { JsonObject } from 'swagger-ui-express'; -import { License } from '@/License'; +import { License } from '@/license'; import { UserRepository } from '@db/repositories/user.repository'; import { UrlService } from '@/services/url.service'; import type { AuthenticatedRequest } from '@/requests'; diff --git a/packages/cli/src/PublicApi/v1/handlers/audit/audit.handler.ts b/packages/cli/src/PublicApi/v1/handlers/audit/audit.handler.ts index caf3750ad4..8aa09cb902 100644 --- a/packages/cli/src/PublicApi/v1/handlers/audit/audit.handler.ts +++ b/packages/cli/src/PublicApi/v1/handlers/audit/audit.handler.ts @@ -8,7 +8,7 @@ export = { globalScope('securityAudit:generate'), async (req: AuditRequest.Generate, res: Response): Promise => { try { - const { SecurityAuditService } = await import('@/security-audit/SecurityAudit.service'); + const { SecurityAuditService } = await import('@/security-audit/security-audit.service'); const result = await Container.get(SecurityAuditService).run( req.body?.additionalOptions?.categories, req.body?.additionalOptions?.daysAbandonedWorkflow, diff --git a/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.handler.ts b/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.handler.ts index 57b6bd66f6..2c13f6de56 100644 --- a/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.handler.ts +++ b/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.handler.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/no-unsafe-argument */ import type express from 'express'; -import { CredentialsHelper } from '@/CredentialsHelper'; -import { CredentialTypes } from '@/CredentialTypes'; +import { CredentialsHelper } from '@/credentials-helper'; +import { CredentialTypes } from '@/credential-types'; import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; import type { CredentialTypeRequest, CredentialRequest } from '../../../types'; import { projectScope } from '../../shared/middlewares/global.middleware'; diff --git a/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.middleware.ts b/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.middleware.ts index b7c2412923..8583c866b8 100644 --- a/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.middleware.ts +++ b/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.middleware.ts @@ -3,8 +3,8 @@ import type express from 'express'; import { validate } from 'jsonschema'; -import { CredentialsHelper } from '@/CredentialsHelper'; -import { CredentialTypes } from '@/CredentialTypes'; +import { CredentialsHelper } from '@/credentials-helper'; +import { CredentialTypes } from '@/credential-types'; import type { CredentialRequest } from '../../../types'; import { toJsonSchema } from './credentials.service'; import { Container } from 'typedi'; diff --git a/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.service.ts b/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.service.ts index 2c4a35a6aa..075c7fcf11 100644 --- a/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.service.ts +++ b/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.service.ts @@ -10,7 +10,7 @@ import type { ICredentialsDb } from '@/Interfaces'; import { CredentialsEntity } from '@db/entities/CredentialsEntity'; import { SharedCredentials } from '@db/entities/SharedCredentials'; import type { User } from '@db/entities/User'; -import { ExternalHooks } from '@/ExternalHooks'; +import { ExternalHooks } from '@/external-hooks'; import type { IDependency, IJsonSchema } from '../../../types'; import type { CredentialRequest } from '@/requests'; import { Container } from 'typedi'; diff --git a/packages/cli/src/PublicApi/v1/handlers/executions/executions.handler.ts b/packages/cli/src/PublicApi/v1/handlers/executions/executions.handler.ts index ab6927724c..beba4606aa 100644 --- a/packages/cli/src/PublicApi/v1/handlers/executions/executions.handler.ts +++ b/packages/cli/src/PublicApi/v1/handlers/executions/executions.handler.ts @@ -2,7 +2,7 @@ import type express from 'express'; import { Container } from 'typedi'; import { replaceCircularReferences } from 'n8n-workflow'; -import { ActiveExecutions } from '@/ActiveExecutions'; +import { ActiveExecutions } from '@/active-executions'; import { validCursor } from '../../shared/middlewares/global.middleware'; import type { ExecutionRequest } from '../../../types'; import { getSharedWorkflowIds } from '../workflows/workflows.service'; diff --git a/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts b/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts index 8515548dea..42c3e269ab 100644 --- a/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts +++ b/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts @@ -7,11 +7,11 @@ import type { FindOptionsWhere } from '@n8n/typeorm'; import { In, Like, QueryFailedError } from '@n8n/typeorm'; import { v4 as uuid } from 'uuid'; -import { ActiveWorkflowManager } from '@/ActiveWorkflowManager'; +import { ActiveWorkflowManager } from '@/active-workflow-manager'; import config from '@/config'; import { WorkflowEntity } from '@db/entities/WorkflowEntity'; -import { ExternalHooks } from '@/ExternalHooks'; -import { addNodeIds, replaceInvalidCredentials } from '@/WorkflowHelpers'; +import { ExternalHooks } from '@/external-hooks'; +import { addNodeIds, replaceInvalidCredentials } from '@/workflow-helpers'; import type { WorkflowRequest } from '../../../types'; import { projectScope, validCursor } from '../../shared/middlewares/global.middleware'; import { encodeNextCursor } from '../../shared/services/pagination.service'; @@ -26,7 +26,7 @@ import { updateTags, } from './workflows.service'; import { WorkflowService } from '@/workflows/workflow.service'; -import { WorkflowHistoryService } from '@/workflows/workflowHistory/workflowHistory.service.ee'; +import { WorkflowHistoryService } from '@/workflows/workflow-history/workflow-history.service.ee'; import { SharedWorkflowRepository } from '@/databases/repositories/sharedWorkflow.repository'; import { TagRepository } from '@/databases/repositories/tag.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; diff --git a/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.service.ts b/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.service.ts index 3e3afc078e..5fba88ff16 100644 --- a/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.service.ts +++ b/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.service.ts @@ -8,8 +8,8 @@ import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import type { Project } from '@/databases/entities/Project'; import { TagRepository } from '@db/repositories/tag.repository'; -import { License } from '@/License'; -import { WorkflowSharingService } from '@/workflows/workflowSharing.service'; +import { License } from '@/license'; +import { WorkflowSharingService } from '@/workflows/workflow-sharing.service'; import type { Scope } from '@n8n/permissions'; import config from '@/config'; diff --git a/packages/cli/src/PublicApi/v1/shared/middlewares/global.middleware.ts b/packages/cli/src/PublicApi/v1/shared/middlewares/global.middleware.ts index 7e8c39bb91..1b70b9770d 100644 --- a/packages/cli/src/PublicApi/v1/shared/middlewares/global.middleware.ts +++ b/packages/cli/src/PublicApi/v1/shared/middlewares/global.middleware.ts @@ -2,13 +2,13 @@ import type express from 'express'; import { Container } from 'typedi'; -import { License } from '@/License'; +import { License } from '@/license'; import type { AuthenticatedRequest } from '@/requests'; import type { PaginatedRequest } from '../../../types'; import { decodeCursor } from '../services/pagination.service'; import type { Scope } from '@n8n/permissions'; -import { userHasScope } from '@/permissions/checkAccess'; +import { userHasScope } from '@/permissions/check-access'; import type { BooleanLicenseFeature } from '@/Interfaces'; import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error'; diff --git a/packages/cli/src/UserManagement/email/index.ts b/packages/cli/src/UserManagement/email/index.ts deleted file mode 100644 index 8c94805eb6..0000000000 --- a/packages/cli/src/UserManagement/email/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { UserManagementMailer } from './UserManagementMailer'; - -export { UserManagementMailer }; diff --git a/packages/cli/src/__tests__/License.test.ts b/packages/cli/src/__tests__/License.test.ts index 31effecc0e..712340664f 100644 --- a/packages/cli/src/__tests__/License.test.ts +++ b/packages/cli/src/__tests__/License.test.ts @@ -2,8 +2,8 @@ import { LicenseManager } from '@n8n_io/license-sdk'; import { InstanceSettings } from 'n8n-core'; import { mock } from 'jest-mock-extended'; import config from '@/config'; -import { License } from '@/License'; -import { Logger } from '@/Logger'; +import { License } from '@/license'; +import { Logger } from '@/logger'; import { N8N_VERSION } from '@/constants'; import { mockInstance } from '@test/mocking'; import { OrchestrationService } from '@/services/orchestration.service'; diff --git a/packages/cli/src/__tests__/ActiveExecutions.test.ts b/packages/cli/src/__tests__/active-executions.test.ts similarity index 99% rename from packages/cli/src/__tests__/ActiveExecutions.test.ts rename to packages/cli/src/__tests__/active-executions.test.ts index 0cc70a10a4..539c160700 100644 --- a/packages/cli/src/__tests__/ActiveExecutions.test.ts +++ b/packages/cli/src/__tests__/active-executions.test.ts @@ -1,4 +1,4 @@ -import { ActiveExecutions } from '@/ActiveExecutions'; +import { ActiveExecutions } from '@/active-executions'; import PCancelable from 'p-cancelable'; import { v4 as uuid } from 'uuid'; import type { IExecuteResponsePromiseData, IRun } from 'n8n-workflow'; diff --git a/packages/cli/src/__tests__/CredentialTypes.test.ts b/packages/cli/src/__tests__/credential-types.test.ts similarity index 89% rename from packages/cli/src/__tests__/CredentialTypes.test.ts rename to packages/cli/src/__tests__/credential-types.test.ts index 2a8e6a939a..b65e5a4aa3 100644 --- a/packages/cli/src/__tests__/CredentialTypes.test.ts +++ b/packages/cli/src/__tests__/credential-types.test.ts @@ -1,6 +1,6 @@ -import { CredentialTypes } from '@/CredentialTypes'; +import { CredentialTypes } from '@/credential-types'; import { Container } from 'typedi'; -import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; +import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; import { mockInstance } from '@test/mocking'; describe('CredentialTypes', () => { diff --git a/packages/cli/src/__tests__/CredentialsHelper.test.ts b/packages/cli/src/__tests__/credentials-helper.test.ts similarity index 97% rename from packages/cli/src/__tests__/CredentialsHelper.test.ts rename to packages/cli/src/__tests__/credentials-helper.test.ts index 88cd19ad3d..61c3f93013 100644 --- a/packages/cli/src/__tests__/CredentialsHelper.test.ts +++ b/packages/cli/src/__tests__/credentials-helper.test.ts @@ -9,9 +9,9 @@ import type { } from 'n8n-workflow'; import { deepCopy } from 'n8n-workflow'; import { Workflow } from 'n8n-workflow'; -import { CredentialsHelper } from '@/CredentialsHelper'; -import { NodeTypes } from '@/NodeTypes'; -import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; +import { CredentialsHelper } from '@/credentials-helper'; +import { NodeTypes } from '@/node-types'; +import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; import { CredentialsRepository } from '@db/repositories/credentials.repository'; import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; import { mockInstance } from '@test/mocking'; diff --git a/packages/cli/src/__tests__/WaitTracker.test.ts b/packages/cli/src/__tests__/wait-tracker.test.ts similarity index 98% rename from packages/cli/src/__tests__/WaitTracker.test.ts rename to packages/cli/src/__tests__/wait-tracker.test.ts index fb51d2e25b..ee0697e110 100644 --- a/packages/cli/src/__tests__/WaitTracker.test.ts +++ b/packages/cli/src/__tests__/wait-tracker.test.ts @@ -1,4 +1,4 @@ -import { WaitTracker } from '@/WaitTracker'; +import { WaitTracker } from '@/wait-tracker'; import { mock } from 'jest-mock-extended'; import type { ExecutionRepository } from '@/databases/repositories/execution.repository'; import type { IExecutionResponse } from '@/Interfaces'; diff --git a/packages/cli/src/__tests__/WorkflowExecuteAdditionalData.test.ts b/packages/cli/src/__tests__/workflow-execute-additional-data.test.ts similarity index 88% rename from packages/cli/src/__tests__/WorkflowExecuteAdditionalData.test.ts rename to packages/cli/src/__tests__/workflow-execute-additional-data.test.ts index eca60e56c5..f51d994493 100644 --- a/packages/cli/src/__tests__/WorkflowExecuteAdditionalData.test.ts +++ b/packages/cli/src/__tests__/workflow-execute-additional-data.test.ts @@ -1,10 +1,10 @@ import { VariablesService } from '@/environments/variables/variables.service.ee'; import { mockInstance } from '@test/mocking'; import { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus'; -import { getBase } from '@/WorkflowExecuteAdditionalData'; +import { getBase } from '@/workflow-execute-additional-data'; import Container from 'typedi'; -import { CredentialsHelper } from '@/CredentialsHelper'; -import { SecretsHelper } from '@/SecretsHelpers'; +import { CredentialsHelper } from '@/credentials-helper'; +import { SecretsHelper } from '@/secrets-helpers'; describe('WorkflowExecuteAdditionalData', () => { const messageEventBus = mockInstance(MessageEventBus); diff --git a/packages/cli/src/__tests__/WorkflowHelpers.test.ts b/packages/cli/src/__tests__/workflow-helpers.test.ts similarity index 95% rename from packages/cli/src/__tests__/WorkflowHelpers.test.ts rename to packages/cli/src/__tests__/workflow-helpers.test.ts index 1b5da0a4de..32f4933349 100644 --- a/packages/cli/src/__tests__/WorkflowHelpers.test.ts +++ b/packages/cli/src/__tests__/workflow-helpers.test.ts @@ -1,5 +1,5 @@ import { type Workflow } from 'n8n-workflow'; -import { getExecutionStartNode } from '@/WorkflowHelpers'; +import { getExecutionStartNode } from '@/workflow-helpers'; import type { IWorkflowExecutionDataProcess } from '@/Interfaces'; describe('WorkflowHelpers', () => { diff --git a/packages/cli/src/__tests__/WorkflowRunner.test.ts b/packages/cli/src/__tests__/workflow-runner.test.ts similarity index 97% rename from packages/cli/src/__tests__/WorkflowRunner.test.ts rename to packages/cli/src/__tests__/workflow-runner.test.ts index 668150092f..56dcf97c7c 100644 --- a/packages/cli/src/__tests__/WorkflowRunner.test.ts +++ b/packages/cli/src/__tests__/workflow-runner.test.ts @@ -1,7 +1,7 @@ import Container from 'typedi'; import { WorkflowHooks, type ExecutionError, type IWorkflowExecuteHooks } from 'n8n-workflow'; import type { User } from '@db/entities/User'; -import { WorkflowRunner } from '@/WorkflowRunner'; +import { WorkflowRunner } from '@/workflow-runner'; import config from '@/config'; import * as testDb from '@test-integration/testDb'; diff --git a/packages/cli/src/AbstractServer.ts b/packages/cli/src/abstract-server.ts similarity index 93% rename from packages/cli/src/AbstractServer.ts rename to packages/cli/src/abstract-server.ts index 93ecb500c6..f9080080d0 100644 --- a/packages/cli/src/AbstractServer.ts +++ b/packages/cli/src/abstract-server.ts @@ -10,18 +10,18 @@ import config from '@/config'; import { N8N_VERSION, TEMPLATES_DIR, inDevelopment, inTest } from '@/constants'; import * as Db from '@/Db'; import { N8nInstanceType } from '@/Interfaces'; -import { ExternalHooks } from '@/ExternalHooks'; -import { send, sendErrorResponse } from '@/ResponseHelper'; +import { ExternalHooks } from '@/external-hooks'; +import { send, sendErrorResponse } from '@/response-helper'; import { rawBodyReader, bodyParser, corsMiddleware } from '@/middlewares'; -import { WaitingForms } from '@/WaitingForms'; -import { TestWebhooks } from '@/webhooks/TestWebhooks'; -import { WaitingWebhooks } from '@/webhooks/WaitingWebhooks'; -import { createWebhookHandlerFor } from '@/webhooks/WebhookRequestHandler'; -import { LiveWebhooks } from '@/webhooks/LiveWebhooks'; +import { WaitingForms } from '@/waiting-forms'; +import { TestWebhooks } from '@/webhooks/test-webhooks'; +import { WaitingWebhooks } from '@/webhooks/waiting-webhooks'; +import { createWebhookHandlerFor } from '@/webhooks/webhook-request-handler'; +import { LiveWebhooks } from '@/webhooks/live-webhooks'; import { generateHostInstanceId } from './databases/utils/generators'; -import { Logger } from '@/Logger'; +import { Logger } from '@/logger'; import { ServiceUnavailableError } from './errors/response-errors/service-unavailable.error'; -import { OnShutdown } from '@/decorators/OnShutdown'; +import { OnShutdown } from '@/decorators/on-shutdown'; import { GlobalConfig } from '@n8n/config'; @Service() diff --git a/packages/cli/src/ActivationErrors.service.ts b/packages/cli/src/activation-errors.service.ts similarity index 100% rename from packages/cli/src/ActivationErrors.service.ts rename to packages/cli/src/activation-errors.service.ts diff --git a/packages/cli/src/ActiveExecutions.ts b/packages/cli/src/active-executions.ts similarity index 99% rename from packages/cli/src/ActiveExecutions.ts rename to packages/cli/src/active-executions.ts index c1a6e8ffd6..32f8aac0a7 100644 --- a/packages/cli/src/ActiveExecutions.ts +++ b/packages/cli/src/active-executions.ts @@ -23,7 +23,7 @@ import type { } from '@/Interfaces'; import { isWorkflowIdValid } from '@/utils'; import { ExecutionRepository } from '@db/repositories/execution.repository'; -import { Logger } from '@/Logger'; +import { Logger } from '@/logger'; import { ConcurrencyControlService } from './concurrency/concurrency-control.service'; import config from './config'; diff --git a/packages/cli/src/ActiveWorkflowManager.ts b/packages/cli/src/active-workflow-manager.ts similarity index 97% rename from packages/cli/src/ActiveWorkflowManager.ts rename to packages/cli/src/active-workflow-manager.ts index fac6e9d9fa..0db162a06e 100644 --- a/packages/cli/src/ActiveWorkflowManager.ts +++ b/packages/cli/src/active-workflow-manager.ts @@ -27,28 +27,28 @@ import { } from 'n8n-workflow'; import type { IWorkflowDb } from '@/Interfaces'; -import * as WebhookHelpers from '@/webhooks/WebhookHelpers'; -import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'; +import * as WebhookHelpers from '@/webhooks/webhook-helpers'; +import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; -import { ActiveExecutions } from '@/ActiveExecutions'; +import { ActiveExecutions } from '@/active-executions'; import { ExecutionService } from './executions/execution.service'; import { STARTING_NODES, WORKFLOW_REACTIVATE_INITIAL_TIMEOUT, WORKFLOW_REACTIVATE_MAX_TIMEOUT, } from '@/constants'; -import { NodeTypes } from '@/NodeTypes'; -import { ExternalHooks } from '@/ExternalHooks'; +import { NodeTypes } from '@/node-types'; +import { ExternalHooks } from '@/external-hooks'; import { WebhookService } from '@/webhooks/webhook.service'; -import { Logger } from './Logger'; +import { Logger } from './logger'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { OrchestrationService } from '@/services/orchestration.service'; -import { ActivationErrorsService } from '@/ActivationErrors.service'; +import { ActivationErrorsService } from '@/activation-errors.service'; import { ActiveWorkflowsService } from '@/services/activeWorkflows.service'; -import { WorkflowExecutionService } from '@/workflows/workflowExecution.service'; -import { WorkflowStaticDataService } from '@/workflows/workflowStaticData.service'; -import { OnShutdown } from '@/decorators/OnShutdown'; +import { WorkflowExecutionService } from '@/workflows/workflow-execution.service'; +import { WorkflowStaticDataService } from '@/workflows/workflow-static-data.service'; +import { OnShutdown } from '@/decorators/on-shutdown'; interface QueuedActivation { activationMode: WorkflowActivateMode; diff --git a/packages/cli/src/auth/__tests__/auth.service.test.ts b/packages/cli/src/auth/__tests__/auth.service.test.ts index 60fdd12126..82c820ac9e 100644 --- a/packages/cli/src/auth/__tests__/auth.service.test.ts +++ b/packages/cli/src/auth/__tests__/auth.service.test.ts @@ -6,6 +6,7 @@ import { AuthService } from '@/auth/auth.service'; import config from '@/config'; import { AUTH_COOKIE_NAME, Time } from '@/constants'; import type { User } from '@db/entities/User'; +import type { InvalidAuthTokenRepository } from '@db/repositories/invalidAuthToken.repository'; import type { UserRepository } from '@db/repositories/user.repository'; import { JwtService } from '@/services/jwt.service'; import type { UrlService } from '@/services/url.service'; @@ -26,7 +27,15 @@ describe('AuthService', () => { const jwtService = new JwtService(mock()); const urlService = mock(); const userRepository = mock(); - const authService = new AuthService(mock(), mock(), jwtService, urlService, userRepository); + const invalidAuthTokenRepository = mock(); + const authService = new AuthService( + mock(), + mock(), + jwtService, + urlService, + userRepository, + invalidAuthTokenRepository, + ); const now = new Date('2024-02-01T01:23:45.678Z'); jest.useFakeTimers({ now }); @@ -70,16 +79,36 @@ describe('AuthService', () => { it('should 401 if no cookie is set', async () => { req.cookies[AUTH_COOKIE_NAME] = undefined; + await authService.authMiddleware(req, res, next); + + expect(invalidAuthTokenRepository.existsBy).not.toHaveBeenCalled(); expect(next).not.toHaveBeenCalled(); expect(res.status).toHaveBeenCalledWith(401); }); it('should 401 and clear the cookie if the JWT is expired', async () => { req.cookies[AUTH_COOKIE_NAME] = validToken; + invalidAuthTokenRepository.existsBy.mockResolvedValue(false); jest.advanceTimersByTime(365 * Time.days.toMilliseconds); await authService.authMiddleware(req, res, next); + + expect(invalidAuthTokenRepository.existsBy).toHaveBeenCalled(); + expect(userRepository.findOne).not.toHaveBeenCalled(); + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(401); + expect(res.clearCookie).toHaveBeenCalledWith(AUTH_COOKIE_NAME); + }); + + it('should 401 and clear the cookie if the JWT has been invalidated', async () => { + req.cookies[AUTH_COOKIE_NAME] = validToken; + invalidAuthTokenRepository.existsBy.mockResolvedValue(true); + + await authService.authMiddleware(req, res, next); + + expect(invalidAuthTokenRepository.existsBy).toHaveBeenCalled(); + expect(userRepository.findOne).not.toHaveBeenCalled(); expect(next).not.toHaveBeenCalled(); expect(res.status).toHaveBeenCalledWith(401); expect(res.clearCookie).toHaveBeenCalledWith(AUTH_COOKIE_NAME); @@ -88,9 +117,11 @@ describe('AuthService', () => { it('should refresh the cookie before it expires', async () => { req.cookies[AUTH_COOKIE_NAME] = validToken; jest.advanceTimersByTime(6 * Time.days.toMilliseconds); + invalidAuthTokenRepository.existsBy.mockResolvedValue(false); userRepository.findOne.mockResolvedValue(user); await authService.authMiddleware(req, res, next); + expect(next).toHaveBeenCalled(); expect(res.cookie).toHaveBeenCalledWith('n8n-auth', expect.any(String), { httpOnly: true, @@ -302,4 +333,21 @@ describe('AuthService', () => { expect(resolvedUser).toEqual(user); }); }); + + describe('invalidateToken', () => { + const req = mock({ + cookies: { + [AUTH_COOKIE_NAME]: validToken, + }, + }); + + it('should invalidate the token', async () => { + await authService.invalidateToken(req); + + expect(invalidAuthTokenRepository.insert).toHaveBeenCalledWith({ + token: validToken, + expiresAt: new Date('2024-02-08T01:23:45.000Z'), + }); + }); + }); }); diff --git a/packages/cli/src/auth/auth.service.ts b/packages/cli/src/auth/auth.service.ts index e2487e6f6f..7e83393f27 100644 --- a/packages/cli/src/auth/auth.service.ts +++ b/packages/cli/src/auth/auth.service.ts @@ -6,11 +6,12 @@ import { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken'; import config from '@/config'; import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES, Time } from '@/constants'; import type { User } from '@db/entities/User'; +import { InvalidAuthTokenRepository } from '@db/repositories/invalidAuthToken.repository'; import { UserRepository } from '@db/repositories/user.repository'; import { AuthError } from '@/errors/response-errors/auth.error'; import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; -import { License } from '@/License'; -import { Logger } from '@/Logger'; +import { License } from '@/license'; +import { Logger } from '@/logger'; import type { AuthenticatedRequest } from '@/requests'; import { JwtService } from '@/services/jwt.service'; import { UrlService } from '@/services/url.service'; @@ -53,6 +54,7 @@ export class AuthService { private readonly jwtService: JwtService, private readonly urlService: UrlService, private readonly userRepository: UserRepository, + private readonly invalidAuthTokenRepository: InvalidAuthTokenRepository, ) { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment this.authMiddleware = this.authMiddleware.bind(this); @@ -62,6 +64,8 @@ export class AuthService { const token = req.cookies[AUTH_COOKIE_NAME]; if (token) { try { + const isInvalid = await this.invalidAuthTokenRepository.existsBy({ token }); + if (isInvalid) throw new AuthError('Unauthorized'); req.user = await this.resolveJwt(token, req, res); } catch (error) { if (error instanceof JsonWebTokenError || error instanceof AuthError) { @@ -80,6 +84,22 @@ export class AuthService { res.clearCookie(AUTH_COOKIE_NAME); } + async invalidateToken(req: AuthenticatedRequest) { + const token = req.cookies[AUTH_COOKIE_NAME]; + if (!token) return; + try { + const { exp } = this.jwtService.decode(token); + if (exp) { + await this.invalidAuthTokenRepository.insert({ + token, + expiresAt: new Date(exp * 1000), + }); + } + } catch (e) { + this.logger.warn('failed to invalidate auth token', { error: (e as Error).message }); + } + } + issueCookie(res: Response, user: User, browserId?: string) { // TODO: move this check to the login endpoint in AuthController // If the instance has exceeded its user quota, prevent non-owners from logging in diff --git a/packages/cli/src/auth/methods/email.ts b/packages/cli/src/auth/methods/email.ts index f954991974..fb7213ef3b 100644 --- a/packages/cli/src/auth/methods/email.ts +++ b/packages/cli/src/auth/methods/email.ts @@ -1,7 +1,7 @@ import type { User } from '@db/entities/User'; import { PasswordUtility } from '@/services/password.utility'; import { Container } from 'typedi'; -import { isLdapLoginEnabled } from '@/Ldap/helpers.ee'; +import { isLdapLoginEnabled } from '@/ldap/helpers.ee'; import { UserRepository } from '@db/repositories/user.repository'; import { AuthError } from '@/errors/response-errors/auth.error'; import { EventService } from '@/events/event.service'; diff --git a/packages/cli/src/auth/methods/ldap.ts b/packages/cli/src/auth/methods/ldap.ts index 66f6f6dcd9..c3ec2150c7 100644 --- a/packages/cli/src/auth/methods/ldap.ts +++ b/packages/cli/src/auth/methods/ldap.ts @@ -1,6 +1,6 @@ import { Container } from 'typedi'; -import { LdapService } from '@/Ldap/ldap.service.ee'; +import { LdapService } from '@/ldap/ldap.service.ee'; import { createLdapUserOnLocalDb, getUserByEmail, @@ -9,7 +9,7 @@ import { mapLdapAttributesToUser, createLdapAuthIdentity, updateLdapUserOnLocalDb, -} from '@/Ldap/helpers.ee'; +} from '@/ldap/helpers.ee'; import type { User } from '@db/entities/User'; import { EventService } from '@/events/event.service'; diff --git a/packages/cli/src/commands/audit.ts b/packages/cli/src/commands/audit.ts index 4f6b356028..a447204b67 100644 --- a/packages/cli/src/commands/audit.ts +++ b/packages/cli/src/commands/audit.ts @@ -2,11 +2,11 @@ import { Container } from 'typedi'; import { Flags } from '@oclif/core'; import { ApplicationError } from 'n8n-workflow'; -import { SecurityAuditService } from '@/security-audit/SecurityAudit.service'; +import { SecurityAuditService } from '@/security-audit/security-audit.service'; import { RISK_CATEGORIES } from '@/security-audit/constants'; import config from '@/config'; import type { Risk } from '@/security-audit/types'; -import { BaseCommand } from './BaseCommand'; +import { BaseCommand } from './base-command'; export class SecurityAudit extends BaseCommand { static description = 'Generate a security audit report for this n8n instance'; diff --git a/packages/cli/src/commands/BaseCommand.ts b/packages/cli/src/commands/base-command.ts similarity index 93% rename from packages/cli/src/commands/BaseCommand.ts rename to packages/cli/src/commands/base-command.ts index af3958c25a..db6897c252 100644 --- a/packages/cli/src/commands/BaseCommand.ts +++ b/packages/cli/src/commands/base-command.ts @@ -4,25 +4,25 @@ import { Command, Errors } from '@oclif/core'; import { GlobalConfig } from '@n8n/config'; import { ApplicationError, ErrorReporterProxy as ErrorReporter, sleep } from 'n8n-workflow'; import { BinaryDataService, InstanceSettings, ObjectStoreService } from 'n8n-core'; -import type { AbstractServer } from '@/AbstractServer'; -import { Logger } from '@/Logger'; +import type { AbstractServer } from '@/abstract-server'; +import { Logger } from '@/logger'; import config from '@/config'; import * as Db from '@/Db'; -import * as CrashJournal from '@/CrashJournal'; +import * as CrashJournal from '@/crash-journal'; import { LICENSE_FEATURES, inDevelopment, inTest } from '@/constants'; -import { initErrorHandling } from '@/ErrorReporting'; -import { ExternalHooks } from '@/ExternalHooks'; -import { NodeTypes } from '@/NodeTypes'; -import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; +import { initErrorHandling } from '@/error-reporting'; +import { ExternalHooks } from '@/external-hooks'; +import { NodeTypes } from '@/node-types'; +import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; import type { N8nInstanceType } from '@/Interfaces'; import { PostHogClient } from '@/posthog'; -import { InternalHooks } from '@/InternalHooks'; -import { License } from '@/License'; -import { ExternalSecretsManager } from '@/ExternalSecrets/ExternalSecretsManager.ee'; -import { initExpressionEvaluator } from '@/ExpressionEvaluator'; +import { InternalHooks } from '@/internal-hooks'; +import { License } from '@/license'; +import { ExternalSecretsManager } from '@/external-secrets/external-secrets-manager.ee'; +import { initExpressionEvaluator } from '@/expression-evaluator'; import { generateHostInstanceId } from '@db/utils/generators'; -import { WorkflowHistoryManager } from '@/workflows/workflowHistory/workflowHistoryManager.ee'; -import { ShutdownService } from '@/shutdown/Shutdown.service'; +import { WorkflowHistoryManager } from '@/workflows/workflow-history/workflow-history-manager.ee'; +import { ShutdownService } from '@/shutdown/shutdown.service'; import { TelemetryEventRelay } from '@/events/telemetry-event-relay'; export abstract class BaseCommand extends Command { diff --git a/packages/cli/src/commands/db/__tests__/revert.test.ts b/packages/cli/src/commands/db/__tests__/revert.test.ts index 13c554a786..1afa8be190 100644 --- a/packages/cli/src/commands/db/__tests__/revert.test.ts +++ b/packages/cli/src/commands/db/__tests__/revert.test.ts @@ -1,6 +1,6 @@ import { main } from '@/commands/db/revert'; import { mockInstance } from '@test/mocking'; -import { Logger } from '@/Logger'; +import { Logger } from '@/logger'; import type { IrreversibleMigration, ReversibleMigration } from '@/databases/types'; import type { Migration, MigrationExecutor } from '@n8n/typeorm'; import { type DataSource } from '@n8n/typeorm'; diff --git a/packages/cli/src/commands/db/revert.ts b/packages/cli/src/commands/db/revert.ts index b0a844086c..c52ce8a35c 100644 --- a/packages/cli/src/commands/db/revert.ts +++ b/packages/cli/src/commands/db/revert.ts @@ -4,7 +4,7 @@ import type { DataSourceOptions as ConnectionOptions } from '@n8n/typeorm'; // eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import import { MigrationExecutor, DataSource as Connection } from '@n8n/typeorm'; import { Container } from 'typedi'; -import { Logger } from '@/Logger'; +import { Logger } from '@/logger'; import { getConnectionOptions } from '@db/config'; import type { Migration } from '@db/types'; import { wrapMigration } from '@db/utils/migrationHelpers'; diff --git a/packages/cli/src/commands/execute.ts b/packages/cli/src/commands/execute.ts index cdf949e87c..0788f4d1db 100644 --- a/packages/cli/src/commands/execute.ts +++ b/packages/cli/src/commands/execute.ts @@ -3,11 +3,11 @@ import { Flags } from '@oclif/core'; import type { IWorkflowBase } from 'n8n-workflow'; import { ApplicationError, ExecutionBaseError } from 'n8n-workflow'; -import { ActiveExecutions } from '@/ActiveExecutions'; -import { WorkflowRunner } from '@/WorkflowRunner'; +import { ActiveExecutions } from '@/active-executions'; +import { WorkflowRunner } from '@/workflow-runner'; import type { IWorkflowExecutionDataProcess } from '@/Interfaces'; import { findCliWorkflowStart, isWorkflowIdValid } from '@/utils'; -import { BaseCommand } from './BaseCommand'; +import { BaseCommand } from './base-command'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { OwnershipService } from '@/services/ownership.service'; diff --git a/packages/cli/src/commands/executeBatch.ts b/packages/cli/src/commands/executeBatch.ts index 227dd962ef..bd2de6b5ed 100644 --- a/packages/cli/src/commands/executeBatch.ts +++ b/packages/cli/src/commands/executeBatch.ts @@ -9,15 +9,15 @@ import { sep } from 'path'; import { diff } from 'json-diff'; import pick from 'lodash/pick'; -import { ActiveExecutions } from '@/ActiveExecutions'; -import { WorkflowRunner } from '@/WorkflowRunner'; +import { ActiveExecutions } from '@/active-executions'; +import { WorkflowRunner } from '@/workflow-runner'; import type { IWorkflowDb, IWorkflowExecutionDataProcess } from '@/Interfaces'; import type { User } from '@db/entities/User'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { OwnershipService } from '@/services/ownership.service'; import { findCliWorkflowStart } from '@/utils'; -import { BaseCommand } from './BaseCommand'; +import { BaseCommand } from './base-command'; import type { IExecutionResult, INodeSpecialCase, diff --git a/packages/cli/src/commands/export/credentials.ts b/packages/cli/src/commands/export/credentials.ts index 5fb10dcb2c..4af626c735 100644 --- a/packages/cli/src/commands/export/credentials.ts +++ b/packages/cli/src/commands/export/credentials.ts @@ -3,7 +3,7 @@ import fs from 'fs'; import path from 'path'; import { Credentials } from 'n8n-core'; import type { ICredentialsDb, ICredentialsDecryptedDb } from '@/Interfaces'; -import { BaseCommand } from '../BaseCommand'; +import { BaseCommand } from '../base-command'; import { CredentialsRepository } from '@db/repositories/credentials.repository'; import Container from 'typedi'; import { ApplicationError } from 'n8n-workflow'; diff --git a/packages/cli/src/commands/export/workflow.ts b/packages/cli/src/commands/export/workflow.ts index 19aa2b9e08..b15484c769 100644 --- a/packages/cli/src/commands/export/workflow.ts +++ b/packages/cli/src/commands/export/workflow.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core'; import fs from 'fs'; import path from 'path'; -import { BaseCommand } from '../BaseCommand'; +import { BaseCommand } from '../base-command'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; import Container from 'typedi'; import { ApplicationError } from 'n8n-workflow'; diff --git a/packages/cli/src/commands/import/credentials.ts b/packages/cli/src/commands/import/credentials.ts index 16fd1a5fa5..f36d33ba7b 100644 --- a/packages/cli/src/commands/import/credentials.ts +++ b/packages/cli/src/commands/import/credentials.ts @@ -9,7 +9,7 @@ import type { EntityManager } from '@n8n/typeorm'; import * as Db from '@/Db'; import { SharedCredentials } from '@db/entities/SharedCredentials'; import { CredentialsEntity } from '@db/entities/CredentialsEntity'; -import { BaseCommand } from '../BaseCommand'; +import { BaseCommand } from '../base-command'; import type { ICredentialsEncrypted } from 'n8n-workflow'; import { ApplicationError, jsonParse } from 'n8n-workflow'; import { UM_FIX_INSTRUCTION } from '@/constants'; diff --git a/packages/cli/src/commands/import/workflow.ts b/packages/cli/src/commands/import/workflow.ts index dcf72a9c9a..d7173b591a 100644 --- a/packages/cli/src/commands/import/workflow.ts +++ b/packages/cli/src/commands/import/workflow.ts @@ -11,7 +11,7 @@ import { UserRepository } from '@db/repositories/user.repository'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; import type { IWorkflowToImport } from '@/Interfaces'; import { ImportService } from '@/services/import.service'; -import { BaseCommand } from '../BaseCommand'; +import { BaseCommand } from '../base-command'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { ProjectRepository } from '@/databases/repositories/project.repository'; diff --git a/packages/cli/src/commands/ldap/reset.ts b/packages/cli/src/commands/ldap/reset.ts index 2f0cc7b305..c91564ead9 100644 --- a/packages/cli/src/commands/ldap/reset.ts +++ b/packages/cli/src/commands/ldap/reset.ts @@ -1,10 +1,10 @@ import Container from 'typedi'; -import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/Ldap/constants'; +import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/ldap/constants'; import { AuthIdentityRepository } from '@db/repositories/authIdentity.repository'; import { AuthProviderSyncHistoryRepository } from '@db/repositories/authProviderSyncHistory.repository'; import { SettingsRepository } from '@db/repositories/settings.repository'; import { UserRepository } from '@db/repositories/user.repository'; -import { BaseCommand } from '../BaseCommand'; +import { BaseCommand } from '../base-command'; import { Flags } from '@oclif/core'; import { ApplicationError } from 'n8n-workflow'; import { ProjectRepository } from '@/databases/repositories/project.repository'; diff --git a/packages/cli/src/commands/license/clear.ts b/packages/cli/src/commands/license/clear.ts index 808784bdfd..8439417471 100644 --- a/packages/cli/src/commands/license/clear.ts +++ b/packages/cli/src/commands/license/clear.ts @@ -1,8 +1,8 @@ import { Container } from 'typedi'; import { SETTINGS_LICENSE_CERT_KEY } from '@/constants'; -import { BaseCommand } from '../BaseCommand'; +import { BaseCommand } from '../base-command'; import { SettingsRepository } from '@db/repositories/settings.repository'; -import { License } from '@/License'; +import { License } from '@/license'; export class ClearLicenseCommand extends BaseCommand { static description = 'Clear license'; diff --git a/packages/cli/src/commands/license/info.ts b/packages/cli/src/commands/license/info.ts index a4e9e42cf2..6789b4ff41 100644 --- a/packages/cli/src/commands/license/info.ts +++ b/packages/cli/src/commands/license/info.ts @@ -1,6 +1,6 @@ import { Container } from 'typedi'; -import { License } from '@/License'; -import { BaseCommand } from '../BaseCommand'; +import { License } from '@/license'; +import { BaseCommand } from '../base-command'; export class LicenseInfoCommand extends BaseCommand { static description = 'Print license information'; diff --git a/packages/cli/src/commands/list/workflow.ts b/packages/cli/src/commands/list/workflow.ts index 2d33de19e3..35fd073f35 100644 --- a/packages/cli/src/commands/list/workflow.ts +++ b/packages/cli/src/commands/list/workflow.ts @@ -1,7 +1,7 @@ import Container from 'typedi'; import { Flags } from '@oclif/core'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; -import { BaseCommand } from '../BaseCommand'; +import { BaseCommand } from '../base-command'; export class ListWorkflowCommand extends BaseCommand { static description = '\nList workflows'; diff --git a/packages/cli/src/commands/mfa/disable.ts b/packages/cli/src/commands/mfa/disable.ts index dceb81b8b9..acc3439e3d 100644 --- a/packages/cli/src/commands/mfa/disable.ts +++ b/packages/cli/src/commands/mfa/disable.ts @@ -1,7 +1,7 @@ import Container from 'typedi'; import { Flags } from '@oclif/core'; import { AuthUserRepository } from '@db/repositories/authUser.repository'; -import { BaseCommand } from '../BaseCommand'; +import { BaseCommand } from '../base-command'; export class DisableMFACommand extends BaseCommand { static description = 'Disable MFA authentication for a user'; diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index cc4555dc5e..96e3849860 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -11,12 +11,12 @@ import glob from 'fast-glob'; import { jsonParse, randomString } from 'n8n-workflow'; import config from '@/config'; -import { ActiveExecutions } from '@/ActiveExecutions'; -import { ActiveWorkflowManager } from '@/ActiveWorkflowManager'; -import { Server } from '@/Server'; +import { ActiveExecutions } from '@/active-executions'; +import { ActiveWorkflowManager } from '@/active-workflow-manager'; +import { Server } from '@/server'; import { EDITOR_UI_DIST_DIR, LICENSE_FEATURES } from '@/constants'; import { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus'; -import { License } from '@/License'; +import { License } from '@/license'; import { OrchestrationService } from '@/services/orchestration.service'; import { OrchestrationHandlerMainService } from '@/services/orchestration/main/orchestration.handler.main.service'; import { PruningService } from '@/services/pruning.service'; @@ -24,12 +24,12 @@ import { UrlService } from '@/services/url.service'; import { SettingsRepository } from '@db/repositories/settings.repository'; import { ExecutionRepository } from '@db/repositories/execution.repository'; import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error'; -import { WaitTracker } from '@/WaitTracker'; -import { BaseCommand } from './BaseCommand'; +import { WaitTracker } from '@/wait-tracker'; +import { BaseCommand } from './base-command'; import type { IWorkflowExecutionDataProcess } from '@/Interfaces'; import { ExecutionService } from '@/executions/execution.service'; import { OwnershipService } from '@/services/ownership.service'; -import { WorkflowRunner } from '@/WorkflowRunner'; +import { WorkflowRunner } from '@/workflow-runner'; import { EventService } from '@/events/event.service'; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires @@ -239,7 +239,10 @@ export class Start extends BaseCommand { await orchestrationService.init(); - await Container.get(OrchestrationHandlerMainService).init(); + await Container.get(OrchestrationHandlerMainService).initWithOptions({ + queueModeId: this.queueModeId, + redisPublisher: Container.get(OrchestrationService).redisPublisher, + }); if (!orchestrationService.isMultiMainSetupEnabled) return; diff --git a/packages/cli/src/commands/update/workflow.ts b/packages/cli/src/commands/update/workflow.ts index f365db2d98..35a3ac016c 100644 --- a/packages/cli/src/commands/update/workflow.ts +++ b/packages/cli/src/commands/update/workflow.ts @@ -1,7 +1,7 @@ import { Container } from 'typedi'; import { Flags } from '@oclif/core'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; -import { BaseCommand } from '../BaseCommand'; +import { BaseCommand } from '../base-command'; export class UpdateWorkflowCommand extends BaseCommand { static description = 'Update workflows'; diff --git a/packages/cli/src/commands/user-management/reset.ts b/packages/cli/src/commands/user-management/reset.ts index 30f60af0a8..992d367e13 100644 --- a/packages/cli/src/commands/user-management/reset.ts +++ b/packages/cli/src/commands/user-management/reset.ts @@ -6,7 +6,7 @@ import { SettingsRepository } from '@db/repositories/settings.repository'; import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { UserRepository } from '@db/repositories/user.repository'; -import { BaseCommand } from '../BaseCommand'; +import { BaseCommand } from '../base-command'; import { ProjectRepository } from '@/databases/repositories/project.repository'; const defaultUserProps = { diff --git a/packages/cli/src/commands/webhook.ts b/packages/cli/src/commands/webhook.ts index d9c197c850..36e898761c 100644 --- a/packages/cli/src/commands/webhook.ts +++ b/packages/cli/src/commands/webhook.ts @@ -3,9 +3,9 @@ import { Flags, type Config } from '@oclif/core'; import { ApplicationError } from 'n8n-workflow'; import config from '@/config'; -import { ActiveExecutions } from '@/ActiveExecutions'; -import { WebhookServer } from '@/webhooks/WebhookServer'; -import { BaseCommand } from './BaseCommand'; +import { ActiveExecutions } from '@/active-executions'; +import { WebhookServer } from '@/webhooks/webhook-server'; +import { BaseCommand } from './base-command'; import { OrchestrationWebhookService } from '@/services/orchestration/webhook/orchestration.webhook.service'; import { OrchestrationHandlerWebhookService } from '@/services/orchestration/webhook/orchestration.handler.webhook.service'; diff --git a/packages/cli/src/commands/worker.ts b/packages/cli/src/commands/worker.ts index 5e75e4792d..594b2f5668 100644 --- a/packages/cli/src/commands/worker.ts +++ b/packages/cli/src/commands/worker.ts @@ -5,12 +5,12 @@ import http from 'http'; import { ApplicationError } from 'n8n-workflow'; import * as Db from '@/Db'; -import * as ResponseHelper from '@/ResponseHelper'; +import * as ResponseHelper from '@/response-helper'; import config from '@/config'; import type { ScalingService } from '@/scaling/scaling.service'; import { N8N_VERSION, inTest } from '@/constants'; import type { ICredentialsOverwrite } from '@/Interfaces'; -import { CredentialsOverwrites } from '@/CredentialsOverwrites'; +import { CredentialsOverwrites } from '@/credentials-overwrites'; import { rawBodyReader, bodyParser } from '@/middlewares'; import { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus'; import type { RedisServicePubSubSubscriber } from '@/services/redis/RedisServicePubSubSubscriber'; @@ -18,7 +18,7 @@ import { EventMessageGeneric } from '@/eventbus/EventMessageClasses/EventMessage import { OrchestrationHandlerWorkerService } from '@/services/orchestration/worker/orchestration.handler.worker.service'; import { OrchestrationWorkerService } from '@/services/orchestration/worker/orchestration.worker.service'; import { ServiceUnavailableError } from '@/errors/response-errors/service-unavailable.error'; -import { BaseCommand } from './BaseCommand'; +import { BaseCommand } from './base-command'; import { JobProcessor } from '@/scaling/job-processor'; import { LogStreamingEventRelay } from '@/events/log-streaming-event-relay'; diff --git a/packages/cli/src/concurrency/__tests__/concurrency-control.service.test.ts b/packages/cli/src/concurrency/__tests__/concurrency-control.service.test.ts index 08f58ac600..418ac96709 100644 --- a/packages/cli/src/concurrency/__tests__/concurrency-control.service.test.ts +++ b/packages/cli/src/concurrency/__tests__/concurrency-control.service.test.ts @@ -5,7 +5,7 @@ import { CLOUD_TEMP_REPORTABLE_THRESHOLDS, ConcurrencyControlService, } from '@/concurrency/concurrency-control.service'; -import type { Logger } from '@/Logger'; +import type { Logger } from '@/logger'; import { InvalidConcurrencyLimitError } from '@/errors/invalid-concurrency-limit.error'; import { ConcurrencyQueue } from '../concurrency-queue'; import type { WorkflowExecuteMode as ExecutionMode } from 'n8n-workflow'; diff --git a/packages/cli/src/concurrency/concurrency-control.service.ts b/packages/cli/src/concurrency/concurrency-control.service.ts index 6e62e9b78d..b2b94a5d6e 100644 --- a/packages/cli/src/concurrency/concurrency-control.service.ts +++ b/packages/cli/src/concurrency/concurrency-control.service.ts @@ -1,4 +1,4 @@ -import { Logger } from '@/Logger'; +import { Logger } from '@/logger'; import config from '@/config'; import { Service } from 'typedi'; import { ConcurrencyQueue } from './concurrency-queue'; diff --git a/packages/cli/src/controllers/__tests__/dynamic-node-parameters.controller.test.ts b/packages/cli/src/controllers/__tests__/dynamic-node-parameters.controller.test.ts index fe1313a4ec..0a129eb821 100644 --- a/packages/cli/src/controllers/__tests__/dynamic-node-parameters.controller.test.ts +++ b/packages/cli/src/controllers/__tests__/dynamic-node-parameters.controller.test.ts @@ -2,7 +2,7 @@ import { DynamicNodeParametersController } from '@/controllers/dynamicNodeParame import type { DynamicNodeParametersRequest } from '@/requests'; import type { DynamicNodeParametersService } from '@/services/dynamicNodeParameters.service'; import { mock } from 'jest-mock-extended'; -import * as AdditionalData from '@/WorkflowExecuteAdditionalData'; +import * as AdditionalData from '@/workflow-execute-additional-data'; import type { ILoadOptions, IWorkflowExecuteAdditionalData } from 'n8n-workflow'; describe('DynamicNodeParametersController', () => { diff --git a/packages/cli/src/controllers/__tests__/me.controller.test.ts b/packages/cli/src/controllers/__tests__/me.controller.test.ts index 71a6d69384..99f4714023 100644 --- a/packages/cli/src/controllers/__tests__/me.controller.test.ts +++ b/packages/cli/src/controllers/__tests__/me.controller.test.ts @@ -2,21 +2,23 @@ import type { Response } from 'express'; import { Container } from 'typedi'; import jwt from 'jsonwebtoken'; import { mock, anyObject } from 'jest-mock-extended'; + import type { PublicUser } from '@/Interfaces'; import type { User } from '@db/entities/User'; import { API_KEY_PREFIX, MeController } from '@/controllers/me.controller'; import { AUTH_COOKIE_NAME } from '@/constants'; import type { AuthenticatedRequest, MeRequest } from '@/requests'; import { UserService } from '@/services/user.service'; -import { ExternalHooks } from '@/ExternalHooks'; -import { License } from '@/License'; +import { ExternalHooks } from '@/external-hooks'; +import { License } from '@/license'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; -import { UserRepository } from '@/databases/repositories/user.repository'; import { EventService } from '@/events/event.service'; import { badPasswords } from '@test/testData'; import { mockInstance } from '@test/mocking'; import { AuthUserRepository } from '@/databases/repositories/authUser.repository'; -import { MfaService } from '@/Mfa/mfa.service'; +import { InvalidAuthTokenRepository } from '@db/repositories/invalidAuthToken.repository'; +import { UserRepository } from '@db/repositories/user.repository'; +import { MfaService } from '@/mfa/mfa.service'; import { InvalidMfaCodeError } from '@/errors/response-errors/invalid-mfa-code.error'; const browserId = 'test-browser-id'; @@ -28,6 +30,7 @@ describe('MeController', () => { const userRepository = mockInstance(UserRepository); const mockMfaService = mockInstance(MfaService); mockInstance(AuthUserRepository); + mockInstance(InvalidAuthTokenRepository); mockInstance(License).isWithinUsersLimit.mockReturnValue(true); const controller = Container.get(MeController); @@ -349,10 +352,40 @@ describe('MeController', () => { ); }); - it('should throw BadRequestError on XSS attempt', async () => { - const req = mock({ - body: { 'test-answer': '' }, - }); + test.each([ + 'automationGoalDevops', + 'companyIndustryExtended', + 'otherCompanyIndustryExtended', + 'automationGoalSm', + 'usageModes', + ])('should throw BadRequestError on XSS attempt for an array field %s', async (fieldName) => { + const req = mock(); + req.body = { + version: 'v4', + personalization_survey_n8n_version: '1.0.0', + personalization_survey_submitted_at: new Date().toISOString(), + [fieldName]: [''], + }; + + await expect(controller.storeSurveyAnswers(req)).rejects.toThrowError(BadRequestError); + }); + + test.each([ + 'automationGoalDevopsOther', + 'companySize', + 'companyType', + 'automationGoalSmOther', + 'roleOther', + 'reportedSource', + 'reportedSourceOther', + ])('should throw BadRequestError on XSS attempt for a string field %s', async (fieldName) => { + const req = mock(); + req.body = { + version: 'v4', + personalization_survey_n8n_version: '1.0.0', + personalization_survey_submitted_at: new Date().toISOString(), + [fieldName]: '', + }; await expect(controller.storeSurveyAnswers(req)).rejects.toThrowError(BadRequestError); }); diff --git a/packages/cli/src/controllers/__tests__/owner.controller.test.ts b/packages/cli/src/controllers/__tests__/owner.controller.test.ts index 8c4f00abee..0170333e8a 100644 --- a/packages/cli/src/controllers/__tests__/owner.controller.test.ts +++ b/packages/cli/src/controllers/__tests__/owner.controller.test.ts @@ -10,7 +10,7 @@ import type { User } from '@db/entities/User'; import type { SettingsRepository } from '@db/repositories/settings.repository'; import type { UserRepository } from '@db/repositories/user.repository'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; -import { License } from '@/License'; +import { License } from '@/license'; import type { OwnerRequest } from '@/requests'; import type { UserService } from '@/services/user.service'; import { PasswordUtility } from '@/services/password.utility'; diff --git a/packages/cli/src/controllers/__tests__/translation.controller.test.ts b/packages/cli/src/controllers/__tests__/translation.controller.test.ts index e34237cd69..129a2f91a6 100644 --- a/packages/cli/src/controllers/__tests__/translation.controller.test.ts +++ b/packages/cli/src/controllers/__tests__/translation.controller.test.ts @@ -6,7 +6,7 @@ import { CREDENTIAL_TRANSLATIONS_DIR, } from '@/controllers/translation.controller'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; -import type { CredentialTypes } from '@/CredentialTypes'; +import type { CredentialTypes } from '@/credential-types'; describe('TranslationController', () => { const configGetSpy = jest.spyOn(config, 'getEnv'); diff --git a/packages/cli/src/controllers/auth.controller.ts b/packages/cli/src/controllers/auth.controller.ts index 99b5c52320..fe48530831 100644 --- a/packages/cli/src/controllers/auth.controller.ts +++ b/packages/cli/src/controllers/auth.controller.ts @@ -1,9 +1,9 @@ import validator from 'validator'; +import { Response } from 'express'; import { AuthService } from '@/auth/auth.service'; import { Get, Post, RestController } from '@/decorators'; import { RESPONSE_ERROR_MESSAGES } from '@/constants'; -import { Request, Response } from 'express'; import type { User } from '@db/entities/User'; import { AuthenticatedRequest, LoginRequest, UserRequest } from '@/requests'; import type { PublicUser } from '@/Interfaces'; @@ -13,11 +13,11 @@ import { getCurrentAuthenticationMethod, isLdapCurrentAuthenticationMethod, isSamlCurrentAuthenticationMethod, -} from '@/sso/ssoHelpers'; -import { License } from '@/License'; +} from '@/sso/sso-helpers'; +import { License } from '@/license'; import { UserService } from '@/services/user.service'; -import { MfaService } from '@/Mfa/mfa.service'; -import { Logger } from '@/Logger'; +import { MfaService } from '@/mfa/mfa.service'; +import { Logger } from '@/logger'; import { AuthError } from '@/errors/response-errors/auth.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; @@ -185,7 +185,8 @@ export class AuthController { /** Log out a user */ @Post('/logout') - logout(_: Request, res: Response) { + async logout(req: AuthenticatedRequest, res: Response) { + await this.authService.invalidateToken(req); this.authService.clearCookie(res); return { loggedOut: true }; } diff --git a/packages/cli/src/controllers/debug.controller.ts b/packages/cli/src/controllers/debug.controller.ts index 663c375611..3b340aad7e 100644 --- a/packages/cli/src/controllers/debug.controller.ts +++ b/packages/cli/src/controllers/debug.controller.ts @@ -1,5 +1,5 @@ import { Get, RestController } from '@/decorators'; -import { ActiveWorkflowManager } from '@/ActiveWorkflowManager'; +import { ActiveWorkflowManager } from '@/active-workflow-manager'; import { OrchestrationService } from '@/services/orchestration.service'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; diff --git a/packages/cli/src/controllers/dynamicNodeParameters.controller.ts b/packages/cli/src/controllers/dynamicNodeParameters.controller.ts index bfd1cf651b..cff338e437 100644 --- a/packages/cli/src/controllers/dynamicNodeParameters.controller.ts +++ b/packages/cli/src/controllers/dynamicNodeParameters.controller.ts @@ -1,7 +1,7 @@ import type { INodePropertyOptions, NodeParameterValueType } from 'n8n-workflow'; import { Post, RestController } from '@/decorators'; -import { getBase } from '@/WorkflowExecuteAdditionalData'; +import { getBase } from '@/workflow-execute-additional-data'; import { DynamicNodeParametersService } from '@/services/dynamicNodeParameters.service'; import { DynamicNodeParametersRequest } from '@/requests'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; diff --git a/packages/cli/src/controllers/e2e.controller.ts b/packages/cli/src/controllers/e2e.controller.ts index 2a93fe8eda..6d8eb88fed 100644 --- a/packages/cli/src/controllers/e2e.controller.ts +++ b/packages/cli/src/controllers/e2e.controller.ts @@ -3,19 +3,19 @@ import { v4 as uuid } from 'uuid'; import config from '@/config'; import { SettingsRepository } from '@db/repositories/settings.repository'; import { UserRepository } from '@db/repositories/user.repository'; -import { ActiveWorkflowManager } from '@/ActiveWorkflowManager'; +import { ActiveWorkflowManager } from '@/active-workflow-manager'; import { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus'; -import { License } from '@/License'; +import { License } from '@/license'; import { LICENSE_FEATURES, LICENSE_QUOTAS, UNLIMITED_LICENSE_QUOTA, inE2ETests } from '@/constants'; import { Patch, Post, RestController } from '@/decorators'; import type { UserSetupPayload } from '@/requests'; import type { BooleanLicenseFeature, IPushDataType, NumericLicenseFeature } from '@/Interfaces'; -import { MfaService } from '@/Mfa/mfa.service'; +import { MfaService } from '@/mfa/mfa.service'; import { Push } from '@/push'; import { CacheService } from '@/services/cache/cache.service'; import { PasswordUtility } from '@/services/password.utility'; import Container from 'typedi'; -import { Logger } from '@/Logger'; +import { Logger } from '@/logger'; import { AuthUserRepository } from '@/databases/repositories/authUser.repository'; if (!inE2ETests) { diff --git a/packages/cli/src/controllers/invitation.controller.ts b/packages/cli/src/controllers/invitation.controller.ts index 19bd803c5e..624cc9b3f0 100644 --- a/packages/cli/src/controllers/invitation.controller.ts +++ b/packages/cli/src/controllers/invitation.controller.ts @@ -6,17 +6,17 @@ import config from '@/config'; import { Post, GlobalScope, RestController } from '@/decorators'; import { RESPONSE_ERROR_MESSAGES } from '@/constants'; import { UserRequest } from '@/requests'; -import { License } from '@/License'; +import { License } from '@/license'; import { UserService } from '@/services/user.service'; -import { Logger } from '@/Logger'; -import { isSamlLicensedAndEnabled } from '@/sso/saml/samlHelpers'; +import { Logger } from '@/logger'; +import { isSamlLicensedAndEnabled } from '@/sso/saml/saml-helpers'; import { PasswordUtility } from '@/services/password.utility'; import { PostHogClient } from '@/posthog'; import type { User } from '@/databases/entities/User'; import { UserRepository } from '@db/repositories/user.repository'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; -import { ExternalHooks } from '@/ExternalHooks'; +import { ExternalHooks } from '@/external-hooks'; import { EventService } from '@/events/event.service'; @RestController('/invitations') diff --git a/packages/cli/src/controllers/me.controller.ts b/packages/cli/src/controllers/me.controller.ts index 19429228ed..6cad9dcef5 100644 --- a/packages/cli/src/controllers/me.controller.ts +++ b/packages/cli/src/controllers/me.controller.ts @@ -6,7 +6,7 @@ import { randomBytes } from 'crypto'; import { AuthService } from '@/auth/auth.service'; import { Delete, Get, Patch, Post, RestController } from '@/decorators'; import { PasswordUtility } from '@/services/password.utility'; -import { validateEntity, validateRecordNoXss } from '@/GenericHelpers'; +import { validateEntity } from '@/generic-helpers'; import type { User } from '@db/entities/User'; import { AuthenticatedRequest, @@ -15,16 +15,17 @@ import { UserUpdatePayload, } from '@/requests'; import type { PublicUser } from '@/Interfaces'; -import { isSamlLicensedAndEnabled } from '@/sso/saml/samlHelpers'; +import { isSamlLicensedAndEnabled } from '@/sso/saml/saml-helpers'; import { UserService } from '@/services/user.service'; -import { Logger } from '@/Logger'; -import { ExternalHooks } from '@/ExternalHooks'; +import { Logger } from '@/logger'; +import { ExternalHooks } from '@/external-hooks'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { UserRepository } from '@/databases/repositories/user.repository'; import { isApiEnabled } from '@/PublicApi'; import { EventService } from '@/events/event.service'; -import { MfaService } from '@/Mfa/mfa.service'; +import { MfaService } from '@/mfa/mfa.service'; import { InvalidMfaCodeError } from '@/errors/response-errors/invalid-mfa-code.error'; +import { PersonalizationSurveyAnswersV4 } from './survey-answers.dto'; export const API_KEY_PREFIX = 'n8n_api_'; @@ -195,7 +196,7 @@ export class MeController { if (!personalizationAnswers) { this.logger.debug( - 'Request to store user personalization survey failed because of empty payload', + 'Request to store user personalization survey failed because of undefined payload', { userId: req.user.id, }, @@ -203,12 +204,18 @@ export class MeController { throw new BadRequestError('Personalization answers are mandatory'); } - await validateRecordNoXss(personalizationAnswers); + const validatedAnswers = plainToInstance( + PersonalizationSurveyAnswersV4, + personalizationAnswers, + { excludeExtraneousValues: true }, + ); + + await validateEntity(validatedAnswers); await this.userRepository.save( { id: req.user.id, - personalizationAnswers, + personalizationAnswers: validatedAnswers, }, { transaction: false }, ); @@ -217,7 +224,7 @@ export class MeController { this.eventService.emit('user-submitted-personalization-survey', { userId: req.user.id, - answers: personalizationAnswers, + answers: validatedAnswers, }); return { success: true }; diff --git a/packages/cli/src/controllers/mfa.controller.ts b/packages/cli/src/controllers/mfa.controller.ts index 6c228df47e..3d10736a1a 100644 --- a/packages/cli/src/controllers/mfa.controller.ts +++ b/packages/cli/src/controllers/mfa.controller.ts @@ -1,6 +1,6 @@ import { Get, Post, RestController } from '@/decorators'; import { AuthenticatedRequest, MFA } from '@/requests'; -import { MfaService } from '@/Mfa/mfa.service'; +import { MfaService } from '@/mfa/mfa.service'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; @RestController('/mfa') diff --git a/packages/cli/src/controllers/nodeTypes.controller.ts b/packages/cli/src/controllers/nodeTypes.controller.ts index fddefb7e10..122b9f1fbd 100644 --- a/packages/cli/src/controllers/nodeTypes.controller.ts +++ b/packages/cli/src/controllers/nodeTypes.controller.ts @@ -4,7 +4,7 @@ import { Request } from 'express'; import type { INodeTypeDescription, INodeTypeNameVersion } from 'n8n-workflow'; import { Post, RestController } from '@/decorators'; import config from '@/config'; -import { NodeTypes } from '@/NodeTypes'; +import { NodeTypes } from '@/node-types'; @RestController('/node-types') export class NodeTypesController { diff --git a/packages/cli/src/controllers/oauth/__tests__/oAuth1Credential.controller.test.ts b/packages/cli/src/controllers/oauth/__tests__/oAuth1Credential.controller.test.ts index fae443b4ce..d0031186f7 100644 --- a/packages/cli/src/controllers/oauth/__tests__/oAuth1Credential.controller.test.ts +++ b/packages/cli/src/controllers/oauth/__tests__/oAuth1Credential.controller.test.ts @@ -11,11 +11,11 @@ import type { User } from '@db/entities/User'; import type { OAuthRequest } from '@/requests'; import { CredentialsRepository } from '@db/repositories/credentials.repository'; import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; -import { ExternalHooks } from '@/ExternalHooks'; -import { Logger } from '@/Logger'; +import { ExternalHooks } from '@/external-hooks'; +import { Logger } from '@/logger'; import { VariablesService } from '@/environments/variables/variables.service.ee'; -import { SecretsHelper } from '@/SecretsHelpers'; -import { CredentialsHelper } from '@/CredentialsHelper'; +import { SecretsHelper } from '@/secrets-helpers'; +import { CredentialsHelper } from '@/credentials-helper'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; diff --git a/packages/cli/src/controllers/oauth/__tests__/oAuth2Credential.controller.test.ts b/packages/cli/src/controllers/oauth/__tests__/oAuth2Credential.controller.test.ts index f354a1ec02..c81a0c1559 100644 --- a/packages/cli/src/controllers/oauth/__tests__/oAuth2Credential.controller.test.ts +++ b/packages/cli/src/controllers/oauth/__tests__/oAuth2Credential.controller.test.ts @@ -11,11 +11,11 @@ import type { User } from '@db/entities/User'; import type { OAuthRequest } from '@/requests'; import { CredentialsRepository } from '@db/repositories/credentials.repository'; import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; -import { ExternalHooks } from '@/ExternalHooks'; -import { Logger } from '@/Logger'; +import { ExternalHooks } from '@/external-hooks'; +import { Logger } from '@/logger'; import { VariablesService } from '@/environments/variables/variables.service.ee'; -import { SecretsHelper } from '@/SecretsHelpers'; -import { CredentialsHelper } from '@/CredentialsHelper'; +import { SecretsHelper } from '@/secrets-helpers'; +import { CredentialsHelper } from '@/credentials-helper'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; diff --git a/packages/cli/src/controllers/oauth/abstractOAuth.controller.ts b/packages/cli/src/controllers/oauth/abstractOAuth.controller.ts index b9f0d64446..0d14b222d4 100644 --- a/packages/cli/src/controllers/oauth/abstractOAuth.controller.ts +++ b/packages/cli/src/controllers/oauth/abstractOAuth.controller.ts @@ -11,10 +11,10 @@ import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials. import type { ICredentialsDb } from '@/Interfaces'; import type { OAuthRequest } from '@/requests'; import { RESPONSE_ERROR_MESSAGES } from '@/constants'; -import { CredentialsHelper } from '@/CredentialsHelper'; -import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'; -import { Logger } from '@/Logger'; -import { ExternalHooks } from '@/ExternalHooks'; +import { CredentialsHelper } from '@/credentials-helper'; +import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data'; +import { Logger } from '@/logger'; +import { ExternalHooks } from '@/external-hooks'; import { UrlService } from '@/services/url.service'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; diff --git a/packages/cli/src/controllers/oauth/oAuth1Credential.controller.ts b/packages/cli/src/controllers/oauth/oAuth1Credential.controller.ts index 2a50b00bf9..21bbe83f99 100644 --- a/packages/cli/src/controllers/oauth/oAuth1Credential.controller.ts +++ b/packages/cli/src/controllers/oauth/oAuth1Credential.controller.ts @@ -6,7 +6,7 @@ import clientOAuth1 from 'oauth-1.0a'; import { createHmac } from 'crypto'; import { Get, RestController } from '@/decorators'; import { OAuthRequest } from '@/requests'; -import { sendErrorResponse } from '@/ResponseHelper'; +import { sendErrorResponse } from '@/response-helper'; import { AbstractOAuthController, type CsrfStateParam } from './abstractOAuth.controller'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; diff --git a/packages/cli/src/controllers/orchestration.controller.ts b/packages/cli/src/controllers/orchestration.controller.ts index 74a9665e1e..852d6f7cc3 100644 --- a/packages/cli/src/controllers/orchestration.controller.ts +++ b/packages/cli/src/controllers/orchestration.controller.ts @@ -1,7 +1,7 @@ import { Post, RestController, GlobalScope } from '@/decorators'; import { OrchestrationRequest } from '@/requests'; import { OrchestrationService } from '@/services/orchestration.service'; -import { License } from '@/License'; +import { License } from '@/license'; @RestController('/orchestration') export class OrchestrationController { diff --git a/packages/cli/src/controllers/owner.controller.ts b/packages/cli/src/controllers/owner.controller.ts index c0c2e7308d..826bbc2fa7 100644 --- a/packages/cli/src/controllers/owner.controller.ts +++ b/packages/cli/src/controllers/owner.controller.ts @@ -3,7 +3,7 @@ import { Response } from 'express'; import { AuthService } from '@/auth/auth.service'; import config from '@/config'; -import { validateEntity } from '@/GenericHelpers'; +import { validateEntity } from '@/generic-helpers'; import { GlobalScope, Post, RestController } from '@/decorators'; import { PasswordUtility } from '@/services/password.utility'; import { OwnerRequest } from '@/requests'; @@ -11,7 +11,7 @@ import { SettingsRepository } from '@db/repositories/settings.repository'; import { UserRepository } from '@db/repositories/user.repository'; import { PostHogClient } from '@/posthog'; import { UserService } from '@/services/user.service'; -import { Logger } from '@/Logger'; +import { Logger } from '@/logger'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { EventService } from '@/events/event.service'; diff --git a/packages/cli/src/controllers/passwordReset.controller.ts b/packages/cli/src/controllers/passwordReset.controller.ts index 0cfe7cac19..3f93ee08e9 100644 --- a/packages/cli/src/controllers/passwordReset.controller.ts +++ b/packages/cli/src/controllers/passwordReset.controller.ts @@ -4,15 +4,15 @@ import validator from 'validator'; import { AuthService } from '@/auth/auth.service'; import { Get, Post, RestController } from '@/decorators'; import { PasswordUtility } from '@/services/password.utility'; -import { UserManagementMailer } from '@/UserManagement/email'; +import { UserManagementMailer } from '@/user-management/email'; import { PasswordResetRequest } from '@/requests'; -import { isSamlCurrentAuthenticationMethod } from '@/sso/ssoHelpers'; +import { isSamlCurrentAuthenticationMethod } from '@/sso/sso-helpers'; import { UserService } from '@/services/user.service'; -import { License } from '@/License'; +import { License } from '@/license'; import { RESPONSE_ERROR_MESSAGES } from '@/constants'; -import { MfaService } from '@/Mfa/mfa.service'; -import { Logger } from '@/Logger'; -import { ExternalHooks } from '@/ExternalHooks'; +import { MfaService } from '@/mfa/mfa.service'; +import { Logger } from '@/logger'; +import { ExternalHooks } from '@/external-hooks'; import { UrlService } from '@/services/url.service'; import { InternalServerError } from '@/errors/response-errors/internal-server.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; diff --git a/packages/cli/src/controllers/survey-answers.dto.ts b/packages/cli/src/controllers/survey-answers.dto.ts new file mode 100644 index 0000000000..f115a6992b --- /dev/null +++ b/packages/cli/src/controllers/survey-answers.dto.ts @@ -0,0 +1,109 @@ +import { NoXss } from '@/validators/no-xss.validator'; +import { Expose } from 'class-transformer'; +import { IsString, IsArray, IsOptional, IsEmail, IsEnum } from 'class-validator'; +import type { IPersonalizationSurveyAnswersV4 } from 'n8n-workflow'; + +export class PersonalizationSurveyAnswersV4 implements IPersonalizationSurveyAnswersV4 { + @NoXss() + @Expose() + @IsEnum(['v4']) + version: 'v4'; + + @NoXss() + @Expose() + @IsString() + personalization_survey_submitted_at: string; + + @NoXss() + @Expose() + @IsString() + personalization_survey_n8n_version: string; + + @Expose() + @IsOptional() + @IsArray() + @NoXss({ each: true }) + @IsString({ each: true }) + automationGoalDevops?: string[] | null; + + @NoXss() + @Expose() + @IsOptional() + @IsString() + automationGoalDevopsOther?: string | null; + + @NoXss({ each: true }) + @Expose() + @IsOptional() + @IsArray() + @IsString({ each: true }) + companyIndustryExtended?: string[] | null; + + @NoXss({ each: true }) + @Expose() + @IsOptional() + @IsString({ each: true }) + otherCompanyIndustryExtended?: string[] | null; + + @NoXss() + @Expose() + @IsOptional() + @IsString() + companySize?: string | null; + + @NoXss() + @Expose() + @IsOptional() + @IsString() + companyType?: string | null; + + @NoXss({ each: true }) + @Expose() + @IsOptional() + @IsArray() + @IsString({ each: true }) + automationGoalSm?: string[] | null; + + @NoXss() + @Expose() + @IsOptional() + @IsString() + automationGoalSmOther?: string | null; + + @NoXss({ each: true }) + @Expose() + @IsOptional() + @IsArray() + @IsString({ each: true }) + usageModes?: string[] | null; + + @NoXss() + @Expose() + @IsOptional() + @IsEmail() + email?: string | null; + + @NoXss() + @Expose() + @IsOptional() + @IsString() + role?: string | null; + + @NoXss() + @Expose() + @IsOptional() + @IsString() + roleOther?: string | null; + + @NoXss() + @Expose() + @IsOptional() + @IsString() + reportedSource?: string | null; + + @NoXss() + @Expose() + @IsOptional() + @IsString() + reportedSourceOther?: string | null; +} diff --git a/packages/cli/src/controllers/translation.controller.ts b/packages/cli/src/controllers/translation.controller.ts index f359ec2b3a..03a8c3b035 100644 --- a/packages/cli/src/controllers/translation.controller.ts +++ b/packages/cli/src/controllers/translation.controller.ts @@ -6,7 +6,7 @@ import config from '@/config'; import { NODES_BASE_DIR } from '@/constants'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { InternalServerError } from '@/errors/response-errors/internal-server.error'; -import { CredentialTypes } from '@/CredentialTypes'; +import { CredentialTypes } from '@/credential-types'; export const CREDENTIAL_TRANSLATIONS_DIR = 'n8n-nodes-base/dist/credentials/translations'; export const NODE_HEADERS_PATH = join(NODES_BASE_DIR, 'dist/nodes/headers'); diff --git a/packages/cli/src/controllers/users.controller.ts b/packages/cli/src/controllers/users.controller.ts index f3055ee4dc..43916903fe 100644 --- a/packages/cli/src/controllers/users.controller.ts +++ b/packages/cli/src/controllers/users.controller.ts @@ -16,12 +16,12 @@ import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.reposi import { UserRepository } from '@db/repositories/user.repository'; import { UserService } from '@/services/user.service'; import { listQueryMiddleware } from '@/middlewares'; -import { Logger } from '@/Logger'; +import { Logger } from '@/logger'; import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; -import { ExternalHooks } from '@/ExternalHooks'; -import { validateEntity } from '@/GenericHelpers'; +import { ExternalHooks } from '@/external-hooks'; +import { validateEntity } from '@/generic-helpers'; import { ProjectRepository } from '@/databases/repositories/project.repository'; import { Project } from '@/databases/entities/Project'; import { WorkflowService } from '@/workflows/workflow.service'; diff --git a/packages/cli/src/controllers/workflowStatistics.controller.ts b/packages/cli/src/controllers/workflowStatistics.controller.ts index 0c86786129..2cae52e8cc 100644 --- a/packages/cli/src/controllers/workflowStatistics.controller.ts +++ b/packages/cli/src/controllers/workflowStatistics.controller.ts @@ -5,7 +5,7 @@ import { StatisticsNames } from '@db/entities/WorkflowStatistics'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { WorkflowStatisticsRepository } from '@db/repositories/workflowStatistics.repository'; import type { IWorkflowStatisticsDataLoaded } from '@/Interfaces'; -import { Logger } from '@/Logger'; +import { Logger } from '@/logger'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { StatisticsRequest } from './workflow-statistics.types'; diff --git a/packages/cli/src/CrashJournal.ts b/packages/cli/src/crash-journal.ts similarity index 96% rename from packages/cli/src/CrashJournal.ts rename to packages/cli/src/crash-journal.ts index ab13f2cb12..dcba654a3d 100644 --- a/packages/cli/src/CrashJournal.ts +++ b/packages/cli/src/crash-journal.ts @@ -5,7 +5,7 @@ import { Container } from 'typedi'; import { InstanceSettings } from 'n8n-core'; import { sleep } from 'n8n-workflow'; import { inProduction } from '@/constants'; -import { Logger } from '@/Logger'; +import { Logger } from '@/logger'; export const touchFile = async (filePath: string): Promise => { await mkdir(dirname(filePath), { recursive: true }); diff --git a/packages/cli/src/CredentialTypes.ts b/packages/cli/src/credential-types.ts similarity index 96% rename from packages/cli/src/CredentialTypes.ts rename to packages/cli/src/credential-types.ts index d97825a672..8d665c7592 100644 --- a/packages/cli/src/CredentialTypes.ts +++ b/packages/cli/src/credential-types.ts @@ -7,7 +7,7 @@ import { type LoadedClass, } from 'n8n-workflow'; import { RESPONSE_ERROR_MESSAGES } from '@/constants'; -import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; +import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; @Service() export class CredentialTypes implements ICredentialTypes { diff --git a/packages/cli/src/CredentialsHelper.ts b/packages/cli/src/credentials-helper.ts similarity index 99% rename from packages/cli/src/CredentialsHelper.ts rename to packages/cli/src/credentials-helper.ts index 43ccf970e0..aa0491955c 100644 --- a/packages/cli/src/CredentialsHelper.ts +++ b/packages/cli/src/credentials-helper.ts @@ -30,8 +30,8 @@ import { ICredentialsHelper, NodeHelpers, Workflow, ApplicationError } from 'n8n import type { ICredentialsDb } from '@/Interfaces'; import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; -import { CredentialTypes } from '@/CredentialTypes'; -import { CredentialsOverwrites } from '@/CredentialsOverwrites'; +import { CredentialTypes } from '@/credential-types'; +import { CredentialsOverwrites } from '@/credentials-overwrites'; import { RESPONSE_ERROR_MESSAGES } from './constants'; import { CredentialsRepository } from '@db/repositories/credentials.repository'; diff --git a/packages/cli/src/CredentialsOverwrites.ts b/packages/cli/src/credentials-overwrites.ts similarity index 97% rename from packages/cli/src/CredentialsOverwrites.ts rename to packages/cli/src/credentials-overwrites.ts index b80a31d9ce..b8df1a848c 100644 --- a/packages/cli/src/CredentialsOverwrites.ts +++ b/packages/cli/src/credentials-overwrites.ts @@ -3,8 +3,8 @@ import { GlobalConfig } from '@n8n/config'; import type { ICredentialDataDecryptedObject } from 'n8n-workflow'; import { deepCopy, jsonParse } from 'n8n-workflow'; import type { ICredentialsOverwrite } from '@/Interfaces'; -import { CredentialTypes } from '@/CredentialTypes'; -import { Logger } from '@/Logger'; +import { CredentialTypes } from '@/credential-types'; +import { Logger } from '@/logger'; @Service() export class CredentialsOverwrites { diff --git a/packages/cli/src/credentials/__tests__/credentials.service.test.ts b/packages/cli/src/credentials/__tests__/credentials.service.test.ts index a5fa3ca183..8a5ce77e15 100644 --- a/packages/cli/src/credentials/__tests__/credentials.service.test.ts +++ b/packages/cli/src/credentials/__tests__/credentials.service.test.ts @@ -2,7 +2,7 @@ import { CREDENTIAL_EMPTY_VALUE, type ICredentialType } from 'n8n-workflow'; import { mock } from 'jest-mock-extended'; import { CREDENTIAL_BLANKING_VALUE } from '@/constants'; import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; -import type { CredentialTypes } from '@/CredentialTypes'; +import type { CredentialTypes } from '@/credential-types'; import { CredentialsService } from '@/credentials/credentials.service'; describe('CredentialsService', () => { diff --git a/packages/cli/src/credentials/credentials.controller.ts b/packages/cli/src/credentials/credentials.controller.ts index ba965a1add..b8dacf445f 100644 --- a/packages/cli/src/credentials/credentials.controller.ts +++ b/packages/cli/src/credentials/credentials.controller.ts @@ -5,11 +5,11 @@ import { In } from '@n8n/typeorm'; import { CredentialsService } from './credentials.service'; import { CredentialRequest } from '@/requests'; -import { Logger } from '@/Logger'; +import { Logger } from '@/logger'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; import { NamingService } from '@/services/naming.service'; -import { License } from '@/License'; +import { License } from '@/license'; import { EnterpriseCredentialsService } from './credentials.service.ee'; import { Delete, @@ -22,7 +22,7 @@ import { ProjectScope, } from '@/decorators'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; -import { UserManagementMailer } from '@/UserManagement/email'; +import { UserManagementMailer } from '@/user-management/email'; import * as Db from '@/Db'; import * as utils from '@/utils'; import { listQueryMiddleware } from '@/middlewares'; diff --git a/packages/cli/src/credentials/credentials.service.ts b/packages/cli/src/credentials/credentials.service.ts index 03ea13efe1..e5d1a53252 100644 --- a/packages/cli/src/credentials/credentials.service.ts +++ b/packages/cli/src/credentials/credentials.service.ts @@ -16,17 +16,17 @@ import { import type { Scope } from '@n8n/permissions'; import * as Db from '@/Db'; import type { ICredentialsDb } from '@/Interfaces'; -import { createCredentialsFromCredentialsEntity } from '@/CredentialsHelper'; +import { createCredentialsFromCredentialsEntity } from '@/credentials-helper'; import { CREDENTIAL_BLANKING_VALUE } from '@/constants'; import { CredentialsEntity } from '@db/entities/CredentialsEntity'; import { SharedCredentials } from '@db/entities/SharedCredentials'; -import { validateEntity } from '@/GenericHelpers'; -import { ExternalHooks } from '@/ExternalHooks'; +import { validateEntity } from '@/generic-helpers'; +import { ExternalHooks } from '@/external-hooks'; import type { User } from '@db/entities/User'; import type { CredentialRequest, ListQuery } from '@/requests'; -import { CredentialTypes } from '@/CredentialTypes'; +import { CredentialTypes } from '@/credential-types'; import { OwnershipService } from '@/services/ownership.service'; -import { Logger } from '@/Logger'; +import { Logger } from '@/logger'; import { CredentialsRepository } from '@db/repositories/credentials.repository'; import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; import { Service } from 'typedi'; @@ -38,7 +38,7 @@ import { NotFoundError } from '@/errors/response-errors/not-found.error'; import type { ProjectRelation } from '@/databases/entities/ProjectRelation'; import { RoleService } from '@/services/role.service'; import { UserRepository } from '@/databases/repositories/user.repository'; -import { userHasScope } from '@/permissions/checkAccess'; +import { userHasScope } from '@/permissions/check-access'; export type CredentialsGetSharedOptions = | { allowGlobalScope: true; globalScope: Scope } diff --git a/packages/cli/src/databases/dsl/Indices.ts b/packages/cli/src/databases/dsl/Indices.ts index a5ec95e101..040ab04105 100644 --- a/packages/cli/src/databases/dsl/Indices.ts +++ b/packages/cli/src/databases/dsl/Indices.ts @@ -1,5 +1,5 @@ import type { QueryRunner } from '@n8n/typeorm'; -import { TableIndex } from '@n8n/typeorm'; +import { TableIndex, TypeORMError } from '@n8n/typeorm'; import LazyPromise from 'p-lazy'; abstract class IndexOperation extends LazyPromise { @@ -48,10 +48,29 @@ export class CreateIndex extends IndexOperation { } export class DropIndex extends IndexOperation { + constructor( + tableName: string, + columnNames: string[], + tablePrefix: string, + queryRunner: QueryRunner, + customIndexName?: string, + protected skipIfMissing = false, + ) { + super(tableName, columnNames, tablePrefix, queryRunner, customIndexName); + } + async execute(queryRunner: QueryRunner) { - return await queryRunner.dropIndex( - this.fullTableName, - this.customIndexName ?? this.fullIndexName, - ); + return await queryRunner + .dropIndex(this.fullTableName, this.customIndexName ?? this.fullIndexName) + .catch((error) => { + if ( + error instanceof TypeORMError && + error.message.includes('not found') && + this.skipIfMissing + ) { + return; + } + throw error; + }); } } diff --git a/packages/cli/src/databases/dsl/index.ts b/packages/cli/src/databases/dsl/index.ts index bb5ef859ec..a51e61bd17 100644 --- a/packages/cli/src/databases/dsl/index.ts +++ b/packages/cli/src/databases/dsl/index.ts @@ -32,8 +32,14 @@ export const createSchemaBuilder = (tablePrefix: string, queryRunner: QueryRunne customIndexName?: string, ) => new CreateIndex(tableName, columnNames, isUnique, tablePrefix, queryRunner, customIndexName), - dropIndex: (tableName: string, columnNames: string[], customIndexName?: string) => - new DropIndex(tableName, columnNames, tablePrefix, queryRunner, customIndexName), + dropIndex: ( + tableName: string, + columnNames: string[], + { customIndexName, skipIfMissing }: { customIndexName?: string; skipIfMissing?: boolean } = { + skipIfMissing: false, + }, + ) => + new DropIndex(tableName, columnNames, tablePrefix, queryRunner, customIndexName, skipIfMissing), addForeignKey: ( tableName: string, diff --git a/packages/cli/src/databases/entities/InvalidAuthToken.ts b/packages/cli/src/databases/entities/InvalidAuthToken.ts new file mode 100644 index 0000000000..e21860d146 --- /dev/null +++ b/packages/cli/src/databases/entities/InvalidAuthToken.ts @@ -0,0 +1,11 @@ +import { Column, Entity, PrimaryColumn } from '@n8n/typeorm'; +import { datetimeColumnType } from './AbstractEntity'; + +@Entity() +export class InvalidAuthToken { + @PrimaryColumn() + token: string; + + @Column(datetimeColumnType) + expiresAt: Date; +} diff --git a/packages/cli/src/databases/entities/User.ts b/packages/cli/src/databases/entities/User.ts index d24af19742..dad8bbbe80 100644 --- a/packages/cli/src/databases/entities/User.ts +++ b/packages/cli/src/databases/entities/User.ts @@ -13,7 +13,7 @@ import { IsEmail, IsString, Length } from 'class-validator'; import type { IUser, IUserSettings } from 'n8n-workflow'; import type { SharedWorkflow } from './SharedWorkflow'; import type { SharedCredentials } from './SharedCredentials'; -import { NoXss } from '../utils/customValidators'; +import { NoXss } from '@/validators/no-xss.validator'; import { objectRetriever, lowerCaser } from '../utils/transformers'; import { WithTimestamps, jsonColumnType } from './AbstractEntity'; import type { IPersonalizationSurveyAnswers } from '@/Interfaces'; @@ -25,6 +25,7 @@ import { } from '@/permissions/global-roles'; import { hasScope, type ScopeOptions, type Scope } from '@n8n/permissions'; import type { ProjectRelation } from './ProjectRelation'; +import { NoUrl } from '@/validators/no-url.validator'; export type GlobalRole = 'global:owner' | 'global:admin' | 'global:member'; export type AssignableRole = Exclude; @@ -51,12 +52,14 @@ export class User extends WithTimestamps implements IUser { @Column({ length: 32, nullable: true }) @NoXss() + @NoUrl() @IsString({ message: 'First name must be of type string.' }) @Length(1, 32, { message: 'First name must be $constraint1 to $constraint2 characters long.' }) firstName: string; @Column({ length: 32, nullable: true }) @NoXss() + @NoUrl() @IsString({ message: 'Last name must be of type string.' }) @Length(1, 32, { message: 'Last name must be $constraint1 to $constraint2 characters long.' }) lastName: string; diff --git a/packages/cli/src/databases/entities/index.ts b/packages/cli/src/databases/entities/index.ts index db8b113baf..bd7d187486 100644 --- a/packages/cli/src/databases/entities/index.ts +++ b/packages/cli/src/databases/entities/index.ts @@ -21,6 +21,7 @@ import { ExecutionData } from './ExecutionData'; import { WorkflowHistory } from './WorkflowHistory'; import { Project } from './Project'; import { ProjectRelation } from './ProjectRelation'; +import { InvalidAuthToken } from './InvalidAuthToken'; export const entities = { AuthIdentity, @@ -31,6 +32,7 @@ export const entities = { ExecutionEntity, InstalledNodes, InstalledPackages, + InvalidAuthToken, Settings, SharedCredentials, SharedWorkflow, diff --git a/packages/cli/src/databases/migrations/common/1674509946020-CreateLdapEntities.ts b/packages/cli/src/databases/migrations/common/1674509946020-CreateLdapEntities.ts index 2115cbeffd..1d915a5576 100644 --- a/packages/cli/src/databases/migrations/common/1674509946020-CreateLdapEntities.ts +++ b/packages/cli/src/databases/migrations/common/1674509946020-CreateLdapEntities.ts @@ -1,5 +1,5 @@ import type { MigrationContext, ReversibleMigration } from '@db/types'; -import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/Ldap/constants'; +import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/ldap/constants'; export class CreateLdapEntities1674509946020 implements ReversibleMigration { async up({ escape, dbType, isMysql, runQuery }: MigrationContext) { diff --git a/packages/cli/src/databases/migrations/common/1723627610222-CreateInvalidAuthTokenTable.ts b/packages/cli/src/databases/migrations/common/1723627610222-CreateInvalidAuthTokenTable.ts new file mode 100644 index 0000000000..f28696c199 --- /dev/null +++ b/packages/cli/src/databases/migrations/common/1723627610222-CreateInvalidAuthTokenTable.ts @@ -0,0 +1,16 @@ +import type { MigrationContext, ReversibleMigration } from '@db/types'; + +const tableName = 'invalid_auth_token'; + +export class CreateInvalidAuthTokenTable1723627610222 implements ReversibleMigration { + async up({ schemaBuilder: { createTable, column } }: MigrationContext) { + await createTable(tableName).withColumns( + column('token').varchar(512).primary, + column('expiresAt').timestamp().notNull, + ); + } + + async down({ schemaBuilder: { dropTable } }: MigrationContext) { + await dropTable(tableName); + } +} diff --git a/packages/cli/src/databases/migrations/common/1723796243146-RefactorExecutionIndices.ts b/packages/cli/src/databases/migrations/common/1723796243146-RefactorExecutionIndices.ts new file mode 100644 index 0000000000..dcf558f202 --- /dev/null +++ b/packages/cli/src/databases/migrations/common/1723796243146-RefactorExecutionIndices.ts @@ -0,0 +1,118 @@ +import type { MigrationContext, ReversibleMigration } from '@/databases/types'; + +/** + * Add new indices: + * + * - `workflowId, startedAt` for `ExecutionRepository.findManyByRangeQuery` (default query) and for `ExecutionRepository.findManyByRangeQuery` (filter query) + * - `waitTill, status, deletedAt` for `ExecutionRepository.getWaitingExecutions` + * - `stoppedAt, status, deletedAt` for `ExecutionRepository.softDeletePrunableExecutions` + * + * Remove unused indices in sqlite: + * + * - `stoppedAt` (duplicate with different casing) + * - `waitTill` + * - `status, workflowId` + * + * Remove unused indices in MySQL: + * + * - `status` + * + * Remove unused indices in all DBs: + * + * - `waitTill, id` + * - `workflowId, id` + * + * Remove incomplete index in all DBs: + * + * - `stopped_at` (replaced with composite index) + * + * Keep index as is: + * + * - `deletedAt` for query at `ExecutionRepository.hardDeleteSoftDeletedExecutions` + */ +export class RefactorExecutionIndices1723796243146 implements ReversibleMigration { + async up({ schemaBuilder, isPostgres, isSqlite, isMysql, runQuery, escape }: MigrationContext) { + if (isSqlite || isPostgres) { + const executionEntity = escape.tableName('execution_entity'); + + const workflowId = escape.columnName('workflowId'); + const startedAt = escape.columnName('startedAt'); + const waitTill = escape.columnName('waitTill'); + const status = escape.columnName('status'); + const deletedAt = escape.columnName('deletedAt'); + const stoppedAt = escape.columnName('stoppedAt'); + + await runQuery(` + CREATE INDEX idx_execution_entity_workflow_id_started_at + ON ${executionEntity} (${workflowId}, ${startedAt}) + WHERE ${startedAt} IS NOT NULL AND ${deletedAt} IS NULL; + `); + + await runQuery(` + CREATE INDEX idx_execution_entity_wait_till_status_deleted_at + ON ${executionEntity} (${waitTill}, ${status}, ${deletedAt}) + WHERE ${waitTill} IS NOT NULL AND ${deletedAt} IS NULL; + `); + + await runQuery(` + CREATE INDEX idx_execution_entity_stopped_at_status_deleted_at + ON ${executionEntity} (${stoppedAt}, ${status}, ${deletedAt}) + WHERE ${stoppedAt} IS NOT NULL AND ${deletedAt} IS NULL; + `); + } else if (isMysql) { + await schemaBuilder.createIndex('execution_entity', ['workflowId', 'startedAt']); + await schemaBuilder.createIndex('execution_entity', ['waitTill', 'status', 'deletedAt']); + await schemaBuilder.createIndex('execution_entity', ['stoppedAt', 'status', 'deletedAt']); + } + + if (isSqlite) { + await schemaBuilder.dropIndex('execution_entity', ['waitTill'], { + customIndexName: 'idx_execution_entity_wait_till', + skipIfMissing: true, + }); + + await schemaBuilder.dropIndex('execution_entity', ['status', 'workflowId'], { + customIndexName: 'IDX_8b6f3f9ae234f137d707b98f3bf43584', + skipIfMissing: true, + }); + } + + if (isMysql) { + await schemaBuilder.dropIndex('execution_entity', ['status'], { + customIndexName: 'IDX_8b6f3f9ae234f137d707b98f3bf43584', + skipIfMissing: true, + }); + } + + // all DBs + + await schemaBuilder.dropIndex( + 'execution_entity', + ['stoppedAt'], + isSqlite ? { customIndexName: 'idx_execution_entity_stopped_at', skipIfMissing: true } : {}, + ); + await schemaBuilder.dropIndex('execution_entity', ['waitTill', 'id'], { + customIndexName: isPostgres + ? 'IDX_85b981df7b444f905f8bf50747' + : 'IDX_b94b45ce2c73ce46c54f20b5f9', + skipIfMissing: true, + }); + await schemaBuilder.dropIndex('execution_entity', ['workflowId', 'id'], { + customIndexName: + isPostgres || isMysql + ? 'idx_execution_entity_workflow_id_id' + : 'IDX_81fc04c8a17de15835713505e4', + skipIfMissing: true, + }); + } + + async down({ schemaBuilder }: MigrationContext) { + await schemaBuilder.dropIndex('execution_entity', ['workflowId', 'startedAt']); + await schemaBuilder.dropIndex('execution_entity', ['waitTill', 'status']); + await schemaBuilder.dropIndex('execution_entity', ['stoppedAt', 'deletedAt', 'status']); + + await schemaBuilder.createIndex('execution_entity', ['waitTill', 'id']); + await schemaBuilder.createIndex('execution_entity', ['stoppedAt']); + await schemaBuilder.createIndex('execution_entity', ['workflowId', 'id']); + } +} diff --git a/packages/cli/src/databases/migrations/mysqldb/index.ts b/packages/cli/src/databases/migrations/mysqldb/index.ts index ecd5f66a7c..ed08c6b2f2 100644 --- a/packages/cli/src/databases/migrations/mysqldb/index.ts +++ b/packages/cli/src/databases/migrations/mysqldb/index.ts @@ -58,7 +58,9 @@ import { MoveSshKeysToDatabase1711390882123 } from '../common/1711390882123-Move import { RemoveNodesAccess1712044305787 } from '../common/1712044305787-RemoveNodesAccess'; import { MakeExecutionStatusNonNullable1714133768521 } from '../common/1714133768521-MakeExecutionStatusNonNullable'; import { AddActivatedAtUserSetting1717498465931 } from './1717498465931-AddActivatedAtUserSetting'; +import { RefactorExecutionIndices1723796243146 } from '../common/1723796243146-RefactorExecutionIndices'; import { AddConstraintToExecutionMetadata1720101653148 } from '../common/1720101653148-AddConstraintToExecutionMetadata'; +import { CreateInvalidAuthTokenTable1723627610222 } from '../common/1723627610222-CreateInvalidAuthTokenTable'; export const mysqlMigrations: Migration[] = [ InitialMigration1588157391238, @@ -121,4 +123,6 @@ export const mysqlMigrations: Migration[] = [ MakeExecutionStatusNonNullable1714133768521, AddActivatedAtUserSetting1717498465931, AddConstraintToExecutionMetadata1720101653148, + CreateInvalidAuthTokenTable1723627610222, + RefactorExecutionIndices1723796243146, ]; diff --git a/packages/cli/src/databases/migrations/postgresdb/index.ts b/packages/cli/src/databases/migrations/postgresdb/index.ts index 720c79a8e3..3a37f36936 100644 --- a/packages/cli/src/databases/migrations/postgresdb/index.ts +++ b/packages/cli/src/databases/migrations/postgresdb/index.ts @@ -57,8 +57,10 @@ import { MoveSshKeysToDatabase1711390882123 } from '../common/1711390882123-Move import { RemoveNodesAccess1712044305787 } from '../common/1712044305787-RemoveNodesAccess'; import { MakeExecutionStatusNonNullable1714133768521 } from '../common/1714133768521-MakeExecutionStatusNonNullable'; import { AddActivatedAtUserSetting1717498465931 } from './1717498465931-AddActivatedAtUserSetting'; +import { RefactorExecutionIndices1723796243146 } from '../common/1723796243146-RefactorExecutionIndices'; import { AddConstraintToExecutionMetadata1720101653148 } from '../common/1720101653148-AddConstraintToExecutionMetadata'; import { FixExecutionMetadataSequence1721377157740 } from './1721377157740-FixExecutionMetadataSequence'; +import { CreateInvalidAuthTokenTable1723627610222 } from '../common/1723627610222-CreateInvalidAuthTokenTable'; export const postgresMigrations: Migration[] = [ InitialMigration1587669153312, @@ -121,4 +123,6 @@ export const postgresMigrations: Migration[] = [ AddActivatedAtUserSetting1717498465931, AddConstraintToExecutionMetadata1720101653148, FixExecutionMetadataSequence1721377157740, + CreateInvalidAuthTokenTable1723627610222, + RefactorExecutionIndices1723796243146, ]; diff --git a/packages/cli/src/databases/migrations/sqlite/index.ts b/packages/cli/src/databases/migrations/sqlite/index.ts index 15000a78e0..e55a36c1c1 100644 --- a/packages/cli/src/databases/migrations/sqlite/index.ts +++ b/packages/cli/src/databases/migrations/sqlite/index.ts @@ -55,7 +55,9 @@ import { MoveSshKeysToDatabase1711390882123 } from '../common/1711390882123-Move import { RemoveNodesAccess1712044305787 } from '../common/1712044305787-RemoveNodesAccess'; import { MakeExecutionStatusNonNullable1714133768521 } from '../common/1714133768521-MakeExecutionStatusNonNullable'; import { AddActivatedAtUserSetting1717498465931 } from './1717498465931-AddActivatedAtUserSetting'; +import { RefactorExecutionIndices1723796243146 } from '../common/1723796243146-RefactorExecutionIndices'; import { AddConstraintToExecutionMetadata1720101653148 } from '../common/1720101653148-AddConstraintToExecutionMetadata'; +import { CreateInvalidAuthTokenTable1723627610222 } from '../common/1723627610222-CreateInvalidAuthTokenTable'; const sqliteMigrations: Migration[] = [ InitialMigration1588102412422, @@ -115,6 +117,8 @@ const sqliteMigrations: Migration[] = [ MakeExecutionStatusNonNullable1714133768521, AddActivatedAtUserSetting1717498465931, AddConstraintToExecutionMetadata1720101653148, + CreateInvalidAuthTokenTable1723627610222, + RefactorExecutionIndices1723796243146, ]; export { sqliteMigrations }; diff --git a/packages/cli/src/databases/repositories/execution.repository.ts b/packages/cli/src/databases/repositories/execution.repository.ts index 5b4e515af6..dbcbfc994e 100644 --- a/packages/cli/src/databases/repositories/execution.repository.ts +++ b/packages/cli/src/databases/repositories/execution.repository.ts @@ -42,7 +42,7 @@ import type { ExecutionData } from '../entities/ExecutionData'; import { ExecutionEntity } from '../entities/ExecutionEntity'; import { ExecutionMetadata } from '../entities/ExecutionMetadata'; import { ExecutionDataRepository } from './executionData.repository'; -import { Logger } from '@/Logger'; +import { Logger } from '@/logger'; import type { ExecutionSummaries } from '@/executions/execution.types'; import { PostgresLiveRowsRetrievalError } from '@/errors/postgres-live-rows-retrieval.error'; import { separate } from '@/utils'; @@ -784,8 +784,8 @@ export class ExecutionRepository extends Repository { if (firstId) qb.andWhere('execution.id > :firstId', { firstId }); if (lastId) qb.andWhere('execution.id < :lastId', { lastId }); - if (query.order?.stoppedAt === 'DESC') { - qb.orderBy({ 'execution.stoppedAt': 'DESC' }); + if (query.order?.startedAt === 'DESC') { + qb.orderBy({ 'execution.startedAt': 'DESC' }); } else if (query.order?.top) { qb.orderBy(`(CASE WHEN execution.status = '${query.order.top}' THEN 0 ELSE 1 END)`); } else { diff --git a/packages/cli/src/databases/repositories/invalidAuthToken.repository.ts b/packages/cli/src/databases/repositories/invalidAuthToken.repository.ts new file mode 100644 index 0000000000..c6340ba88a --- /dev/null +++ b/packages/cli/src/databases/repositories/invalidAuthToken.repository.ts @@ -0,0 +1,10 @@ +import { Service } from 'typedi'; +import { DataSource, Repository } from '@n8n/typeorm'; +import { InvalidAuthToken } from '../entities/InvalidAuthToken'; + +@Service() +export class InvalidAuthTokenRepository extends Repository { + constructor(dataSource: DataSource) { + super(InvalidAuthToken, dataSource.manager); + } +} diff --git a/packages/cli/src/databases/repositories/settings.repository.ts b/packages/cli/src/databases/repositories/settings.repository.ts index 94cb32305a..6a745a8a70 100644 --- a/packages/cli/src/databases/repositories/settings.repository.ts +++ b/packages/cli/src/databases/repositories/settings.repository.ts @@ -1,4 +1,4 @@ -import { EXTERNAL_SECRETS_DB_KEY } from '@/ExternalSecrets/constants'; +import { EXTERNAL_SECRETS_DB_KEY } from '@/external-secrets/constants'; import { Service } from 'typedi'; import { DataSource, Repository } from '@n8n/typeorm'; import { ErrorReporterProxy as ErrorReporter } from 'n8n-workflow'; diff --git a/packages/cli/src/databases/subscribers/UserSubscriber.ts b/packages/cli/src/databases/subscribers/UserSubscriber.ts index b925965a0c..52ef95871e 100644 --- a/packages/cli/src/databases/subscribers/UserSubscriber.ts +++ b/packages/cli/src/databases/subscribers/UserSubscriber.ts @@ -2,7 +2,7 @@ import { Container } from 'typedi'; import type { EntitySubscriberInterface, UpdateEvent } from '@n8n/typeorm'; import { EventSubscriber } from '@n8n/typeorm'; import { ApplicationError, ErrorReporterProxy } from 'n8n-workflow'; -import { Logger } from '@/Logger'; +import { Logger } from '@/logger'; import { Project } from '../entities/Project'; import { User } from '../entities/User'; diff --git a/packages/cli/src/databases/types.ts b/packages/cli/src/databases/types.ts index e1dc8f20e9..96a26e4712 100644 --- a/packages/cli/src/databases/types.ts +++ b/packages/cli/src/databases/types.ts @@ -1,6 +1,6 @@ import type { INodeTypes } from 'n8n-workflow'; import type { QueryRunner, ObjectLiteral } from '@n8n/typeorm'; -import type { Logger } from '@/Logger'; +import type { Logger } from '@/logger'; import type { createSchemaBuilder } from './dsl'; export type DatabaseType = 'mariadb' | 'postgresdb' | 'mysqldb' | 'sqlite'; @@ -11,6 +11,8 @@ export interface MigrationContext { tablePrefix: string; dbType: DatabaseType; isMysql: boolean; + isSqlite: boolean; + isPostgres: boolean; dbName: string; migrationName: string; nodeTypes: INodeTypes; diff --git a/packages/cli/src/databases/utils/__tests__/customValidators.test.ts b/packages/cli/src/databases/utils/__tests__/customValidators.test.ts deleted file mode 100644 index df906b36d6..0000000000 --- a/packages/cli/src/databases/utils/__tests__/customValidators.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { NoXss } from '@db/utils/customValidators'; -import { validate } from 'class-validator'; - -describe('customValidators', () => { - describe('NoXss', () => { - class Person { - @NoXss() - name: string; - } - const person = new Person(); - - const invalidNames = ['http://google.com', '", `Jack`]; + + for (const str of XSS_STRINGS) { + test(`should block ${str}`, async () => { + entity.name = str; + const errors = await validate(entity); + expect(errors).toHaveLength(1); + const [error] = errors; + expect(error.property).toEqual('name'); + expect(error.constraints).toEqual({ NoXss: 'Potentially malicious string' }); + }); + } + }); + + describe('Names', () => { + const VALID_NAMES = [ + 'Johann Strauß', + 'Вагиф Сәмәдоғлу', + 'René Magritte', + 'সুকুমার রায়', + 'མགོན་པོ་རྡོ་རྗེ།', + 'عبدالحليم حافظ', + ]; + + for (const name of VALID_NAMES) { + test(`should allow ${name}`, async () => { + entity.name = name; + expect(await validate(entity)).toBeEmptyArray(); + }); + } + }); + + describe('ISO-8601 timestamps', () => { + const VALID_TIMESTAMPS = ['2022-01-01T00:00:00.000Z', '2022-01-01T00:00:00.000+02:00']; + + for (const timestamp of VALID_TIMESTAMPS) { + test(`should allow ${timestamp}`, async () => { + entity.timestamp = timestamp; + await expect(validate(entity)).resolves.toBeEmptyArray(); + }); + } + }); + + describe('Semver versions', () => { + const VALID_VERSIONS = ['1.0.0', '1.0.0-alpha.1']; + + for (const version of VALID_VERSIONS) { + test(`should allow ${version}`, async () => { + entity.version = version; + await expect(validate(entity)).resolves.toBeEmptyArray(); + }); + } + }); + + describe('Miscellaneous strings', () => { + const VALID_MISCELLANEOUS_STRINGS = ['CI/CD']; + + for (const str of VALID_MISCELLANEOUS_STRINGS) { + test(`should allow ${str}`, async () => { + entity.name = str; + await expect(validate(entity)).resolves.toBeEmptyArray(); + }); + } + }); + + describe('Array of strings', () => { + const VALID_STRING_ARRAYS = [ + ['cloud-infrastructure-orchestration', 'ci-cd', 'reporting'], + ['automationGoalDevops', 'cloudComputing', 'containerization'], + ]; + + for (const arr of VALID_STRING_ARRAYS) { + test(`should allow array: ${JSON.stringify(arr)}`, async () => { + entity.categories = arr; + await expect(validate(entity)).resolves.toBeEmptyArray(); + }); + } + + const INVALID_STRING_ARRAYS = [ + ['valid-string', '', 'another-valid-string'], + ['', 'valid-string'], + ]; + + for (const arr of INVALID_STRING_ARRAYS) { + test(`should reject array containing invalid string: ${JSON.stringify(arr)}`, async () => { + entity.categories = arr; + const errors = await validate(entity); + expect(errors).toHaveLength(1); + const [error] = errors; + expect(error.property).toEqual('categories'); + expect(error.constraints).toEqual({ NoXss: 'Potentially malicious string' }); + }); + } + }); +}); diff --git a/packages/cli/src/validators/no-url.validator.ts b/packages/cli/src/validators/no-url.validator.ts new file mode 100644 index 0000000000..0cdacaddc1 --- /dev/null +++ b/packages/cli/src/validators/no-url.validator.ts @@ -0,0 +1,27 @@ +import type { ValidationOptions, ValidatorConstraintInterface } from 'class-validator'; +import { registerDecorator, ValidatorConstraint } from 'class-validator'; + +const URL_REGEX = /^(https?:\/\/|www\.)|(\.[\p{L}\d-]+)/iu; + +@ValidatorConstraint({ name: 'NoUrl', async: false }) +class NoUrlConstraint implements ValidatorConstraintInterface { + validate(value: string) { + return !URL_REGEX.test(value); + } + + defaultMessage() { + return 'Potentially malicious string'; + } +} + +export function NoUrl(options?: ValidationOptions) { + return function (object: object, propertyName: string) { + registerDecorator({ + name: 'NoUrl', + target: object.constructor, + propertyName, + options, + validator: NoUrlConstraint, + }); + }; +} diff --git a/packages/cli/src/validators/no-xss.validator.ts b/packages/cli/src/validators/no-xss.validator.ts new file mode 100644 index 0000000000..69960c39dd --- /dev/null +++ b/packages/cli/src/validators/no-xss.validator.ts @@ -0,0 +1,33 @@ +import xss from 'xss'; +import type { ValidationOptions, ValidatorConstraintInterface } from 'class-validator'; +import { registerDecorator, ValidatorConstraint } from 'class-validator'; + +@ValidatorConstraint({ name: 'NoXss', async: false }) +class NoXssConstraint implements ValidatorConstraintInterface { + validate(value: unknown) { + if (typeof value !== 'string') return false; + + return ( + value === + xss(value, { + whiteList: {}, // no tags are allowed + }) + ); + } + + defaultMessage() { + return 'Potentially malicious string'; + } +} + +export function NoXss(options?: ValidationOptions) { + return function (object: object, propertyName: string) { + registerDecorator({ + name: 'NoXss', + target: object.constructor, + propertyName, + options, + validator: NoXssConstraint, + }); + }; +} diff --git a/packages/cli/src/WaitTracker.ts b/packages/cli/src/wait-tracker.ts similarity index 98% rename from packages/cli/src/WaitTracker.ts rename to packages/cli/src/wait-tracker.ts index 5050d80ba9..eec9cb64b1 100644 --- a/packages/cli/src/WaitTracker.ts +++ b/packages/cli/src/wait-tracker.ts @@ -1,10 +1,10 @@ import { ApplicationError, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow'; import { Service } from 'typedi'; import type { IWorkflowExecutionDataProcess } from '@/Interfaces'; -import { WorkflowRunner } from '@/WorkflowRunner'; +import { WorkflowRunner } from '@/workflow-runner'; import { ExecutionRepository } from '@db/repositories/execution.repository'; import { OwnershipService } from '@/services/ownership.service'; -import { Logger } from '@/Logger'; +import { Logger } from '@/logger'; import { OrchestrationService } from '@/services/orchestration.service'; @Service() diff --git a/packages/cli/src/WaitingForms.ts b/packages/cli/src/waiting-forms.ts similarity index 89% rename from packages/cli/src/WaitingForms.ts rename to packages/cli/src/waiting-forms.ts index bf0ab7dedb..2fc1655594 100644 --- a/packages/cli/src/WaitingForms.ts +++ b/packages/cli/src/waiting-forms.ts @@ -1,7 +1,7 @@ import { Service } from 'typedi'; import type { IExecutionResponse } from '@/Interfaces'; -import { WaitingWebhooks } from '@/webhooks/WaitingWebhooks'; +import { WaitingWebhooks } from '@/webhooks/waiting-webhooks'; @Service() export class WaitingForms extends WaitingWebhooks { diff --git a/packages/cli/src/webhooks/__tests__/TestWebhooks.test.ts b/packages/cli/src/webhooks/__tests__/TestWebhooks.test.ts index deb6930b96..292b20e1bd 100644 --- a/packages/cli/src/webhooks/__tests__/TestWebhooks.test.ts +++ b/packages/cli/src/webhooks/__tests__/TestWebhooks.test.ts @@ -1,10 +1,10 @@ import { mock } from 'jest-mock-extended'; -import { TestWebhooks } from '@/webhooks/TestWebhooks'; +import { TestWebhooks } from '@/webhooks/test-webhooks'; import { WebhookNotFoundError } from '@/errors/response-errors/webhook-not-found.error'; import { v4 as uuid } from 'uuid'; import { generateNanoId } from '@/databases/utils/generators'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; -import * as WebhookHelpers from '@/webhooks/WebhookHelpers'; +import * as WebhookHelpers from '@/webhooks/webhook-helpers'; import type * as express from 'express'; import type { IWorkflowDb } from '@/Interfaces'; @@ -14,10 +14,10 @@ import type { TestWebhookRegistration, } from '@/webhooks/test-webhook-registrations.service'; -import * as AdditionalData from '@/WorkflowExecuteAdditionalData'; +import * as AdditionalData from '@/workflow-execute-additional-data'; import type { WebhookRequest } from '@/webhooks/webhook.types'; -jest.mock('@/WorkflowExecuteAdditionalData'); +jest.mock('@/workflow-execute-additional-data'); const mockedAdditionalData = AdditionalData as jest.Mocked; diff --git a/packages/cli/src/webhooks/__tests__/WebhookRequestHandler.test.ts b/packages/cli/src/webhooks/__tests__/WebhookRequestHandler.test.ts index aff3102419..c807c6c4d9 100644 --- a/packages/cli/src/webhooks/__tests__/WebhookRequestHandler.test.ts +++ b/packages/cli/src/webhooks/__tests__/WebhookRequestHandler.test.ts @@ -9,7 +9,7 @@ import type { WebhookOptionsRequest, WebhookRequest, } from '@/webhooks/webhook.types'; -import { createWebhookHandlerFor } from '@/webhooks/WebhookRequestHandler'; +import { createWebhookHandlerFor } from '@/webhooks/webhook-request-handler'; import { ResponseError } from '@/errors/response-errors/abstract/response.error'; describe('WebhookRequestHandler', () => { diff --git a/packages/cli/src/webhooks/__tests__/waiting-webhooks.test.ts b/packages/cli/src/webhooks/__tests__/waiting-webhooks.test.ts index 31f64eb198..5cb8e005ad 100644 --- a/packages/cli/src/webhooks/__tests__/waiting-webhooks.test.ts +++ b/packages/cli/src/webhooks/__tests__/waiting-webhooks.test.ts @@ -1,5 +1,5 @@ import { mock } from 'jest-mock-extended'; -import { WaitingWebhooks } from '@/webhooks/WaitingWebhooks'; +import { WaitingWebhooks } from '@/webhooks/waiting-webhooks'; import { ConflictError } from '@/errors/response-errors/conflict.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import type { IExecutionResponse } from '@/Interfaces'; diff --git a/packages/cli/src/webhooks/LiveWebhooks.ts b/packages/cli/src/webhooks/live-webhooks.ts similarity index 94% rename from packages/cli/src/webhooks/LiveWebhooks.ts rename to packages/cli/src/webhooks/live-webhooks.ts index 3d66b7ee7f..1db85e2315 100644 --- a/packages/cli/src/webhooks/LiveWebhooks.ts +++ b/packages/cli/src/webhooks/live-webhooks.ts @@ -10,14 +10,14 @@ import type { WebhookAccessControlOptions, WebhookRequest, } from './webhook.types'; -import { Logger } from '@/Logger'; -import { NodeTypes } from '@/NodeTypes'; +import { Logger } from '@/logger'; +import { NodeTypes } from '@/node-types'; import { WebhookService } from '@/webhooks/webhook.service'; import { WebhookNotFoundError } from '@/errors/response-errors/webhook-not-found.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; -import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'; -import * as WebhookHelpers from '@/webhooks/WebhookHelpers'; -import { WorkflowStaticDataService } from '@/workflows/workflowStaticData.service'; +import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data'; +import * as WebhookHelpers from '@/webhooks/webhook-helpers'; +import { WorkflowStaticDataService } from '@/workflows/workflow-static-data.service'; /** * Service for handling the execution of live webhooks, i.e. webhooks diff --git a/packages/cli/src/webhooks/TestWebhooks.ts b/packages/cli/src/webhooks/test-webhooks.ts similarity index 98% rename from packages/cli/src/webhooks/TestWebhooks.ts rename to packages/cli/src/webhooks/test-webhooks.ts index 8cf1b42929..e10b77aa24 100644 --- a/packages/cli/src/webhooks/TestWebhooks.ts +++ b/packages/cli/src/webhooks/test-webhooks.ts @@ -14,8 +14,8 @@ import type { WebhookRequest, } from './webhook.types'; import { Push } from '@/push'; -import { NodeTypes } from '@/NodeTypes'; -import * as WebhookHelpers from '@/webhooks/WebhookHelpers'; +import { NodeTypes } from '@/node-types'; +import * as WebhookHelpers from '@/webhooks/webhook-helpers'; import { TEST_WEBHOOK_TIMEOUT } from '@/constants'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { WorkflowMissingIdError } from '@/errors/workflow-missing-id.error'; @@ -25,7 +25,7 @@ import { removeTrailingSlash } from '@/utils'; import type { TestWebhookRegistration } from '@/webhooks/test-webhook-registrations.service'; import { TestWebhookRegistrationsService } from '@/webhooks/test-webhook-registrations.service'; import { OrchestrationService } from '@/services/orchestration.service'; -import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'; +import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data'; import type { IWorkflowDb } from '@/Interfaces'; /** diff --git a/packages/cli/src/webhooks/WaitingWebhooks.ts b/packages/cli/src/webhooks/waiting-webhooks.ts similarity index 95% rename from packages/cli/src/webhooks/WaitingWebhooks.ts rename to packages/cli/src/webhooks/waiting-webhooks.ts index 367635c45e..f2c5706817 100644 --- a/packages/cli/src/webhooks/WaitingWebhooks.ts +++ b/packages/cli/src/webhooks/waiting-webhooks.ts @@ -2,16 +2,16 @@ import { NodeHelpers, Workflow } from 'n8n-workflow'; import { Service } from 'typedi'; import type express from 'express'; -import * as WebhookHelpers from '@/webhooks/WebhookHelpers'; -import { NodeTypes } from '@/NodeTypes'; +import * as WebhookHelpers from '@/webhooks/webhook-helpers'; +import { NodeTypes } from '@/node-types'; import type { IWebhookResponseCallbackData, IWebhookManager, WaitingWebhookRequest, } from './webhook.types'; -import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'; +import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data'; import { ExecutionRepository } from '@db/repositories/execution.repository'; -import { Logger } from '@/Logger'; +import { Logger } from '@/logger'; import { ConflictError } from '@/errors/response-errors/conflict.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import type { IExecutionResponse, IWorkflowDb } from '@/Interfaces'; diff --git a/packages/cli/src/webhooks/WebhookHelpers.ts b/packages/cli/src/webhooks/webhook-helpers.ts similarity index 98% rename from packages/cli/src/webhooks/WebhookHelpers.ts rename to packages/cli/src/webhooks/webhook-helpers.ts index 1b8bc2b7e9..725f0339a1 100644 --- a/packages/cli/src/webhooks/WebhookHelpers.ts +++ b/packages/cli/src/webhooks/webhook-helpers.ts @@ -41,14 +41,14 @@ import { } from 'n8n-workflow'; import type { IWebhookResponseCallbackData, WebhookRequest } from './webhook.types'; -import * as WorkflowHelpers from '@/WorkflowHelpers'; -import { WorkflowRunner } from '@/WorkflowRunner'; -import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'; -import { ActiveExecutions } from '@/ActiveExecutions'; +import * as WorkflowHelpers from '@/workflow-helpers'; +import { WorkflowRunner } from '@/workflow-runner'; +import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data'; +import { ActiveExecutions } from '@/active-executions'; import { WorkflowStatisticsService } from '@/services/workflow-statistics.service'; import { OwnershipService } from '@/services/ownership.service'; import { parseBody } from '@/middlewares'; -import { Logger } from '@/Logger'; +import { Logger } from '@/logger'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { InternalServerError } from '@/errors/response-errors/internal-server.error'; import { UnprocessableRequestError } from '@/errors/response-errors/unprocessable.error'; diff --git a/packages/cli/src/webhooks/WebhookRequestHandler.ts b/packages/cli/src/webhooks/webhook-request-handler.ts similarity index 98% rename from packages/cli/src/webhooks/WebhookRequestHandler.ts rename to packages/cli/src/webhooks/webhook-request-handler.ts index 289175ac05..a495873a68 100644 --- a/packages/cli/src/webhooks/WebhookRequestHandler.ts +++ b/packages/cli/src/webhooks/webhook-request-handler.ts @@ -5,7 +5,7 @@ import type { WebhookOptionsRequest, WebhookRequest, } from '@/webhooks/webhook.types'; -import * as ResponseHelper from '@/ResponseHelper'; +import * as ResponseHelper from '@/response-helper'; const WEBHOOK_METHODS: IHttpRequestMethods[] = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT']; diff --git a/packages/cli/src/webhooks/WebhookServer.ts b/packages/cli/src/webhooks/webhook-server.ts similarity index 72% rename from packages/cli/src/webhooks/WebhookServer.ts rename to packages/cli/src/webhooks/webhook-server.ts index 60f59f606d..bb00510e56 100644 --- a/packages/cli/src/webhooks/WebhookServer.ts +++ b/packages/cli/src/webhooks/webhook-server.ts @@ -1,5 +1,5 @@ import { Service } from 'typedi'; -import { AbstractServer } from '@/AbstractServer'; +import { AbstractServer } from '@/abstract-server'; @Service() export class WebhookServer extends AbstractServer { diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/workflow-execute-additional-data.ts similarity index 97% rename from packages/cli/src/WorkflowExecuteAdditionalData.ts rename to packages/cli/src/workflow-execute-additional-data.ts index 127e0cc817..459c946926 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/workflow-execute-additional-data.ts @@ -36,9 +36,9 @@ import { import { Container } from 'typedi'; import config from '@/config'; -import { ActiveExecutions } from '@/ActiveExecutions'; -import { CredentialsHelper } from '@/CredentialsHelper'; -import { ExternalHooks } from '@/ExternalHooks'; +import { ActiveExecutions } from '@/active-executions'; +import { CredentialsHelper } from '@/credentials-helper'; +import { ExternalHooks } from '@/external-hooks'; import type { IPushDataExecutionFinished, IWorkflowExecuteProcess, @@ -47,28 +47,28 @@ import type { IPushDataType, ExecutionPayload, } from '@/Interfaces'; -import { NodeTypes } from '@/NodeTypes'; +import { NodeTypes } from '@/node-types'; import { Push } from '@/push'; -import * as WorkflowHelpers from '@/WorkflowHelpers'; +import * as WorkflowHelpers from '@/workflow-helpers'; import { findSubworkflowStart, isWorkflowIdValid } from '@/utils'; -import { PermissionChecker } from './UserManagement/PermissionChecker'; +import { PermissionChecker } from './user-management/permission-checker'; import { ExecutionRepository } from '@db/repositories/execution.repository'; import { WorkflowStatisticsService } from '@/services/workflow-statistics.service'; -import { SecretsHelper } from './SecretsHelpers'; +import { SecretsHelper } from './secrets-helpers'; import { OwnershipService } from './services/ownership.service'; import { determineFinalExecutionStatus, prepareExecutionDataForDbUpdate, updateExistingExecution, -} from './executionLifecycleHooks/shared/sharedHookFunctions'; -import { restoreBinaryDataId } from './executionLifecycleHooks/restoreBinaryDataId'; -import { toSaveSettings } from './executionLifecycleHooks/toSaveSettings'; -import { Logger } from './Logger'; -import { saveExecutionProgress } from './executionLifecycleHooks/saveExecutionProgress'; -import { WorkflowStaticDataService } from './workflows/workflowStaticData.service'; +} from './execution-lifecycle-hooks/shared/shared-hook-functions'; +import { restoreBinaryDataId } from './execution-lifecycle-hooks/restore-binary-data-id'; +import { toSaveSettings } from './execution-lifecycle-hooks/to-save-settings'; +import { Logger } from './logger'; +import { saveExecutionProgress } from './execution-lifecycle-hooks/save-execution-progress'; +import { WorkflowStaticDataService } from './workflows/workflow-static-data.service'; import { WorkflowRepository } from './databases/repositories/workflow.repository'; import { UrlService } from './services/url.service'; -import { WorkflowExecutionService } from './workflows/workflowExecution.service'; +import { WorkflowExecutionService } from './workflows/workflow-execution.service'; import { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus'; import { EventService } from './events/event.service'; import { GlobalConfig } from '@n8n/config'; diff --git a/packages/cli/src/WorkflowHelpers.ts b/packages/cli/src/workflow-helpers.ts similarity index 100% rename from packages/cli/src/WorkflowHelpers.ts rename to packages/cli/src/workflow-helpers.ts diff --git a/packages/cli/src/WorkflowRunner.ts b/packages/cli/src/workflow-runner.ts similarity index 97% rename from packages/cli/src/WorkflowRunner.ts rename to packages/cli/src/workflow-runner.ts index 9ff1de8f35..b8012b3d4f 100644 --- a/packages/cli/src/WorkflowRunner.ts +++ b/packages/cli/src/workflow-runner.ts @@ -22,20 +22,20 @@ import { import PCancelable from 'p-cancelable'; -import { ActiveExecutions } from '@/ActiveExecutions'; +import { ActiveExecutions } from '@/active-executions'; import config from '@/config'; import { ExecutionRepository } from '@db/repositories/execution.repository'; -import { ExternalHooks } from '@/ExternalHooks'; +import { ExternalHooks } from '@/external-hooks'; import type { IExecutionResponse, IWorkflowExecutionDataProcess } from '@/Interfaces'; -import { NodeTypes } from '@/NodeTypes'; +import { NodeTypes } from '@/node-types'; import type { Job, JobData, JobResult } from '@/scaling/types'; import type { ScalingService } from '@/scaling/scaling.service'; -import * as WorkflowHelpers from '@/WorkflowHelpers'; -import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'; -import { generateFailedExecutionFromError } from '@/WorkflowHelpers'; -import { PermissionChecker } from '@/UserManagement/PermissionChecker'; -import { Logger } from '@/Logger'; -import { WorkflowStaticDataService } from '@/workflows/workflowStaticData.service'; +import * as WorkflowHelpers from '@/workflow-helpers'; +import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data'; +import { generateFailedExecutionFromError } from '@/workflow-helpers'; +import { PermissionChecker } from '@/user-management/permission-checker'; +import { Logger } from '@/logger'; +import { WorkflowStaticDataService } from '@/workflows/workflow-static-data.service'; import { EventService } from './events/event.service'; import { GlobalConfig } from '@n8n/config'; diff --git a/packages/cli/src/workflows/__tests__/workflow-execution.service.test.ts b/packages/cli/src/workflows/__tests__/workflow-execution.service.test.ts index d06dde35e2..b56cbffbbd 100644 --- a/packages/cli/src/workflows/__tests__/workflow-execution.service.test.ts +++ b/packages/cli/src/workflows/__tests__/workflow-execution.service.test.ts @@ -3,8 +3,8 @@ import { mock } from 'jest-mock-extended'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import type { IWorkflowDb } from '@/Interfaces'; -import { WorkflowExecutionService } from '@/workflows/workflowExecution.service'; -import type { WorkflowRunner } from '@/WorkflowRunner'; +import { WorkflowExecutionService } from '@/workflows/workflow-execution.service'; +import type { WorkflowRunner } from '@/workflow-runner'; const webhookNode: INode = { name: 'Webhook', diff --git a/packages/cli/src/workflows/workflowExecution.service.ts b/packages/cli/src/workflows/workflow-execution.service.ts similarity index 96% rename from packages/cli/src/workflows/workflowExecution.service.ts rename to packages/cli/src/workflows/workflow-execution.service.ts index 9ddce37d2f..3611684ef1 100644 --- a/packages/cli/src/workflows/workflowExecution.service.ts +++ b/packages/cli/src/workflows/workflow-execution.service.ts @@ -19,7 +19,7 @@ import { import type { User } from '@db/entities/User'; import { ExecutionRepository } from '@db/repositories/execution.repository'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; -import * as WorkflowHelpers from '@/WorkflowHelpers'; +import * as WorkflowHelpers from '@/workflow-helpers'; import type { WorkflowRequest } from '@/workflows/workflow.request'; import type { ExecutionPayload, @@ -27,11 +27,11 @@ import type { IWorkflowErrorData, IWorkflowExecutionDataProcess, } from '@/Interfaces'; -import { NodeTypes } from '@/NodeTypes'; -import { WorkflowRunner } from '@/WorkflowRunner'; -import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'; -import { TestWebhooks } from '@/webhooks/TestWebhooks'; -import { Logger } from '@/Logger'; +import { NodeTypes } from '@/node-types'; +import { WorkflowRunner } from '@/workflow-runner'; +import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data'; +import { TestWebhooks } from '@/webhooks/test-webhooks'; +import { Logger } from '@/logger'; import type { Project } from '@/databases/entities/Project'; import { GlobalConfig } from '@n8n/config'; import { SubworkflowPolicyChecker } from '@/subworkflows/subworkflow-policy-checker.service'; diff --git a/packages/cli/src/workflows/workflowHistory/__tests__/workflowHistoryHelper.ee.test.ts b/packages/cli/src/workflows/workflow-history/__tests__/workflow-history-helper.ee.test.ts similarity index 94% rename from packages/cli/src/workflows/workflowHistory/__tests__/workflowHistoryHelper.ee.test.ts rename to packages/cli/src/workflows/workflow-history/__tests__/workflow-history-helper.ee.test.ts index 427f981884..a4b7a70abf 100644 --- a/packages/cli/src/workflows/workflowHistory/__tests__/workflowHistoryHelper.ee.test.ts +++ b/packages/cli/src/workflows/workflow-history/__tests__/workflow-history-helper.ee.test.ts @@ -1,6 +1,6 @@ -import { License } from '@/License'; +import { License } from '@/license'; import config from '@/config'; -import { getWorkflowHistoryPruneTime } from '@/workflows/workflowHistory/workflowHistoryHelper.ee'; +import { getWorkflowHistoryPruneTime } from '@/workflows/workflow-history/workflow-history-helper.ee'; import { mockInstance } from '@test/mocking'; let licensePruneTime = -1; diff --git a/packages/cli/src/workflows/workflowHistory/__tests__/workflowHistory.service.ee.test.ts b/packages/cli/src/workflows/workflow-history/__tests__/workflow-history.service.ee.test.ts similarity index 94% rename from packages/cli/src/workflows/workflowHistory/__tests__/workflowHistory.service.ee.test.ts rename to packages/cli/src/workflows/workflow-history/__tests__/workflow-history.service.ee.test.ts index 5b28b8d171..0417908d1c 100644 --- a/packages/cli/src/workflows/workflowHistory/__tests__/workflowHistory.service.ee.test.ts +++ b/packages/cli/src/workflows/workflow-history/__tests__/workflow-history.service.ee.test.ts @@ -2,8 +2,8 @@ import { mockClear } from 'jest-mock-extended'; import { User } from '@db/entities/User'; import { WorkflowHistoryRepository } from '@db/repositories/workflowHistory.repository'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; -import { WorkflowHistoryService } from '@/workflows/workflowHistory/workflowHistory.service.ee'; -import { Logger } from '@/Logger'; +import { WorkflowHistoryService } from '@/workflows/workflow-history/workflow-history.service.ee'; +import { Logger } from '@/logger'; import { mockInstance } from '@test/mocking'; import { getWorkflow } from '@test-integration/workflow'; @@ -24,7 +24,7 @@ const testUser = Object.assign(new User(), { }); let isWorkflowHistoryEnabled = true; -jest.mock('@/workflows/workflowHistory/workflowHistoryHelper.ee', () => { +jest.mock('@/workflows/workflow-history/workflow-history-helper.ee', () => { return { isWorkflowHistoryEnabled: jest.fn(() => isWorkflowHistoryEnabled), }; diff --git a/packages/cli/src/workflows/workflowHistory/constants.ts b/packages/cli/src/workflows/workflow-history/constants.ts similarity index 100% rename from packages/cli/src/workflows/workflowHistory/constants.ts rename to packages/cli/src/workflows/workflow-history/constants.ts diff --git a/packages/cli/src/workflows/workflowHistory/workflowHistoryHelper.ee.ts b/packages/cli/src/workflows/workflow-history/workflow-history-helper.ee.ts similarity index 96% rename from packages/cli/src/workflows/workflowHistory/workflowHistoryHelper.ee.ts rename to packages/cli/src/workflows/workflow-history/workflow-history-helper.ee.ts index bf784c5aba..27da19b7da 100644 --- a/packages/cli/src/workflows/workflowHistory/workflowHistoryHelper.ee.ts +++ b/packages/cli/src/workflows/workflow-history/workflow-history-helper.ee.ts @@ -1,4 +1,4 @@ -import { License } from '@/License'; +import { License } from '@/license'; import config from '@/config'; import Container from 'typedi'; diff --git a/packages/cli/src/workflows/workflowHistory/workflowHistoryManager.ee.ts b/packages/cli/src/workflows/workflow-history/workflow-history-manager.ee.ts similarity index 90% rename from packages/cli/src/workflows/workflowHistory/workflowHistoryManager.ee.ts rename to packages/cli/src/workflows/workflow-history/workflow-history-manager.ee.ts index 4898666c88..d6eb214d8c 100644 --- a/packages/cli/src/workflows/workflowHistory/workflowHistoryManager.ee.ts +++ b/packages/cli/src/workflows/workflow-history/workflow-history-manager.ee.ts @@ -2,7 +2,10 @@ import { Service } from 'typedi'; import { DateTime } from 'luxon'; import { WorkflowHistoryRepository } from '@db/repositories/workflowHistory.repository'; import { WORKFLOW_HISTORY_PRUNE_INTERVAL } from './constants'; -import { getWorkflowHistoryPruneTime, isWorkflowHistoryEnabled } from './workflowHistoryHelper.ee'; +import { + getWorkflowHistoryPruneTime, + isWorkflowHistoryEnabled, +} from './workflow-history-helper.ee'; @Service() export class WorkflowHistoryManager { diff --git a/packages/cli/src/workflows/workflowHistory/workflowHistory.controller.ee.ts b/packages/cli/src/workflows/workflow-history/workflow-history.controller.ee.ts similarity index 94% rename from packages/cli/src/workflows/workflowHistory/workflowHistory.controller.ee.ts rename to packages/cli/src/workflows/workflow-history/workflow-history.controller.ee.ts index c57e3cb0b5..dd90037554 100644 --- a/packages/cli/src/workflows/workflowHistory/workflowHistory.controller.ee.ts +++ b/packages/cli/src/workflows/workflow-history/workflow-history.controller.ee.ts @@ -1,10 +1,10 @@ import { RestController, Get, Middleware } from '@/decorators'; import { WorkflowHistoryRequest } from '@/requests'; -import { WorkflowHistoryService } from './workflowHistory.service.ee'; +import { WorkflowHistoryService } from './workflow-history.service.ee'; import { Request, Response, NextFunction } from 'express'; -import { isWorkflowHistoryEnabled, isWorkflowHistoryLicensed } from './workflowHistoryHelper.ee'; +import { isWorkflowHistoryEnabled, isWorkflowHistoryLicensed } from './workflow-history-helper.ee'; -import { paginationListQueryMiddleware } from '@/middlewares/listQuery/pagination'; +import { paginationListQueryMiddleware } from '@/middlewares/list-query/pagination'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { SharedWorkflowNotFoundError } from '@/errors/shared-workflow-not-found.error'; import { WorkflowHistoryVersionNotFoundError } from '@/errors/workflow-history-version-not-found.error'; diff --git a/packages/cli/src/workflows/workflowHistory/workflowHistory.service.ee.ts b/packages/cli/src/workflows/workflow-history/workflow-history.service.ee.ts similarity index 96% rename from packages/cli/src/workflows/workflowHistory/workflowHistory.service.ee.ts rename to packages/cli/src/workflows/workflow-history/workflow-history.service.ee.ts index b92fc440cc..55cb085f42 100644 --- a/packages/cli/src/workflows/workflowHistory/workflowHistory.service.ee.ts +++ b/packages/cli/src/workflows/workflow-history/workflow-history.service.ee.ts @@ -4,8 +4,8 @@ import type { WorkflowHistory } from '@db/entities/WorkflowHistory'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { WorkflowHistoryRepository } from '@db/repositories/workflowHistory.repository'; import { Service } from 'typedi'; -import { isWorkflowHistoryEnabled } from './workflowHistoryHelper.ee'; -import { Logger } from '@/Logger'; +import { isWorkflowHistoryEnabled } from './workflow-history-helper.ee'; +import { Logger } from '@/logger'; import { SharedWorkflowNotFoundError } from '@/errors/shared-workflow-not-found.error'; import { WorkflowHistoryVersionNotFoundError } from '@/errors/workflow-history-version-not-found.error'; diff --git a/packages/cli/src/workflows/workflowSharing.service.ts b/packages/cli/src/workflows/workflow-sharing.service.ts similarity index 100% rename from packages/cli/src/workflows/workflowSharing.service.ts rename to packages/cli/src/workflows/workflow-sharing.service.ts diff --git a/packages/cli/src/workflows/workflowStaticData.service.ts b/packages/cli/src/workflows/workflow-static-data.service.ts similarity index 98% rename from packages/cli/src/workflows/workflowStaticData.service.ts rename to packages/cli/src/workflows/workflow-static-data.service.ts index 4c981acb87..d9c3564b26 100644 --- a/packages/cli/src/workflows/workflowStaticData.service.ts +++ b/packages/cli/src/workflows/workflow-static-data.service.ts @@ -1,7 +1,7 @@ import { Service } from 'typedi'; import { GlobalConfig } from '@n8n/config'; import { type IDataObject, type Workflow, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow'; -import { Logger } from '@/Logger'; +import { Logger } from '@/logger'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { isWorkflowIdValid } from '@/utils'; diff --git a/packages/cli/src/workflows/workflow.service.ee.ts b/packages/cli/src/workflows/workflow.service.ee.ts index 31029464f6..e3aae3a4e9 100644 --- a/packages/cli/src/workflows/workflow.service.ee.ts +++ b/packages/cli/src/workflows/workflow.service.ee.ts @@ -11,7 +11,7 @@ import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.reposi import { CredentialsService } from '@/credentials/credentials.service'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; -import { Logger } from '@/Logger'; +import { Logger } from '@/logger'; import type { WorkflowWithSharingsAndCredentials, WorkflowWithSharingsMetaDataAndCredentials, @@ -21,7 +21,7 @@ import { OwnershipService } from '@/services/ownership.service'; import { In, type EntityManager } from '@n8n/typeorm'; import { Project } from '@/databases/entities/Project'; import { ProjectService } from '@/services/project.service'; -import { ActiveWorkflowManager } from '@/ActiveWorkflowManager'; +import { ActiveWorkflowManager } from '@/active-workflow-manager'; import { TransferWorkflowError } from '@/errors/response-errors/transfer-workflow.error'; import { SharedWorkflow } from '@/databases/entities/SharedWorkflow'; diff --git a/packages/cli/src/workflows/workflow.service.ts b/packages/cli/src/workflows/workflow.service.ts index 20917e60cb..fb0619726f 100644 --- a/packages/cli/src/workflows/workflow.service.ts +++ b/packages/cli/src/workflows/workflow.service.ts @@ -11,20 +11,20 @@ import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { WorkflowTagMappingRepository } from '@db/repositories/workflowTagMapping.repository'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; -import { ActiveWorkflowManager } from '@/ActiveWorkflowManager'; -import * as WorkflowHelpers from '@/WorkflowHelpers'; -import { validateEntity } from '@/GenericHelpers'; -import { ExternalHooks } from '@/ExternalHooks'; +import { ActiveWorkflowManager } from '@/active-workflow-manager'; +import * as WorkflowHelpers from '@/workflow-helpers'; +import { validateEntity } from '@/generic-helpers'; +import { ExternalHooks } from '@/external-hooks'; import { hasSharing, type ListQuery } from '@/requests'; import { TagService } from '@/services/tag.service'; import { OwnershipService } from '@/services/ownership.service'; -import { WorkflowHistoryService } from './workflowHistory/workflowHistory.service.ee'; -import { Logger } from '@/Logger'; +import { WorkflowHistoryService } from './workflow-history/workflow-history.service.ee'; +import { Logger } from '@/logger'; import { OrchestrationService } from '@/services/orchestration.service'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { RoleService } from '@/services/role.service'; -import { WorkflowSharingService } from './workflowSharing.service'; +import { WorkflowSharingService } from './workflow-sharing.service'; import { ProjectService } from '@/services/project.service'; import { ExecutionRepository } from '@/databases/repositories/execution.repository'; import type { Scope } from '@n8n/permissions'; diff --git a/packages/cli/src/workflows/workflows.controller.ts b/packages/cli/src/workflows/workflows.controller.ts index 4c03dd9e24..821bddd295 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -3,8 +3,8 @@ import { v4 as uuid } from 'uuid'; import axios from 'axios'; import * as Db from '@/Db'; -import * as ResponseHelper from '@/ResponseHelper'; -import * as WorkflowHelpers from '@/WorkflowHelpers'; +import * as ResponseHelper from '@/response-helper'; +import * as WorkflowHelpers from '@/workflow-helpers'; import type { IWorkflowResponse } from '@/Interfaces'; import config from '@/config'; import { Delete, Get, Patch, Post, ProjectScope, Put, RestController } from '@/decorators'; @@ -13,15 +13,15 @@ import { WorkflowEntity } from '@db/entities/WorkflowEntity'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { TagRepository } from '@db/repositories/tag.repository'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; -import { validateEntity } from '@/GenericHelpers'; -import { ExternalHooks } from '@/ExternalHooks'; +import { validateEntity } from '@/generic-helpers'; +import { ExternalHooks } from '@/external-hooks'; import { WorkflowService } from './workflow.service'; -import { License } from '@/License'; +import { License } from '@/license'; import * as utils from '@/utils'; import { listQueryMiddleware } from '@/middlewares'; import { TagService } from '@/services/tag.service'; -import { WorkflowHistoryService } from './workflowHistory/workflowHistory.service.ee'; -import { Logger } from '@/Logger'; +import { WorkflowHistoryService } from './workflow-history/workflow-history.service.ee'; +import { Logger } from '@/logger'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { InternalServerError } from '@/errors/response-errors/internal-server.error'; @@ -31,8 +31,8 @@ import { UserOnboardingService } from '@/services/userOnboarding.service'; import { CredentialsService } from '../credentials/credentials.service'; import { WorkflowRequest } from './workflow.request'; import { EnterpriseWorkflowService } from './workflow.service.ee'; -import { WorkflowExecutionService } from './workflowExecution.service'; -import { UserManagementMailer } from '@/UserManagement/email'; +import { WorkflowExecutionService } from './workflow-execution.service'; +import { UserManagementMailer } from '@/user-management/email'; import { ProjectRepository } from '@/databases/repositories/project.repository'; import { ProjectService } from '@/services/project.service'; import { ApplicationError } from 'n8n-workflow'; diff --git a/packages/cli/test/integration/CredentialsHelper.test.ts b/packages/cli/test/integration/CredentialsHelper.test.ts index 88738c3c26..df6c079960 100644 --- a/packages/cli/test/integration/CredentialsHelper.test.ts +++ b/packages/cli/test/integration/CredentialsHelper.test.ts @@ -1,7 +1,7 @@ import Container from 'typedi'; import * as testDb from '../integration/shared/testDb'; -import { CredentialsHelper } from '@/CredentialsHelper'; +import { CredentialsHelper } from '@/credentials-helper'; import { createOwner, createAdmin, createMember } from './shared/db/users'; import type { User } from '@/databases/entities/User'; import { saveCredential } from './shared/db/credentials'; diff --git a/packages/cli/test/integration/ExternalSecrets/externalSecrets.api.test.ts b/packages/cli/test/integration/ExternalSecrets/externalSecrets.api.test.ts index 190ab437fa..f6f448bf16 100644 --- a/packages/cli/test/integration/ExternalSecrets/externalSecrets.api.test.ts +++ b/packages/cli/test/integration/ExternalSecrets/externalSecrets.api.test.ts @@ -3,12 +3,12 @@ import { Cipher } from 'n8n-core'; import { jsonParse, type IDataObject } from 'n8n-workflow'; import { mock } from 'jest-mock-extended'; -import { License } from '@/License'; +import { License } from '@/license'; import type { ExternalSecretsSettings, SecretsProviderState } from '@/Interfaces'; import { SettingsRepository } from '@db/repositories/settings.repository'; -import { ExternalSecretsProviders } from '@/ExternalSecrets/ExternalSecretsProviders.ee'; +import { ExternalSecretsProviders } from '@/external-secrets/external-secrets-providers.ee'; import config from '@/config'; -import { ExternalSecretsManager } from '@/ExternalSecrets/ExternalSecretsManager.ee'; +import { ExternalSecretsManager } from '@/external-secrets/external-secrets-manager.ee'; import { CREDENTIAL_BLANKING_VALUE } from '@/constants'; import { mockInstance } from '../../shared/mocking'; diff --git a/packages/cli/test/integration/activation-errors.service.test.ts b/packages/cli/test/integration/activation-errors.service.test.ts index 5d56f0dc90..37dfcc2e08 100644 --- a/packages/cli/test/integration/activation-errors.service.test.ts +++ b/packages/cli/test/integration/activation-errors.service.test.ts @@ -1,4 +1,4 @@ -import { ActivationErrorsService } from '@/ActivationErrors.service'; +import { ActivationErrorsService } from '@/activation-errors.service'; import { CacheService } from '@/services/cache/cache.service'; import { GlobalConfig } from '@n8n/config'; import { mockInstance } from '@test/mocking'; diff --git a/packages/cli/test/integration/active-workflow-manager.test.ts b/packages/cli/test/integration/active-workflow-manager.test.ts index de586b643a..39d6c3b272 100644 --- a/packages/cli/test/integration/active-workflow-manager.test.ts +++ b/packages/cli/test/integration/active-workflow-manager.test.ts @@ -3,16 +3,16 @@ import { mock } from 'jest-mock-extended'; import { NodeApiError, NodeOperationError, Workflow } from 'n8n-workflow'; import type { IWebhookData, WorkflowActivateMode } from 'n8n-workflow'; -import { ActiveExecutions } from '@/ActiveExecutions'; -import { ActiveWorkflowManager } from '@/ActiveWorkflowManager'; -import { ExternalHooks } from '@/ExternalHooks'; +import { ActiveExecutions } from '@/active-executions'; +import { ActiveWorkflowManager } from '@/active-workflow-manager'; +import { ExternalHooks } from '@/external-hooks'; import { Push } from '@/push'; -import { SecretsHelper } from '@/SecretsHelpers'; +import { SecretsHelper } from '@/secrets-helpers'; import { WebhookService } from '@/webhooks/webhook.service'; -import * as WebhookHelpers from '@/webhooks/WebhookHelpers'; -import * as AdditionalData from '@/WorkflowExecuteAdditionalData'; +import * as WebhookHelpers from '@/webhooks/webhook-helpers'; +import * as AdditionalData from '@/workflow-execute-additional-data'; import type { WebhookEntity } from '@db/entities/WebhookEntity'; -import { NodeTypes } from '@/NodeTypes'; +import { NodeTypes } from '@/node-types'; import { ExecutionService } from '@/executions/execution.service'; import { WorkflowService } from '@/workflows/workflow.service'; @@ -20,7 +20,7 @@ import { mockInstance } from '../shared/mocking'; import * as testDb from './shared/testDb'; import { createOwner } from './shared/db/users'; import { createWorkflow } from './shared/db/workflows'; -import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; +import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; import type { WorkflowEntity } from '@/databases/entities/WorkflowEntity'; mockInstance(ActiveExecutions); diff --git a/packages/cli/test/integration/auth.api.test.ts b/packages/cli/test/integration/auth.api.test.ts index 08d4eb0a66..d56a1004ed 100644 --- a/packages/cli/test/integration/auth.api.test.ts +++ b/packages/cli/test/integration/auth.api.test.ts @@ -5,7 +5,7 @@ import config from '@/config'; import { AUTH_COOKIE_NAME } from '@/constants'; import type { User } from '@db/entities/User'; import { UserRepository } from '@db/repositories/user.repository'; -import { MfaService } from '@/Mfa/mfa.service'; +import { MfaService } from '@/mfa/mfa.service'; import { LOGGED_OUT_RESPONSE_BODY } from './shared/constants'; import { randomValidPassword } from './shared/random'; @@ -386,13 +386,19 @@ describe('GET /resolve-signup-token', () => { describe('POST /logout', () => { test('should log user out', async () => { const owner = await createUser({ role: 'global:owner' }); + const ownerAgent = testServer.authAgentFor(owner); + // @ts-expect-error `accessInfo` types are incorrect + const cookie = ownerAgent.jar.getCookie(AUTH_COOKIE_NAME, { path: '/' }); - const response = await testServer.authAgentFor(owner).post('/logout'); + const response = await ownerAgent.post('/logout'); expect(response.statusCode).toBe(200); expect(response.body).toEqual(LOGGED_OUT_RESPONSE_BODY); const authToken = utils.getAuthToken(response); expect(authToken).toBeUndefined(); + + ownerAgent.jar.setCookie(`${AUTH_COOKIE_NAME}=${cookie!.value}`); + await ownerAgent.get('/login').expect(401); }); }); diff --git a/packages/cli/test/integration/auth.mw.test.ts b/packages/cli/test/integration/auth.mw.test.ts index b4a47667fe..262bea38fe 100644 --- a/packages/cli/test/integration/auth.mw.test.ts +++ b/packages/cli/test/integration/auth.mw.test.ts @@ -1,4 +1,4 @@ -import { ActiveWorkflowManager } from '@/ActiveWorkflowManager'; +import { ActiveWorkflowManager } from '@/active-workflow-manager'; import * as utils from './shared/utils/'; import { createUser } from './shared/db/users'; diff --git a/packages/cli/test/integration/commands/credentials.cmd.test.ts b/packages/cli/test/integration/commands/credentials.cmd.test.ts index bcb6f0173b..61b4169712 100644 --- a/packages/cli/test/integration/commands/credentials.cmd.test.ts +++ b/packages/cli/test/integration/commands/credentials.cmd.test.ts @@ -1,7 +1,7 @@ import { nanoid } from 'nanoid'; import { ImportCredentialsCommand } from '@/commands/import/credentials'; -import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; +import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; import { setupTestCommand } from '@test-integration/utils/testCommand'; import { mockInstance } from '../../shared/mocking'; diff --git a/packages/cli/test/integration/commands/import.cmd.test.ts b/packages/cli/test/integration/commands/import.cmd.test.ts index ba72d64d77..7a000ca9c6 100644 --- a/packages/cli/test/integration/commands/import.cmd.test.ts +++ b/packages/cli/test/integration/commands/import.cmd.test.ts @@ -1,7 +1,7 @@ import { nanoid } from 'nanoid'; import { ImportWorkflowsCommand } from '@/commands/import/workflow'; -import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; +import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; import { setupTestCommand } from '@test-integration/utils/testCommand'; import { mockInstance } from '../../shared/mocking'; diff --git a/packages/cli/test/integration/commands/ldap/reset.test.ts b/packages/cli/test/integration/commands/ldap/reset.test.ts index 720c98c401..52350733ca 100644 --- a/packages/cli/test/integration/commands/ldap/reset.test.ts +++ b/packages/cli/test/integration/commands/ldap/reset.test.ts @@ -3,13 +3,13 @@ import { v4 as uuid } from 'uuid'; import { EntityNotFoundError } from '@n8n/typeorm'; import { Reset } from '@/commands/ldap/reset'; -import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; +import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { CredentialsRepository } from '@db/repositories/credentials.repository'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; -import { getLdapSynchronizations, saveLdapSynchronization } from '@/Ldap/helpers.ee'; -import { LdapService } from '@/Ldap/ldap.service.ee'; +import { getLdapSynchronizations, saveLdapSynchronization } from '@/ldap/helpers.ee'; +import { LdapService } from '@/ldap/ldap.service.ee'; import { Push } from '@/push'; import { Telemetry } from '@/telemetry'; diff --git a/packages/cli/test/integration/commands/license.cmd.test.ts b/packages/cli/test/integration/commands/license.cmd.test.ts index 1717e1a5ff..bdca192201 100644 --- a/packages/cli/test/integration/commands/license.cmd.test.ts +++ b/packages/cli/test/integration/commands/license.cmd.test.ts @@ -1,5 +1,5 @@ -import { License } from '@/License'; -import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; +import { License } from '@/license'; +import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; import { ClearLicenseCommand } from '@/commands/license/clear'; import { setupTestCommand } from '@test-integration/utils/testCommand'; diff --git a/packages/cli/test/integration/commands/reset.cmd.test.ts b/packages/cli/test/integration/commands/reset.cmd.test.ts index 573e85043e..73bb7877fe 100644 --- a/packages/cli/test/integration/commands/reset.cmd.test.ts +++ b/packages/cli/test/integration/commands/reset.cmd.test.ts @@ -1,8 +1,8 @@ import { Container } from 'typedi'; import { Reset } from '@/commands/user-management/reset'; -import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; -import { NodeTypes } from '@/NodeTypes'; +import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; +import { NodeTypes } from '@/node-types'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; import { CredentialsRepository } from '@db/repositories/credentials.repository'; diff --git a/packages/cli/test/integration/commands/update/workflow.test.ts b/packages/cli/test/integration/commands/update/workflow.test.ts index 95a25bc4f6..1119536a8a 100644 --- a/packages/cli/test/integration/commands/update/workflow.test.ts +++ b/packages/cli/test/integration/commands/update/workflow.test.ts @@ -1,4 +1,4 @@ -import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; +import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; import { UpdateWorkflowCommand } from '@/commands/update/workflow'; import { setupTestCommand } from '@test-integration/utils/testCommand'; diff --git a/packages/cli/test/integration/commands/worker.cmd.test.ts b/packages/cli/test/integration/commands/worker.cmd.test.ts index e7f783bf94..aad419d23b 100644 --- a/packages/cli/test/integration/commands/worker.cmd.test.ts +++ b/packages/cli/test/integration/commands/worker.cmd.test.ts @@ -2,13 +2,13 @@ import { BinaryDataService } from 'n8n-core'; import { Worker } from '@/commands/worker'; import config from '@/config'; -import { ExternalSecretsManager } from '@/ExternalSecrets/ExternalSecretsManager.ee'; +import { ExternalSecretsManager } from '@/external-secrets/external-secrets-manager.ee'; import { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus'; -import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; +import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; import { OrchestrationHandlerWorkerService } from '@/services/orchestration/worker/orchestration.handler.worker.service'; import { OrchestrationWorkerService } from '@/services/orchestration/worker/orchestration.worker.service'; -import { License } from '@/License'; -import { ExternalHooks } from '@/ExternalHooks'; +import { License } from '@/license'; +import { ExternalHooks } from '@/external-hooks'; import { ScalingService } from '@/scaling/scaling.service'; import { setupTestCommand } from '@test-integration/utils/testCommand'; diff --git a/packages/cli/test/integration/community-packages.api.test.ts b/packages/cli/test/integration/community-packages.api.test.ts index 46c7efad03..ff935a2c5b 100644 --- a/packages/cli/test/integration/community-packages.api.test.ts +++ b/packages/cli/test/integration/community-packages.api.test.ts @@ -2,7 +2,7 @@ import path from 'path'; import type { InstalledPackages } from '@db/entities/InstalledPackages'; import type { InstalledNodes } from '@db/entities/InstalledNodes'; -import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; +import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; import { CommunityPackagesService } from '@/services/communityPackages.service'; import { mockInstance } from '../shared/mocking'; diff --git a/packages/cli/test/integration/controllers/dynamic-node-parameters.controller.test.ts b/packages/cli/test/integration/controllers/dynamic-node-parameters.controller.test.ts index fd66cbf5fd..6488fb3450 100644 --- a/packages/cli/test/integration/controllers/dynamic-node-parameters.controller.test.ts +++ b/packages/cli/test/integration/controllers/dynamic-node-parameters.controller.test.ts @@ -6,7 +6,7 @@ import type { import { mock } from 'jest-mock-extended'; import { DynamicNodeParametersService } from '@/services/dynamicNodeParameters.service'; -import * as AdditionalData from '@/WorkflowExecuteAdditionalData'; +import * as AdditionalData from '@/workflow-execute-additional-data'; import { createOwner } from '../shared/db/users'; import { setupTestServer } from '../shared/utils'; diff --git a/packages/cli/test/integration/controllers/invitation/invitation.controller.integration.test.ts b/packages/cli/test/integration/controllers/invitation/invitation.controller.integration.test.ts index 8b1048267d..3f7e55f866 100644 --- a/packages/cli/test/integration/controllers/invitation/invitation.controller.integration.test.ts +++ b/packages/cli/test/integration/controllers/invitation/invitation.controller.integration.test.ts @@ -1,8 +1,8 @@ import Container from 'typedi'; import { Not } from '@n8n/typeorm'; import { EventService } from '@/events/event.service'; -import { ExternalHooks } from '@/ExternalHooks'; -import { UserManagementMailer } from '@/UserManagement/email'; +import { ExternalHooks } from '@/external-hooks'; +import { UserManagementMailer } from '@/user-management/email'; import { UserRepository } from '@/databases/repositories/user.repository'; import { PasswordUtility } from '@/services/password.utility'; diff --git a/packages/cli/test/integration/controllers/oauth/oauth2.api.test.ts b/packages/cli/test/integration/controllers/oauth/oauth2.api.test.ts index 970727c2c8..46ce3b89a9 100644 --- a/packages/cli/test/integration/controllers/oauth/oauth2.api.test.ts +++ b/packages/cli/test/integration/controllers/oauth/oauth2.api.test.ts @@ -5,7 +5,7 @@ import { parse as parseQs } from 'querystring'; import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; import type { User } from '@db/entities/User'; -import { CredentialsHelper } from '@/CredentialsHelper'; +import { CredentialsHelper } from '@/credentials-helper'; import { OAuth2CredentialController } from '@/controllers/oauth/oAuth2Credential.controller'; import { createOwner } from '@test-integration/db/users'; diff --git a/packages/cli/test/integration/credentials/credentials.api.ee.test.ts b/packages/cli/test/integration/credentials/credentials.api.ee.test.ts index 6b74dd843c..18f45a518b 100644 --- a/packages/cli/test/integration/credentials/credentials.api.ee.test.ts +++ b/packages/cli/test/integration/credentials/credentials.api.ee.test.ts @@ -8,7 +8,7 @@ import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials. import { ProjectRepository } from '@db/repositories/project.repository'; import type { Project } from '@db/entities/Project'; import { ProjectService } from '@/services/project.service'; -import { UserManagementMailer } from '@/UserManagement/email'; +import { UserManagementMailer } from '@/user-management/email'; import { randomCredentialPayload } from '../shared/random'; import * as testDb from '../shared/testDb'; diff --git a/packages/cli/test/integration/debug.controller.test.ts b/packages/cli/test/integration/debug.controller.test.ts index c44a4fd477..a85eb57705 100644 --- a/packages/cli/test/integration/debug.controller.test.ts +++ b/packages/cli/test/integration/debug.controller.test.ts @@ -1,5 +1,5 @@ import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; -import { ActiveWorkflowManager } from '@/ActiveWorkflowManager'; +import { ActiveWorkflowManager } from '@/active-workflow-manager'; import { generateNanoId } from '@/databases/utils/generators'; import type { WorkflowEntity } from '@/databases/entities/WorkflowEntity'; import { OrchestrationService } from '@/services/orchestration.service'; diff --git a/packages/cli/test/integration/executions.controller.test.ts b/packages/cli/test/integration/executions.controller.test.ts index 94faa32351..4c20f7244c 100644 --- a/packages/cli/test/integration/executions.controller.test.ts +++ b/packages/cli/test/integration/executions.controller.test.ts @@ -8,7 +8,7 @@ import { setupTestServer } from './shared/utils'; import { mockInstance } from '../shared/mocking'; import { ConcurrencyControlService } from '@/concurrency/concurrency-control.service'; -import { WaitTracker } from '@/WaitTracker'; +import { WaitTracker } from '@/wait-tracker'; import { createTeamProject, linkUserToProject } from './shared/db/projects'; mockInstance(WaitTracker); diff --git a/packages/cli/test/integration/ldap/ldap.api.test.ts b/packages/cli/test/integration/ldap/ldap.api.test.ts index 25dbbd3b9d..e3805674f3 100644 --- a/packages/cli/test/integration/ldap/ldap.api.test.ts +++ b/packages/cli/test/integration/ldap/ldap.api.test.ts @@ -7,10 +7,10 @@ import config from '@/config'; import type { User } from '@db/entities/User'; import { UserRepository } from '@db/repositories/user.repository'; import { AuthProviderSyncHistoryRepository } from '@db/repositories/authProviderSyncHistory.repository'; -import { LDAP_DEFAULT_CONFIGURATION } from '@/Ldap/constants'; -import { LdapService } from '@/Ldap/ldap.service.ee'; -import { saveLdapSynchronization } from '@/Ldap/helpers.ee'; -import { getCurrentAuthenticationMethod, setCurrentAuthenticationMethod } from '@/sso/ssoHelpers'; +import { LDAP_DEFAULT_CONFIGURATION } from '@/ldap/constants'; +import { LdapService } from '@/ldap/ldap.service.ee'; +import { saveLdapSynchronization } from '@/ldap/helpers.ee'; +import { getCurrentAuthenticationMethod, setCurrentAuthenticationMethod } from '@/sso/sso-helpers'; import { randomEmail, randomName, uniqueId } from './../shared/random'; import * as testDb from './../shared/testDb'; diff --git a/packages/cli/test/integration/license.api.test.ts b/packages/cli/test/integration/license.api.test.ts index d5e434b3c8..1428de8663 100644 --- a/packages/cli/test/integration/license.api.test.ts +++ b/packages/cli/test/integration/license.api.test.ts @@ -3,7 +3,7 @@ import config from '@/config'; import { RESPONSE_ERROR_MESSAGES } from '@/constants'; import type { User } from '@db/entities/User'; import type { ILicensePostResponse, ILicenseReadResponse } from '@/Interfaces'; -import { License } from '@/License'; +import { License } from '@/license'; import * as testDb from './shared/testDb'; import * as utils from './shared/utils/'; diff --git a/packages/cli/test/integration/me.api.test.ts b/packages/cli/test/integration/me.api.test.ts index ef9757dc44..b644d61550 100644 --- a/packages/cli/test/integration/me.api.test.ts +++ b/packages/cli/test/integration/me.api.test.ts @@ -1,7 +1,6 @@ import { Container } from 'typedi'; import { IsNull } from '@n8n/typeorm'; import validator from 'validator'; -import { randomString } from 'n8n-workflow'; import type { User } from '@db/entities/User'; import { UserRepository } from '@db/repositories/user.repository'; @@ -15,6 +14,7 @@ import { addApiKey, createOwner, createUser, createUserShell } from './shared/db import type { SuperAgentTest } from './shared/types'; import { mockInstance } from '@test/mocking'; import { GlobalConfig } from '@n8n/config'; +import type { IPersonalizationSurveyAnswersV4 } from 'n8n-workflow'; const testServer = utils.setupTestServer({ endpointGroups: ['me'] }); @@ -145,16 +145,16 @@ describe('Owner shell', () => { }); test('POST /me/survey should succeed with valid inputs', async () => { - const validPayloads = [SURVEY, {}]; + const validPayloads = [SURVEY, EMPTY_SURVEY]; for (const validPayload of validPayloads) { const response = await authOwnerShellAgent.post('/me/survey').send(validPayload); - expect(response.statusCode).toBe(200); expect(response.body).toEqual(SUCCESS_RESPONSE_BODY); + expect(response.statusCode).toBe(200); const storedShellOwner = await Container.get(UserRepository).findOneOrFail({ - where: { email: IsNull() }, + where: { id: ownerShell.id }, }); expect(storedShellOwner.personalizationAnswers).toEqual(validPayload); @@ -300,7 +300,7 @@ describe('Member', () => { }); test('POST /me/survey should succeed with valid inputs', async () => { - const validPayloads = [SURVEY, {}]; + const validPayloads = [SURVEY, EMPTY_SURVEY]; for (const validPayload of validPayloads) { const response = await authMemberAgent.post('/me/survey').send(validPayload); @@ -392,16 +392,31 @@ describe('Owner', () => { }); }); -const SURVEY = [ - 'codingSkill', - 'companyIndustry', - 'companySize', - 'otherCompanyIndustry', - 'otherWorkArea', - 'workArea', -].reduce>((acc, cur) => { - return (acc[cur] = randomString(2, 10)), acc; -}, {}); +const SURVEY: IPersonalizationSurveyAnswersV4 = { + version: 'v4', + personalization_survey_submitted_at: '2024-08-21T13:05:51.709Z', + personalization_survey_n8n_version: '1.0.0', + automationGoalDevops: ['test'], + automationGoalDevopsOther: 'test', + companyIndustryExtended: ['test'], + otherCompanyIndustryExtended: ['test'], + companySize: 'test', + companyType: 'test', + automationGoalSm: ['test'], + automationGoalSmOther: 'test', + usageModes: ['test'], + email: 'test@email.com', + role: 'test', + roleOther: 'test', + reportedSource: 'test', + reportedSourceOther: 'test', +}; + +const EMPTY_SURVEY: IPersonalizationSurveyAnswersV4 = { + version: 'v4', + personalization_survey_submitted_at: '2024-08-21T13:05:51.709Z', + personalization_survey_n8n_version: '1.0.0', +}; const VALID_PATCH_ME_PAYLOADS = [ { diff --git a/packages/cli/test/integration/mfa/mfa.api.test.ts b/packages/cli/test/integration/mfa/mfa.api.test.ts index 792ad1cb88..7bfdc01477 100644 --- a/packages/cli/test/integration/mfa/mfa.api.test.ts +++ b/packages/cli/test/integration/mfa/mfa.api.test.ts @@ -5,7 +5,7 @@ import { AuthService } from '@/auth/auth.service'; import config from '@/config'; import type { User } from '@db/entities/User'; import { AuthUserRepository } from '@db/repositories/authUser.repository'; -import { TOTPService } from '@/Mfa/totp.service'; +import { TOTPService } from '@/mfa/totp.service'; import * as testDb from '../shared/testDb'; import * as utils from '../shared/utils'; diff --git a/packages/cli/test/integration/middlewares/bodyParser.test.ts b/packages/cli/test/integration/middlewares/bodyParser.test.ts index d17c3fcb11..00163e05bb 100644 --- a/packages/cli/test/integration/middlewares/bodyParser.test.ts +++ b/packages/cli/test/integration/middlewares/bodyParser.test.ts @@ -2,7 +2,7 @@ import { createServer } from 'http'; import { gzipSync, deflateSync } from 'zlib'; import type { Request, Response } from 'express'; import request from 'supertest'; -import { rawBodyReader, bodyParser } from '@/middlewares/bodyParser'; +import { rawBodyReader, bodyParser } from '@/middlewares/body-parser'; describe('bodyParser', () => { const server = createServer((req: Request, res: Response) => { diff --git a/packages/cli/test/integration/passwordReset.api.test.ts b/packages/cli/test/integration/passwordReset.api.test.ts index 3b46fd476f..bfc5c2b35e 100644 --- a/packages/cli/test/integration/passwordReset.api.test.ts +++ b/packages/cli/test/integration/passwordReset.api.test.ts @@ -5,13 +5,13 @@ import { mock } from 'jest-mock-extended'; import { randomString } from 'n8n-workflow'; import { AuthService } from '@/auth/auth.service'; -import { License } from '@/License'; +import { License } from '@/license'; import config from '@/config'; import type { User } from '@db/entities/User'; -import { setCurrentAuthenticationMethod } from '@/sso/ssoHelpers'; -import { ExternalHooks } from '@/ExternalHooks'; +import { setCurrentAuthenticationMethod } from '@/sso/sso-helpers'; +import { ExternalHooks } from '@/external-hooks'; import { JwtService } from '@/services/jwt.service'; -import { UserManagementMailer } from '@/UserManagement/email'; +import { UserManagementMailer } from '@/user-management/email'; import { UserRepository } from '@db/repositories/user.repository'; import { PasswordUtility } from '@/services/password.utility'; diff --git a/packages/cli/test/integration/PermissionChecker.test.ts b/packages/cli/test/integration/permission-checker.test.ts similarity index 92% rename from packages/cli/test/integration/PermissionChecker.test.ts rename to packages/cli/test/integration/permission-checker.test.ts index ff10d04974..f35603a1b6 100644 --- a/packages/cli/test/integration/PermissionChecker.test.ts +++ b/packages/cli/test/integration/permission-checker.test.ts @@ -5,17 +5,17 @@ import { randomInt } from 'n8n-workflow'; import type { User } from '@db/entities/User'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; -import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; -import { NodeTypes } from '@/NodeTypes'; +import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; +import { NodeTypes } from '@/node-types'; import { OwnershipService } from '@/services/ownership.service'; -import { PermissionChecker } from '@/UserManagement/PermissionChecker'; +import { PermissionChecker } from '@/user-management/permission-checker'; import { mockInstance } from '../shared/mocking'; -import { randomCredentialPayload as randomCred } from '../integration/shared/random'; -import * as testDb from '../integration/shared/testDb'; -import type { SaveCredentialFunction } from '../integration/shared/types'; -import { affixRoleToSaveCredential } from '../integration/shared/db/credentials'; -import { createOwner, createUser } from '../integration/shared/db/users'; +import { randomCredentialPayload as randomCred } from './shared/random'; +import * as testDb from './shared/testDb'; +import type { SaveCredentialFunction } from './shared/types'; +import { affixRoleToSaveCredential } from './shared/db/credentials'; +import { createOwner, createUser } from './shared/db/users'; import { SharedCredentialsRepository } from '@/databases/repositories/sharedCredentials.repository'; import { getPersonalProject } from './shared/db/projects'; import type { WorkflowEntity } from '@/databases/entities/WorkflowEntity'; diff --git a/packages/cli/test/integration/project.api.test.ts b/packages/cli/test/integration/project.api.test.ts index 0d24912710..4fb7f08a02 100644 --- a/packages/cli/test/integration/project.api.test.ts +++ b/packages/cli/test/integration/project.api.test.ts @@ -27,7 +27,7 @@ import type { GlobalRole } from '@/databases/entities/User'; import type { Scope } from '@n8n/permissions'; import { CacheService } from '@/services/cache/cache.service'; import { mockInstance } from '../shared/mocking'; -import { ActiveWorkflowManager } from '@/ActiveWorkflowManager'; +import { ActiveWorkflowManager } from '@/active-workflow-manager'; import { ProjectRepository } from '@/databases/repositories/project.repository'; import { RoleService } from '@/services/role.service'; diff --git a/packages/cli/test/integration/pruning.service.test.ts b/packages/cli/test/integration/pruning.service.test.ts index 37d218c09d..f5deabf756 100644 --- a/packages/cli/test/integration/pruning.service.test.ts +++ b/packages/cli/test/integration/pruning.service.test.ts @@ -9,7 +9,7 @@ import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import { ExecutionRepository } from '@db/repositories/execution.repository'; import { TIME } from '@/constants'; import { PruningService } from '@/services/pruning.service'; -import { Logger } from '@/Logger'; +import { Logger } from '@/logger'; import { mockInstance } from '../shared/mocking'; import { createWorkflow } from './shared/db/workflows'; diff --git a/packages/cli/test/integration/publicApi/executions.test.ts b/packages/cli/test/integration/publicApi/executions.test.ts index 8dac97fc22..9994dc8a85 100644 --- a/packages/cli/test/integration/publicApi/executions.test.ts +++ b/packages/cli/test/integration/publicApi/executions.test.ts @@ -1,5 +1,5 @@ import type { User } from '@db/entities/User'; -import type { ActiveWorkflowManager } from '@/ActiveWorkflowManager'; +import type { ActiveWorkflowManager } from '@/active-workflow-manager'; import { randomApiKey } from '../shared/random'; import * as utils from '../shared/utils/'; diff --git a/packages/cli/test/integration/publicApi/users.ee.test.ts b/packages/cli/test/integration/publicApi/users.ee.test.ts index be23d8f45a..4874fab4f5 100644 --- a/packages/cli/test/integration/publicApi/users.ee.test.ts +++ b/packages/cli/test/integration/publicApi/users.ee.test.ts @@ -1,7 +1,7 @@ import validator from 'validator'; import { v4 as uuid } from 'uuid'; -import { License } from '@/License'; +import { License } from '@/license'; import { mockInstance } from '../../shared/mocking'; import { randomApiKey } from '../shared/random'; diff --git a/packages/cli/test/integration/publicApi/workflows.test.ts b/packages/cli/test/integration/publicApi/workflows.test.ts index 5185f3862d..66736aa03d 100644 --- a/packages/cli/test/integration/publicApi/workflows.test.ts +++ b/packages/cli/test/integration/publicApi/workflows.test.ts @@ -9,7 +9,7 @@ import type { Project } from '@db/entities/Project'; import { ProjectRepository } from '@db/repositories/project.repository'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { WorkflowHistoryRepository } from '@db/repositories/workflowHistory.repository'; -import { ActiveWorkflowManager } from '@/ActiveWorkflowManager'; +import { ActiveWorkflowManager } from '@/active-workflow-manager'; import { ExecutionService } from '@/executions/execution.service'; import { randomApiKey } from '../shared/random'; diff --git a/packages/cli/test/integration/saml/saml.api.test.ts b/packages/cli/test/integration/saml/saml.api.test.ts index 23dc49607f..eef02fcfe2 100644 --- a/packages/cli/test/integration/saml/saml.api.test.ts +++ b/packages/cli/test/integration/saml/saml.api.test.ts @@ -1,6 +1,6 @@ import type { User } from '@db/entities/User'; -import { setSamlLoginEnabled } from '@/sso/saml/samlHelpers'; -import { getCurrentAuthenticationMethod, setCurrentAuthenticationMethod } from '@/sso/ssoHelpers'; +import { setSamlLoginEnabled } from '@/sso/saml/saml-helpers'; +import { getCurrentAuthenticationMethod, setCurrentAuthenticationMethod } from '@/sso/sso-helpers'; import { randomEmail, randomName, randomValidPassword } from '../shared/random'; import * as utils from '../shared/utils/'; diff --git a/packages/cli/test/integration/saml/samlHelpers.test.ts b/packages/cli/test/integration/saml/samlHelpers.test.ts index 7941efada1..ac43f57a1b 100644 --- a/packages/cli/test/integration/saml/samlHelpers.test.ts +++ b/packages/cli/test/integration/saml/samlHelpers.test.ts @@ -1,5 +1,5 @@ -import * as helpers from '@/sso/saml/samlHelpers'; -import type { SamlUserAttributes } from '@/sso/saml/types/samlUserAttributes'; +import * as helpers from '@/sso/saml/saml-helpers'; +import type { SamlUserAttributes } from '@/sso/saml/types/saml-user-attributes'; import { getPersonalProject } from '../../integration/shared/db/projects'; import * as testDb from '../shared/testDb'; diff --git a/packages/cli/test/integration/security-audit/CredentialsRiskReporter.test.ts b/packages/cli/test/integration/security-audit/CredentialsRiskReporter.test.ts index 4fdc54dbc1..bd9cd124ed 100644 --- a/packages/cli/test/integration/security-audit/CredentialsRiskReporter.test.ts +++ b/packages/cli/test/integration/security-audit/CredentialsRiskReporter.test.ts @@ -1,6 +1,6 @@ import { v4 as uuid } from 'uuid'; import config from '@/config'; -import { SecurityAuditService } from '@/security-audit/SecurityAudit.service'; +import { SecurityAuditService } from '@/security-audit/security-audit.service'; import { CREDENTIALS_REPORT } from '@/security-audit/constants'; import { getRiskSection } from './utils'; import * as testDb from '../shared/testDb'; diff --git a/packages/cli/test/integration/security-audit/DatabaseRiskReporter.test.ts b/packages/cli/test/integration/security-audit/DatabaseRiskReporter.test.ts index e7ae638d97..bfa8ff0aaf 100644 --- a/packages/cli/test/integration/security-audit/DatabaseRiskReporter.test.ts +++ b/packages/cli/test/integration/security-audit/DatabaseRiskReporter.test.ts @@ -1,5 +1,5 @@ import { v4 as uuid } from 'uuid'; -import { SecurityAuditService } from '@/security-audit/SecurityAudit.service'; +import { SecurityAuditService } from '@/security-audit/security-audit.service'; import { DATABASE_REPORT, SQL_NODE_TYPES, diff --git a/packages/cli/test/integration/security-audit/FilesystemRiskReporter.test.ts b/packages/cli/test/integration/security-audit/FilesystemRiskReporter.test.ts index f6d8537c0d..5ec1f0c4d4 100644 --- a/packages/cli/test/integration/security-audit/FilesystemRiskReporter.test.ts +++ b/packages/cli/test/integration/security-audit/FilesystemRiskReporter.test.ts @@ -1,5 +1,5 @@ import { v4 as uuid } from 'uuid'; -import { SecurityAuditService } from '@/security-audit/SecurityAudit.service'; +import { SecurityAuditService } from '@/security-audit/security-audit.service'; import { FILESYSTEM_INTERACTION_NODE_TYPES, FILESYSTEM_REPORT } from '@/security-audit/constants'; import { getRiskSection, saveManualTriggerWorkflow } from './utils'; import * as testDb from '../shared/testDb'; diff --git a/packages/cli/test/integration/security-audit/InstanceRiskReporter.test.ts b/packages/cli/test/integration/security-audit/InstanceRiskReporter.test.ts index 5f59206426..d6dc020d4f 100644 --- a/packages/cli/test/integration/security-audit/InstanceRiskReporter.test.ts +++ b/packages/cli/test/integration/security-audit/InstanceRiskReporter.test.ts @@ -1,5 +1,5 @@ import { v4 as uuid } from 'uuid'; -import { SecurityAuditService } from '@/security-audit/SecurityAudit.service'; +import { SecurityAuditService } from '@/security-audit/security-audit.service'; import { INSTANCE_REPORT, WEBHOOK_VALIDATOR_NODE_TYPES } from '@/security-audit/constants'; import { getRiskSection, diff --git a/packages/cli/test/integration/security-audit/NodesRiskReporter.test.ts b/packages/cli/test/integration/security-audit/NodesRiskReporter.test.ts index 03386bef00..e61027b356 100644 --- a/packages/cli/test/integration/security-audit/NodesRiskReporter.test.ts +++ b/packages/cli/test/integration/security-audit/NodesRiskReporter.test.ts @@ -1,10 +1,10 @@ import { v4 as uuid } from 'uuid'; import { Container } from 'typedi'; -import { SecurityAuditService } from '@/security-audit/SecurityAudit.service'; +import { SecurityAuditService } from '@/security-audit/security-audit.service'; import { OFFICIAL_RISKY_NODE_TYPES, NODES_REPORT } from '@/security-audit/constants'; import { toReportTitle } from '@/security-audit/utils'; -import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; -import { NodeTypes } from '@/NodeTypes'; +import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; +import { NodeTypes } from '@/node-types'; import { CommunityPackagesService } from '@/services/communityPackages.service'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; diff --git a/packages/cli/test/integration/services/workflowStaticData.service.test.ts b/packages/cli/test/integration/services/workflowStaticData.service.test.ts index 353639bcc2..0ce2665001 100644 --- a/packages/cli/test/integration/services/workflowStaticData.service.test.ts +++ b/packages/cli/test/integration/services/workflowStaticData.service.test.ts @@ -1,10 +1,10 @@ -import { WorkflowStaticDataService } from '@/workflows/workflowStaticData.service'; +import { WorkflowStaticDataService } from '@/workflows/workflow-static-data.service'; import * as testDb from '@test-integration/testDb'; import Container from 'typedi'; import { createWorkflow } from '@test-integration/db/workflows'; import { Workflow } from 'n8n-workflow'; import { mockInstance } from '@test/mocking'; -import { NodeTypes } from '@/NodeTypes'; +import { NodeTypes } from '@/node-types'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; const nodeTypes = mockInstance(NodeTypes); diff --git a/packages/cli/test/integration/shared/db/credentials.ts b/packages/cli/test/integration/shared/db/credentials.ts index 588fee6b51..4aedb09324 100644 --- a/packages/cli/test/integration/shared/db/credentials.ts +++ b/packages/cli/test/integration/shared/db/credentials.ts @@ -12,7 +12,7 @@ import type { Project } from '@/databases/entities/Project'; export async function encryptCredentialData( credential: CredentialsEntity, ): Promise { - const { createCredentialsFromCredentialsEntity } = await import('@/CredentialsHelper'); + const { createCredentialsFromCredentialsEntity } = await import('@/credentials-helper'); const coreCredential = createCredentialsFromCredentialsEntity(credential, true); // @ts-ignore diff --git a/packages/cli/test/integration/shared/db/users.ts b/packages/cli/test/integration/shared/db/users.ts index 98626bc549..a939ce7b74 100644 --- a/packages/cli/test/integration/shared/db/users.ts +++ b/packages/cli/test/integration/shared/db/users.ts @@ -4,8 +4,8 @@ import { AuthIdentity } from '@db/entities/AuthIdentity'; import { type GlobalRole, type User } from '@db/entities/User'; import { AuthIdentityRepository } from '@db/repositories/authIdentity.repository'; import { UserRepository } from '@db/repositories/user.repository'; -import { TOTPService } from '@/Mfa/totp.service'; -import { MfaService } from '@/Mfa/mfa.service'; +import { TOTPService } from '@/mfa/totp.service'; +import { MfaService } from '@/mfa/mfa.service'; import { randomApiKey, randomEmail, randomName, randomValidPassword } from '../random'; import { AuthUserRepository } from '@/databases/repositories/authUser.repository'; diff --git a/packages/cli/test/integration/shared/ldap.ts b/packages/cli/test/integration/shared/ldap.ts index 1223bd0f07..147b01f4c6 100644 --- a/packages/cli/test/integration/shared/ldap.ts +++ b/packages/cli/test/integration/shared/ldap.ts @@ -1,5 +1,5 @@ -import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/Ldap/constants'; -import type { LdapConfig } from '@/Ldap/types'; +import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/ldap/constants'; +import type { LdapConfig } from '@/ldap/types'; import { SettingsRepository } from '@/databases/repositories/settings.repository'; import { jsonParse } from 'n8n-workflow'; import Container from 'typedi'; diff --git a/packages/cli/test/integration/shared/license.ts b/packages/cli/test/integration/shared/license.ts index 3f9e736153..701cc36eab 100644 --- a/packages/cli/test/integration/shared/license.ts +++ b/packages/cli/test/integration/shared/license.ts @@ -1,5 +1,5 @@ import type { BooleanLicenseFeature, NumericLicenseFeature } from '@/Interfaces'; -import type { License } from '@/License'; +import type { License } from '@/license'; export interface LicenseMockDefaults { features?: BooleanLicenseFeature[]; diff --git a/packages/cli/test/integration/shared/utils/index.ts b/packages/cli/test/integration/shared/utils/index.ts index 7abdb1a8e8..329b9d7b8a 100644 --- a/packages/cli/test/integration/shared/utils/index.ts +++ b/packages/cli/test/integration/shared/utils/index.ts @@ -14,7 +14,7 @@ import { WorkflowEntity } from '@db/entities/WorkflowEntity'; import { SettingsRepository } from '@db/repositories/settings.repository'; import { AUTH_COOKIE_NAME } from '@/constants'; import { ExecutionService } from '@/executions/execution.service'; -import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; +import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; import { Push } from '@/push'; import { OrchestrationService } from '@/services/orchestration.service'; @@ -37,7 +37,7 @@ export async function initActiveWorkflowManager() { mockInstance(Push); mockInstance(ExecutionService); - const { ActiveWorkflowManager } = await import('@/ActiveWorkflowManager'); + const { ActiveWorkflowManager } = await import('@/active-workflow-manager'); const activeWorkflowManager = Container.get(ActiveWorkflowManager); await activeWorkflowManager.init(); return activeWorkflowManager; diff --git a/packages/cli/test/integration/shared/utils/testCommand.ts b/packages/cli/test/integration/shared/utils/testCommand.ts index 9592b806c3..4e796316bb 100644 --- a/packages/cli/test/integration/shared/utils/testCommand.ts +++ b/packages/cli/test/integration/shared/utils/testCommand.ts @@ -2,11 +2,11 @@ import type { Config } from '@oclif/core'; import type { Class } from 'n8n-core'; import { mock } from 'jest-mock-extended'; -import type { BaseCommand } from '@/commands/BaseCommand'; +import type { BaseCommand } from '@/commands/base-command'; import * as testDb from '../testDb'; import { TelemetryEventRelay } from '@/events/telemetry-event-relay'; import { mockInstance } from '@test/mocking'; -import { InternalHooks } from '@/InternalHooks'; +import { InternalHooks } from '@/internal-hooks'; mockInstance(InternalHooks); diff --git a/packages/cli/test/integration/shared/utils/testServer.ts b/packages/cli/test/integration/shared/utils/testServer.ts index 319c4e2b58..c40285829e 100644 --- a/packages/cli/test/integration/shared/utils/testServer.ts +++ b/packages/cli/test/integration/shared/utils/testServer.ts @@ -12,8 +12,8 @@ import { ControllerRegistry } from '@/decorators'; import { rawBodyReader, bodyParser } from '@/middlewares'; import { PostHogClient } from '@/posthog'; import { Push } from '@/push'; -import { License } from '@/License'; -import { Logger } from '@/Logger'; +import { License } from '@/license'; +import { Logger } from '@/logger'; import { AuthService } from '@/auth/auth.service'; import type { APIRequest } from '@/requests'; @@ -166,14 +166,14 @@ export const setupTestServer = ({ break; case 'ldap': - const { LdapService } = await import('@/Ldap/ldap.service.ee'); - await import('@/Ldap/ldap.controller.ee'); + const { LdapService } = await import('@/ldap/ldap.service.ee'); + await import('@/ldap/ldap.controller.ee'); testServer.license.enable('feat:ldap'); await Container.get(LdapService).init(); break; case 'saml': - const { setSamlLoginEnabled } = await import('@/sso/saml/samlHelpers'); + const { setSamlLoginEnabled } = await import('@/sso/saml/saml-helpers'); await import('@/sso/saml/routes/saml.controller.ee'); await setSamlLoginEnabled(true); break; @@ -211,11 +211,11 @@ export const setupTestServer = ({ break; case 'externalSecrets': - await import('@/ExternalSecrets/ExternalSecrets.controller.ee'); + await import('@/external-secrets/external-secrets.controller.ee'); break; case 'workflowHistory': - await import('@/workflows/workflowHistory/workflowHistory.controller.ee'); + await import('@/workflows/workflow-history/workflow-history.controller.ee'); break; case 'binaryData': diff --git a/packages/cli/test/integration/webhooks.api.test.ts b/packages/cli/test/integration/webhooks.api.test.ts index 5b61dd9861..630fe7350e 100644 --- a/packages/cli/test/integration/webhooks.api.test.ts +++ b/packages/cli/test/integration/webhooks.api.test.ts @@ -2,9 +2,9 @@ import { readFileSync } from 'fs'; import { agent as testAgent } from 'supertest'; import type { INodeType, INodeTypeDescription, IWebhookFunctions } from 'n8n-workflow'; -import { AbstractServer } from '@/AbstractServer'; -import { ExternalHooks } from '@/ExternalHooks'; -import { NodeTypes } from '@/NodeTypes'; +import { AbstractServer } from '@/abstract-server'; +import { ExternalHooks } from '@/external-hooks'; +import { NodeTypes } from '@/node-types'; import { Push } from '@/push'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; diff --git a/packages/cli/test/integration/webhooks.test.ts b/packages/cli/test/integration/webhooks.test.ts index a729f95158..789ced02c6 100644 --- a/packages/cli/test/integration/webhooks.test.ts +++ b/packages/cli/test/integration/webhooks.test.ts @@ -2,12 +2,12 @@ import type SuperAgentTest from 'supertest/lib/agent'; import { agent as testAgent } from 'supertest'; import { mock } from 'jest-mock-extended'; -import { AbstractServer } from '@/AbstractServer'; -import { LiveWebhooks } from '@/webhooks/LiveWebhooks'; -import { ExternalHooks } from '@/ExternalHooks'; -import { TestWebhooks } from '@/webhooks/TestWebhooks'; -import { WaitingWebhooks } from '@/webhooks/WaitingWebhooks'; -import { WaitingForms } from '@/WaitingForms'; +import { AbstractServer } from '@/abstract-server'; +import { LiveWebhooks } from '@/webhooks/live-webhooks'; +import { ExternalHooks } from '@/external-hooks'; +import { TestWebhooks } from '@/webhooks/test-webhooks'; +import { WaitingWebhooks } from '@/webhooks/waiting-webhooks'; +import { WaitingForms } from '@/waiting-forms'; import type { IWebhookResponseCallbackData } from '@/webhooks/webhook.types'; import { mockInstance } from '@test/mocking'; diff --git a/packages/cli/test/integration/workflowHistoryManager.test.ts b/packages/cli/test/integration/workflowHistoryManager.test.ts index 85a114abee..fa2da33e55 100644 --- a/packages/cli/test/integration/workflowHistoryManager.test.ts +++ b/packages/cli/test/integration/workflowHistoryManager.test.ts @@ -4,8 +4,8 @@ import { DateTime } from 'luxon'; import config from '@/config'; import { WorkflowHistoryRepository } from '@db/repositories/workflowHistory.repository'; -import { License } from '@/License'; -import { WorkflowHistoryManager } from '@/workflows/workflowHistory/workflowHistoryManager.ee'; +import { License } from '@/license'; +import { WorkflowHistoryManager } from '@/workflows/workflow-history/workflow-history-manager.ee'; import { mockInstance } from '../shared/mocking'; import * as testDb from './shared/testDb'; diff --git a/packages/cli/test/integration/workflows/workflow.service.test.ts b/packages/cli/test/integration/workflows/workflow.service.test.ts index b09cf30b0d..1853c17839 100644 --- a/packages/cli/test/integration/workflows/workflow.service.test.ts +++ b/packages/cli/test/integration/workflows/workflow.service.test.ts @@ -1,6 +1,6 @@ import Container from 'typedi'; import { mock } from 'jest-mock-extended'; -import { ActiveWorkflowManager } from '@/ActiveWorkflowManager'; +import { ActiveWorkflowManager } from '@/active-workflow-manager'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus'; diff --git a/packages/cli/test/integration/workflows/workflowSharing.service.test.ts b/packages/cli/test/integration/workflows/workflowSharing.service.test.ts index 1907770fb1..58bed7d1e3 100644 --- a/packages/cli/test/integration/workflows/workflowSharing.service.test.ts +++ b/packages/cli/test/integration/workflows/workflowSharing.service.test.ts @@ -1,14 +1,14 @@ import Container from 'typedi'; import type { User } from '@db/entities/User'; -import { WorkflowSharingService } from '@/workflows/workflowSharing.service'; +import { WorkflowSharingService } from '@/workflows/workflow-sharing.service'; import * as testDb from '../shared/testDb'; import { createUser } from '../shared/db/users'; import { createWorkflow, shareWorkflowWithUsers } from '../shared/db/workflows'; import { ProjectService } from '@/services/project.service'; import { LicenseMocker } from '../shared/license'; -import { License } from '@/License'; +import { License } from '@/license'; let owner: User; let member: User; diff --git a/packages/cli/test/integration/workflows/workflows.controller.ee.test.ts b/packages/cli/test/integration/workflows/workflows.controller.ee.test.ts index e18dbd2fb5..060eba25e2 100644 --- a/packages/cli/test/integration/workflows/workflows.controller.ee.test.ts +++ b/packages/cli/test/integration/workflows/workflows.controller.ee.test.ts @@ -7,9 +7,9 @@ import type { Project } from '@db/entities/Project'; import { ProjectRepository } from '@db/repositories/project.repository'; import type { User } from '@db/entities/User'; import { WorkflowHistoryRepository } from '@db/repositories/workflowHistory.repository'; -import { ActiveWorkflowManager } from '@/ActiveWorkflowManager'; -import { License } from '@/License'; -import { UserManagementMailer } from '@/UserManagement/email'; +import { ActiveWorkflowManager } from '@/active-workflow-manager'; +import { License } from '@/license'; +import { UserManagementMailer } from '@/user-management/email'; import type { WorkflowWithSharingsMetaDataAndCredentials } from '@/workflows/workflows.types'; import { mockInstance } from '../../shared/mocking'; diff --git a/packages/cli/test/integration/workflows/workflows.controller.test.ts b/packages/cli/test/integration/workflows/workflows.controller.test.ts index 8ada8166ad..d021df875b 100644 --- a/packages/cli/test/integration/workflows/workflows.controller.test.ts +++ b/packages/cli/test/integration/workflows/workflows.controller.test.ts @@ -11,9 +11,9 @@ import { WorkflowHistoryRepository } from '@db/repositories/workflowHistory.repo import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { ProjectRepository } from '@db/repositories/project.repository'; import { ProjectService } from '@/services/project.service'; -import { ActiveWorkflowManager } from '@/ActiveWorkflowManager'; +import { ActiveWorkflowManager } from '@/active-workflow-manager'; import { EnterpriseWorkflowService } from '@/workflows/workflow.service.ee'; -import { License } from '@/License'; +import { License } from '@/license'; import { mockInstance } from '../../shared/mocking'; import * as utils from '../shared/utils/'; diff --git a/packages/core/package.json b/packages/core/package.json index 24da0bf137..601930264b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "n8n-core", - "version": "1.55.0", + "version": "1.56.0", "description": "Core functionality of n8n", "main": "dist/index", "types": "dist/index.d.ts", diff --git a/packages/core/src/ScheduledTaskManager.ts b/packages/core/src/ScheduledTaskManager.ts index ce656f3716..eb519a60a7 100644 --- a/packages/core/src/ScheduledTaskManager.ts +++ b/packages/core/src/ScheduledTaskManager.ts @@ -1,13 +1,24 @@ import { Service } from 'typedi'; import { CronJob } from 'cron'; import type { CronExpression, Workflow } from 'n8n-workflow'; +import { InstanceSettings } from './InstanceSettings'; @Service() export class ScheduledTaskManager { + constructor(private readonly instanceSettings: InstanceSettings) {} + readonly cronJobs = new Map(); registerCron(workflow: Workflow, cronExpression: CronExpression, onTick: () => void) { - const cronJob = new CronJob(cronExpression, onTick, undefined, true, workflow.timezone); + const cronJob = new CronJob( + cronExpression, + () => { + if (this.instanceSettings.isLeader) onTick(); + }, + undefined, + true, + workflow.timezone, + ); const cronJobsForWorkflow = this.cronJobs.get(workflow.id); if (cronJobsForWorkflow) { cronJobsForWorkflow.push(cronJob); diff --git a/packages/core/test/ScheduledTaskManager.test.ts b/packages/core/test/ScheduledTaskManager.test.ts index df7fb9b77e..15d5f7d487 100644 --- a/packages/core/test/ScheduledTaskManager.test.ts +++ b/packages/core/test/ScheduledTaskManager.test.ts @@ -1,9 +1,11 @@ import type { Workflow } from 'n8n-workflow'; import { mock } from 'jest-mock-extended'; +import type { InstanceSettings } from '@/InstanceSettings'; import { ScheduledTaskManager } from '@/ScheduledTaskManager'; describe('ScheduledTaskManager', () => { + const instanceSettings = mock({ isLeader: true }); const workflow = mock({ timezone: 'GMT' }); const everyMinute = '0 * * * * *'; const onTick = jest.fn(); @@ -13,7 +15,7 @@ describe('ScheduledTaskManager', () => { beforeEach(() => { jest.clearAllMocks(); jest.useFakeTimers(); - scheduledTaskManager = new ScheduledTaskManager(); + scheduledTaskManager = new ScheduledTaskManager(instanceSettings); }); it('should throw when workflow timezone is invalid', () => { @@ -41,6 +43,15 @@ describe('ScheduledTaskManager', () => { expect(onTick).toHaveBeenCalledTimes(10); }); + it('should should not invoke on follower instances', async () => { + scheduledTaskManager = new ScheduledTaskManager(mock({ isLeader: false })); + scheduledTaskManager.registerCron(workflow, everyMinute, onTick); + + expect(onTick).not.toHaveBeenCalled(); + jest.advanceTimersByTime(10 * 60 * 1000); // 10 minutes + expect(onTick).not.toHaveBeenCalled(); + }); + it('should deregister CronJobs for a workflow', async () => { scheduledTaskManager.registerCron(workflow, everyMinute, onTick); scheduledTaskManager.registerCron(workflow, everyMinute, onTick); diff --git a/packages/design-system/.storybook/preview.js b/packages/design-system/.storybook/preview.js index bd329cfaea..bb44f598ea 100644 --- a/packages/design-system/.storybook/preview.js +++ b/packages/design-system/.storybook/preview.js @@ -8,7 +8,7 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { fas } from '@fortawesome/free-solid-svg-icons'; import ElementPlus from 'element-plus'; -import lang from 'element-plus/lib/locale/lang/en'; +import lang from 'element-plus/dist/locale/en.mjs' import { N8nPlugin } from '../src/plugin'; diff --git a/packages/design-system/package.json b/packages/design-system/package.json index f6f0a128ab..09116e5218 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -1,6 +1,6 @@ { "name": "n8n-design-system", - "version": "1.45.0", + "version": "1.46.0", "main": "src/main.ts", "import": "src/main.ts", "scripts": { @@ -44,7 +44,7 @@ "@fortawesome/fontawesome-svg-core": "^1.2.36", "@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/vue-fontawesome": "^3.0.3", - "element-plus": "^2.3.6", + "element-plus": "2.4.3", "markdown-it": "^13.0.2", "markdown-it-emoji": "^2.0.2", "markdown-it-link-attributes": "^4.0.1", diff --git a/packages/design-system/src/components/AskAssistantChat/AskAssistantChat.vue b/packages/design-system/src/components/AskAssistantChat/AskAssistantChat.vue index 30bdf81351..a1ae2e8e88 100644 --- a/packages/design-system/src/components/AskAssistantChat/AskAssistantChat.vue +++ b/packages/design-system/src/components/AskAssistantChat/AskAssistantChat.vue @@ -102,13 +102,20 @@ function growInput() { -
+
-
+
-
+
⚠️ {{ message.content }}
@@ -173,7 +184,11 @@ function growInput() { @undo="() => emit('codeUndo', i)" />
-
+
{{ t('assistantChat.sessionEndMessage.1') }} @@ -193,7 +208,7 @@ function growInput() { :class="$style.quickReplies" >
{{ t('assistantChat.quickRepliesTitle') }}
-
+
-
+
Hi {{ user?.firstName }} 👋

@@ -232,6 +247,7 @@ function growInput() {