mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-27 23:07:12 +02:00
Merge remote-tracking branch 'origin/master' into ADO-3851
This commit is contained in:
commit
fa33743e1f
|
|
@ -15,5 +15,5 @@ packages/**/*.test.*
|
|||
docker/compose
|
||||
docker/**/Dockerfile
|
||||
.vscode
|
||||
cypress
|
||||
test-workflows
|
||||
packages/testing
|
||||
cypress
|
||||
25
.github/ISSUE_TEMPLATE/01-bug.yml
vendored
25
.github/ISSUE_TEMPLATE/01-bug.yml
vendored
|
|
@ -4,7 +4,13 @@ body:
|
|||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
> ⚠️ This form is for reporting bugs only.
|
||||
> ❌ Please do not use this form for general support, feature requests, or questions.
|
||||
> 💬 For help and general inquiries, visit our [community support forum](https://community.n8n.io).
|
||||
> ☁️ If you're experiencing issues with cloud instances not starting or license-related problems, contact [n8n support directly](mailto:help@n8n.io).
|
||||
---
|
||||
Thank you for helping us improve n8n!
|
||||
To ensure we can address your report efficiently, please fill out all sections in English and provide as much detail as possible.
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
|
|
@ -32,6 +38,13 @@ body:
|
|||
description: A clear and concise description of what you expected to happen
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: debug-info
|
||||
attributes:
|
||||
label: Debug Info
|
||||
description: This can be found under Help > About n8n > Copy debug information
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '## Environment'
|
||||
|
|
@ -80,3 +93,13 @@ body:
|
|||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: hosting
|
||||
attributes:
|
||||
label: Hosting
|
||||
options:
|
||||
- n8n cloud
|
||||
- self hosted
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
|
|
|
|||
16
.github/workflows/check-documentation-urls.yml
vendored
16
.github/workflows/check-documentation-urls.yml
vendored
|
|
@ -16,20 +16,10 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
- name: Setup Node.js
|
||||
uses: n8n-io/n8n/.github/actions/setup-nodejs-blacksmith@f5fbbbe0a28a886451c886cac6b49192a39b0eea # v1.104.1
|
||||
with:
|
||||
node-version: 22.x
|
||||
|
||||
- name: Setup corepack and pnpm
|
||||
run: |
|
||||
npm i -g corepack@0.33
|
||||
corepack enable
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build relevant packages
|
||||
run: pnpm build:nodes
|
||||
build-command: turbo build --filter=*nodes*
|
||||
|
||||
- run: npm install --prefix=.github/scripts --no-package-lock
|
||||
|
||||
|
|
|
|||
16
.github/workflows/check-pr-title.yml
vendored
16
.github/workflows/check-pr-title.yml
vendored
|
|
@ -14,23 +14,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Check out branch
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 22.x
|
||||
|
||||
- name: Setup corepack and pnpm
|
||||
run: |
|
||||
npm i -g corepack@0.33
|
||||
corepack enable
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Validate PR title
|
||||
id: validate_pr_title
|
||||
uses: n8n-io/validate-n8n-pull-request-title@c97ff722ac14ee0bda73766473bba764445db805 # v2.2.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
|||
42
.github/workflows/ci-master.yml
vendored
42
.github/workflows/ci-master.yml
vendored
|
|
@ -6,64 +6,24 @@ on:
|
|||
- master
|
||||
|
||||
jobs:
|
||||
install-and-build:
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2204
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=4096'
|
||||
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- uses: useblacksmith/setup-node@65c6ca86fdeb0ab3d85e78f57e4f6a7e4780b391 # v5
|
||||
with:
|
||||
node-version: 22.x
|
||||
|
||||
- name: Setup corepack and pnpm
|
||||
run: |
|
||||
npm i -g corepack@0.33
|
||||
corepack enable
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup build cache
|
||||
uses: useblacksmith/caching-for-turbo@bafb57e7ebdbf1185762286ec94d24648cd3938a # v1
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Cache build artifacts
|
||||
uses: useblacksmith/cache/save@c5fe29eb0efdf1cf4186b9f7fcbbcbc0cf025662 # v5
|
||||
with:
|
||||
path: ./packages/**/dist
|
||||
key: ${{ github.sha }}-base:build
|
||||
|
||||
unit-test:
|
||||
name: Unit tests
|
||||
uses: ./.github/workflows/units-tests-reusable.yml
|
||||
needs: install-and-build
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.x, 22.x, 24.x]
|
||||
node-version: [20.x, 22.x, 24.3.x]
|
||||
with:
|
||||
ref: ${{ inputs.branch }}
|
||||
nodeVersion: ${{ matrix.node-version }}
|
||||
cacheKey: ${{ github.sha }}-base:build
|
||||
collectCoverage: ${{ matrix.node-version == '22.x' }}
|
||||
ignoreTurboCache: ${{ matrix.node-version == '22.x' }}
|
||||
skipFrontendTests: ${{ matrix.node-version != '22.x' }}
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
uses: ./.github/workflows/linting-reusable.yml
|
||||
needs: install-and-build
|
||||
with:
|
||||
ref: ${{ inputs.branch }}
|
||||
cacheKey: ${{ github.sha }}-base:build
|
||||
|
||||
notify-on-failure:
|
||||
name: Notify Slack on failure
|
||||
|
|
|
|||
121
.github/workflows/ci-postgres-mysql.yml
vendored
121
.github/workflows/ci-postgres-mysql.yml
vendored
|
|
@ -21,41 +21,24 @@ concurrency:
|
|||
group: db-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=3072'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Install & Build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2204
|
||||
if: github.event_name != 'pull_request_review' || startsWith(github.event.pull_request.base.ref, 'release/')
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 22.x
|
||||
|
||||
- name: Setup corepack and pnpm
|
||||
run: |
|
||||
npm i -g corepack@0.33
|
||||
corepack enable
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup build cache
|
||||
uses: rharkor/caching-for-turbo@439abec0d28d21b192fa8817b744ffdf1ee5ac0d # v1.5
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Cache build artifacts
|
||||
uses: actions/cache/save@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
with:
|
||||
path: ./packages/**/dist
|
||||
key: ${{ github.sha }}:db-tests
|
||||
- name: Setup and Build
|
||||
uses: n8n-io/n8n/.github/actions/setup-nodejs-blacksmith@f5fbbbe0a28a886451c886cac6b49192a39b0eea # v1.104.1
|
||||
|
||||
sqlite-pooled:
|
||||
name: SQLite Pooled
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2204
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
DB_TYPE: sqlite
|
||||
|
|
@ -63,25 +46,8 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 22.x
|
||||
|
||||
- name: Setup corepack and pnpm
|
||||
run: |
|
||||
npm i -g corepack@0.33
|
||||
corepack enable
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup build cache
|
||||
uses: rharkor/caching-for-turbo@439abec0d28d21b192fa8817b744ffdf1ee5ac0d # v1.5
|
||||
|
||||
- name: Restore cached build artifacts
|
||||
uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
with:
|
||||
path: ./packages/**/dist
|
||||
key: ${{ github.sha }}:db-tests
|
||||
- name: Setup and Build
|
||||
uses: n8n-io/n8n/.github/actions/setup-nodejs-blacksmith@f5fbbbe0a28a886451c886cac6b49192a39b0eea # v1.104.1
|
||||
|
||||
- name: Test SQLite Pooled
|
||||
working-directory: packages/cli
|
||||
|
|
@ -89,33 +55,16 @@ jobs:
|
|||
|
||||
mariadb:
|
||||
name: MariaDB
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2204
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
DB_MYSQLDB_PASSWORD: password
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 22.x
|
||||
|
||||
- name: Setup corepack and pnpm
|
||||
run: |
|
||||
npm i -g corepack@0.33
|
||||
corepack enable
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup build cache
|
||||
uses: rharkor/caching-for-turbo@439abec0d28d21b192fa8817b744ffdf1ee5ac0d # v1.5
|
||||
|
||||
- name: Restore cached build artifacts
|
||||
uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
with:
|
||||
path: ./packages/**/dist
|
||||
key: ${{ github.sha }}:db-tests
|
||||
- name: Setup and Build
|
||||
uses: n8n-io/n8n/.github/actions/setup-nodejs-blacksmith@f5fbbbe0a28a886451c886cac6b49192a39b0eea # v1.104.1
|
||||
|
||||
- name: Start MariaDB
|
||||
uses: isbang/compose-action@802a148945af6399a338c7906c267331b39a71af # v2.0.0
|
||||
|
|
@ -130,8 +79,8 @@ jobs:
|
|||
|
||||
mysql:
|
||||
name: MySQL (${{ matrix.service-name }})
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2204
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
matrix:
|
||||
|
|
@ -141,25 +90,8 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 22.x
|
||||
|
||||
- name: Setup corepack and pnpm
|
||||
run: |
|
||||
npm i -g corepack@0.33
|
||||
corepack enable
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup build cache
|
||||
uses: rharkor/caching-for-turbo@439abec0d28d21b192fa8817b744ffdf1ee5ac0d # v1.5
|
||||
|
||||
- name: Restore cached build artifacts
|
||||
uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
with:
|
||||
path: ./packages/**/dist
|
||||
key: ${{ github.sha }}:db-tests
|
||||
- name: Setup and Build
|
||||
uses: n8n-io/n8n/.github/actions/setup-nodejs-blacksmith@f5fbbbe0a28a886451c886cac6b49192a39b0eea # v1.104.1
|
||||
|
||||
- name: Start MySQL
|
||||
uses: isbang/compose-action@802a148945af6399a338c7906c267331b39a71af # v2.0.0
|
||||
|
|
@ -174,8 +106,8 @@ jobs:
|
|||
|
||||
postgres:
|
||||
name: Postgres
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2204
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
DB_POSTGRESDB_PASSWORD: password
|
||||
|
|
@ -183,25 +115,8 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 22.x
|
||||
|
||||
- name: Setup corepack and pnpm
|
||||
run: |
|
||||
npm i -g corepack@0.33
|
||||
corepack enable
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup build cache
|
||||
uses: rharkor/caching-for-turbo@439abec0d28d21b192fa8817b744ffdf1ee5ac0d # v1.5
|
||||
|
||||
- name: Restore cached build artifacts
|
||||
uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
with:
|
||||
path: ./packages/**/dist
|
||||
key: ${{ github.sha }}:db-tests
|
||||
- name: Setup and Build
|
||||
uses: n8n-io/n8n/.github/actions/setup-nodejs-blacksmith@f5fbbbe0a28a886451c886cac6b49192a39b0eea # v1.104.1
|
||||
|
||||
- name: Start Postgres
|
||||
uses: isbang/compose-action@802a148945af6399a338c7906c267331b39a71af # v2.0.0
|
||||
|
|
|
|||
14
.github/workflows/ci-pull-requests.yml
vendored
14
.github/workflows/ci-pull-requests.yml
vendored
|
|
@ -11,7 +11,7 @@ jobs:
|
|||
name: Install & Build
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2204
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=4096'
|
||||
NODE_OPTIONS: '--max-old-space-size=3072'
|
||||
outputs:
|
||||
frontend_changed: ${{ steps.paths-filter.outputs.frontend == 'true' }}
|
||||
steps:
|
||||
|
|
@ -31,14 +31,10 @@ jobs:
|
|||
- packages/@n8n/codemirror-lang/**
|
||||
- .bundlemonrc.json
|
||||
- .github/workflows/ci-pull-requests.yml
|
||||
- name: Setup and Build
|
||||
uses: n8n-io/n8n/.github/actions/setup-nodejs-blacksmith@f5fbbbe0a28a886451c886cac6b49192a39b0eea # v1.104.1
|
||||
|
||||
- name: Setup Environment and Build Project
|
||||
uses: ./.github/actions/setup-and-build
|
||||
with:
|
||||
node-version: 22.x
|
||||
enable-caching: true
|
||||
|
||||
- name: Run formatcheck
|
||||
- name: Run format check
|
||||
run: pnpm format:check
|
||||
|
||||
- name: Run typecheck
|
||||
|
|
@ -89,7 +85,6 @@ jobs:
|
|||
needs: install-and-build
|
||||
with:
|
||||
ref: refs/pull/${{ github.event.pull_request.number }}/merge
|
||||
cacheKey: ${{ github.sha }}-base:build
|
||||
collectCoverage: true
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
|
@ -100,4 +95,3 @@ jobs:
|
|||
needs: install-and-build
|
||||
with:
|
||||
ref: refs/pull/${{ github.event.pull_request.number }}/merge
|
||||
cacheKey: ${{ github.sha }}-base:build
|
||||
|
|
|
|||
48
.github/workflows/claude.yml
vendored
Normal file
48
.github/workflows/claude.yml
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
name: Claude PR Assistant
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude-code-action:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && contains(github.event.issue.body, '@claude'))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude PR Action
|
||||
uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
# Or use OAuth token instead:
|
||||
# claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
timeout_minutes: '60'
|
||||
# mode: tag # Default: responds to @claude mentions
|
||||
# Optional: Restrict network access to specific domains only
|
||||
# experimental_allowed_domains: |
|
||||
# .anthropic.com
|
||||
# .github.com
|
||||
# api.github.com
|
||||
# .githubusercontent.com
|
||||
# bun.sh
|
||||
# registry.npmjs.org
|
||||
# .blob.core.windows.net
|
||||
24
.github/workflows/docker-build-push.yml
vendored
24
.github/workflows/docker-build-push.yml
vendored
|
|
@ -7,7 +7,7 @@
|
|||
name: 'Docker: Build and Push'
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=8192'
|
||||
NODE_OPTIONS: '--max-old-space-size=7168'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
|
|
@ -166,26 +166,10 @@ jobs:
|
|||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
- name: Setup and Build
|
||||
uses: n8n-io/n8n/.github/actions/setup-nodejs-blacksmith@f5fbbbe0a28a886451c886cac6b49192a39b0eea # v1.104.1
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: useblacksmith/setup-node@65c6ca86fdeb0ab3d85e78f57e4f6a7e4780b391 # v5.0.4
|
||||
with:
|
||||
node-version: 22.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
shell: bash
|
||||
|
||||
- name: Configure Turborepo Cache
|
||||
uses: useblacksmith/caching-for-turbo@bafb57e7ebdbf1185762286ec94d24648cd3938a # v1
|
||||
|
||||
- name: Build n8n for Docker
|
||||
run: pnpm build:n8n
|
||||
shell: bash
|
||||
build-command: pnpm build:n8n
|
||||
|
||||
- name: Determine Docker tags
|
||||
id: determine-tags
|
||||
|
|
|
|||
70
.github/workflows/e2e-flaky.yml
vendored
70
.github/workflows/e2e-flaky.yml
vendored
|
|
@ -1,70 +0,0 @@
|
|||
name: Debug Flaky E2E Test
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
test_name:
|
||||
description: 'The name of the test to filter.'
|
||||
required: true
|
||||
type: string
|
||||
burn_count:
|
||||
description: 'Number of times to run the test.'
|
||||
required: false
|
||||
type: number
|
||||
default: 50
|
||||
branch:
|
||||
description: 'Optional: GitHub branch, tag, or SHA to test. Defaults to the branch selected in UI.'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
debug-test:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 22.x
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache build artifacts
|
||||
id: cache-build-artifacts
|
||||
uses: useblacksmith/cache@c5fe29eb0efdf1cf4186b9f7fcbbcbc0cf025662 # v5.0.2
|
||||
with:
|
||||
path: |
|
||||
/home/runner/.cache/Cypress
|
||||
./packages/**/dist
|
||||
key: ${{ github.ref }}-${{ github.sha }}-debug-build
|
||||
restore-keys: |
|
||||
${{ github.ref }}-debug-build-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build application
|
||||
if: steps.cache-build-artifacts.outputs.cache-hit != 'true'
|
||||
run: pnpm build
|
||||
|
||||
- name: Cypress install
|
||||
if: steps.cache-build-artifacts.outputs.cache-hit != 'true'
|
||||
working-directory: cypress
|
||||
run: pnpm cypress:install
|
||||
|
||||
- name: Run Flaky Debug Command
|
||||
env:
|
||||
TEST_NAME: ${{ github.event.inputs.test_name }}
|
||||
BURN_COUNT: ${{ github.event.inputs.burn_count }}
|
||||
NODE_OPTIONS: --dns-result-order=ipv4first
|
||||
E2E_TESTS: true
|
||||
SHELL: /bin/sh
|
||||
run: pnpm run debug:flaky:e2e "${{ env.TEST_NAME }}" "${{ env.BURN_COUNT }}"
|
||||
2
.github/workflows/e2e-reusable.yml
vendored
2
.github/workflows/e2e-reusable.yml
vendored
|
|
@ -45,7 +45,7 @@ on:
|
|||
required: true
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
NODE_OPTIONS: --max-old-space-size=3072
|
||||
|
||||
jobs:
|
||||
testing:
|
||||
|
|
|
|||
27
.github/workflows/linting-reusable.yml
vendored
27
.github/workflows/linting-reusable.yml
vendored
|
|
@ -13,11 +13,9 @@ on:
|
|||
required: false
|
||||
type: string
|
||||
default: 22.x
|
||||
cacheKey:
|
||||
description: Cache key for modules and build artifacts.
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=7168
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
|
|
@ -28,21 +26,8 @@ jobs:
|
|||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- name: Setup Environment
|
||||
uses: n8n-io/n8n/.github/actions/setup-and-build@7e870b8f7f5a39bb8bf82d1f42b6d44febc0082c # v1.100.1
|
||||
- name: Build and Test
|
||||
uses: n8n-io/n8n/.github/actions/setup-nodejs-blacksmith@f5fbbbe0a28a886451c886cac6b49192a39b0eea # v1.104.1
|
||||
with:
|
||||
build-command: pnpm lint
|
||||
node-version: ${{ inputs.nodeVersion }}
|
||||
enable-caching: true
|
||||
skip-build: ${{ inputs.cacheKey != '' }}
|
||||
|
||||
- name: Lint Backend
|
||||
run: pnpm lint:backend
|
||||
|
||||
- name: Lint Nodes
|
||||
run: pnpm lint:nodes
|
||||
|
||||
- name: Lint Frontend
|
||||
run: pnpm lint:frontend
|
||||
|
||||
- name: Lint Testing
|
||||
run: pnpm lint:testing
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ on:
|
|||
|
||||
env:
|
||||
PLAYWRIGHT_BROWSERS_PATH: packages/testing/playwright/ms-playwright-cache
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
NODE_OPTIONS: --max-old-space-size=3072
|
||||
# Disable Ryuk to avoid issues with Docker since it needs privileged access, containers are cleaned on teardown anyway
|
||||
TESTCONTAINERS_RYUK_DISABLED: true
|
||||
|
||||
|
|
|
|||
213
.github/workflows/test-workflows-callable.yml
vendored
213
.github/workflows/test-workflows-callable.yml
vendored
|
|
@ -1,4 +1,4 @@
|
|||
name: Callable Test Workflows
|
||||
name: Test Workflows - Reusable
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
|
@ -7,208 +7,47 @@ on:
|
|||
description: 'The Git ref (branch, tag, or SHA) to checkout and test.'
|
||||
required: true
|
||||
type: string
|
||||
send_webhook_report:
|
||||
description: 'Set to true to send test results to the webhook.'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
pr_number:
|
||||
description: 'The PR number, if applicable (for context in webhook).'
|
||||
compare_schemas:
|
||||
description: 'Set to "true" to enable schema comparison during tests.'
|
||||
required: false
|
||||
default: 'true'
|
||||
type: string
|
||||
default: ''
|
||||
secrets:
|
||||
ENCRYPTION_KEY:
|
||||
description: 'Encryption key for n8n operations.'
|
||||
required: true
|
||||
CI_SENTRY_DSN:
|
||||
description: 'Sentry DSN for CI test runs.'
|
||||
required: false
|
||||
WORKFLOW_TESTS_RESULT_DESTINATION:
|
||||
description: 'Webhook URL to send test results to (if enabled).'
|
||||
required: false
|
||||
CURRENTS_RECORD_KEY:
|
||||
description: 'Currents record key for uploading test results.'
|
||||
required: true
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=3072
|
||||
|
||||
jobs:
|
||||
build_and_test:
|
||||
name: Install, Build, and Test Workflows
|
||||
run_workflow_tests:
|
||||
name: Run Workflow Tests with Snapshots
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2204
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
N8N_ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.git_ref }}
|
||||
|
||||
- name: Setup Environment and Build Project
|
||||
uses: n8n-io/n8n/.github/actions/setup-and-build@7e870b8f7f5a39bb8bf82d1f42b6d44febc0082c # v1.100.1
|
||||
with:
|
||||
node-version: '22.x'
|
||||
cache-suffix: 'workflow-test'
|
||||
- name: Set up Environment
|
||||
uses: n8n-io/n8n/.github/actions/setup-nodejs-blacksmith@f5fbbbe0a28a886451c886cac6b49192a39b0eea # v1.104.1
|
||||
|
||||
- name: Install OS dependencies
|
||||
run: |
|
||||
sudo apt update -y
|
||||
echo 'tzdata tzdata/Areas select Europe' | sudo debconf-set-selections
|
||||
echo 'tzdata tzdata/Zones/Europe select Paris' | sudo debconf-set-selections
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends graphicsmagick
|
||||
sudo apt-get clean
|
||||
sudo rm -rf /var/lib/apt/lists/*
|
||||
|
||||
- name: Import credentials
|
||||
run: ./packages/cli/bin/n8n import:credentials --input=test-workflows/credentials.json
|
||||
|
||||
- name: Import workflows
|
||||
run: ./packages/cli/bin/n8n import:workflow --separate --input=test-workflows/workflows
|
||||
|
||||
- name: Copy static assets
|
||||
run: |
|
||||
mkdir -p /tmp/testData/pdfs
|
||||
cp assets/n8n-logo.png /tmp/n8n-logo.png
|
||||
cp assets/n8n-screenshot.png /tmp/n8n-screenshot.png
|
||||
cp test-workflows/testData/pdfs/*.pdf /tmp/testData/pdfs/
|
||||
|
||||
- name: Run tests
|
||||
id: tests
|
||||
run: ./packages/cli/bin/n8n executeBatch --shallow --skipList=test-workflows/skipList.json --githubWorkflow --shortOutput --output=test-results.json --concurrency=16 --compare=test-workflows/snapshots
|
||||
continue-on-error: true
|
||||
- name: Set up Workflow Tests
|
||||
run: pnpm --filter=n8n-playwright test:workflows:setup
|
||||
env:
|
||||
SKIP_STATISTICS_EVENTS: 'true'
|
||||
DB_SQLITE_POOL_SIZE: '4'
|
||||
N8N_SENTRY_DSN: ${{ secrets.CI_SENTRY_DSN }}
|
||||
N8N_ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }}
|
||||
|
||||
- name: Report test outcome
|
||||
if: always()
|
||||
run: |
|
||||
echo "Test step outcome was: ${{ steps.tests.outcome }}"
|
||||
if [[ "${{ steps.tests.outcome }}" == "failure" ]]; then
|
||||
echo "Workflow tests failed but the workflow will continue."
|
||||
elif [[ "${{ steps.tests.outcome }}" == "success" ]]; then
|
||||
echo "Workflow tests passed."
|
||||
else
|
||||
echo "Workflow tests outcome: ${{ steps.tests.outcome }}"
|
||||
fi
|
||||
|
||||
- name: Prepare and Send Test Results to Webhook
|
||||
if: inputs.send_webhook_report == true
|
||||
shell: bash
|
||||
- name: Run Workflow Tests
|
||||
run: pnpm --filter=n8n-playwright test:workflows --workers 4
|
||||
env:
|
||||
WEBHOOK_URL: ${{ secrets.WORKFLOW_TESTS_RESULT_DESTINATION }}
|
||||
TEST_RESULTS_FILE: ./test-results.json
|
||||
GH_REPOSITORY: ${{ github.repository }}
|
||||
GH_RUN_ID: ${{ github.run_id }}
|
||||
GH_RUN_ATTEMPT: ${{ github.run_attempt }}
|
||||
GH_REF_TESTED: ${{ inputs.git_ref }}
|
||||
GH_EVENT_NAME: ${{ github.event_name }}
|
||||
GH_PR_NUMBER_INPUT: ${{ inputs.pr_number }}
|
||||
GH_WORKFLOW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
GH_ACTOR: ${{ github.actor }}
|
||||
run: |
|
||||
echo "Attempting to send test results to webhook..."
|
||||
echo "Test results file expected at: $TEST_RESULTS_FILE"
|
||||
CURRENTS_RECORD_KEY: ${{ secrets.CURRENTS_RECORD_KEY }}
|
||||
|
||||
if [ ! -f "$TEST_RESULTS_FILE" ]; then
|
||||
echo "::warning::Test results file ($TEST_RESULTS_FILE) not found. Skipping webhook."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo "jq not found. Installing jq..."
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq jq
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo "::error::Failed to install jq. Cannot process JSON."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
pr_number_to_send="$GH_PR_NUMBER_INPUT"
|
||||
|
||||
echo "Preparing JSON payload..."
|
||||
if [ ! -s "$TEST_RESULTS_FILE" ]; then
|
||||
echo "::warning::Test results file ($TEST_RESULTS_FILE) is empty. Sending only GitHub context."
|
||||
enriched_payload=$(jq -n \
|
||||
--arg repository "$GH_REPOSITORY" \
|
||||
--arg run_id "$GH_RUN_ID" \
|
||||
--arg run_attempt "$GH_RUN_ATTEMPT" \
|
||||
--arg ref_tested "$GH_REF_TESTED" \
|
||||
--arg event_name "$GH_EVENT_NAME" \
|
||||
--arg pr_num "$pr_number_to_send" \
|
||||
--arg workflow_run_url "$GH_WORKFLOW_RUN_URL" \
|
||||
--arg actor "$GH_ACTOR" \
|
||||
'{
|
||||
githubWorkflowContext: {
|
||||
repository: $repository,
|
||||
runId: $run_id,
|
||||
runAttempt: $run_attempt,
|
||||
gitRefTested: $ref_tested,
|
||||
triggeringEventName: $event_name,
|
||||
prNumber: (if $pr_num == "" then null else $pr_num | tonumber? // $pr_num end),
|
||||
workflowRunUrl: $workflow_run_url,
|
||||
triggeredBy: $actor
|
||||
}
|
||||
}')
|
||||
else
|
||||
enriched_payload=$(jq \
|
||||
--arg repository "$GH_REPOSITORY" \
|
||||
--arg run_id "$GH_RUN_ID" \
|
||||
--arg run_attempt "$GH_RUN_ATTEMPT" \
|
||||
--arg ref_tested "$GH_REF_TESTED" \
|
||||
--arg event_name "$GH_EVENT_NAME" \
|
||||
--arg pr_num "$pr_number_to_send" \
|
||||
--arg workflow_run_url "$GH_WORKFLOW_RUN_URL" \
|
||||
--arg actor "$GH_ACTOR" \
|
||||
'. + {
|
||||
githubWorkflowContext: {
|
||||
repository: $repository,
|
||||
runId: $run_id,
|
||||
runAttempt: $run_attempt,
|
||||
gitRefTested: $ref_tested,
|
||||
triggeringEventName: $event_name,
|
||||
prNumber: (if $pr_num == "" then null else $pr_num | tonumber? // $pr_num end),
|
||||
workflowRunUrl: $workflow_run_url,
|
||||
triggeredBy: $actor
|
||||
}
|
||||
}' "$TEST_RESULTS_FILE")
|
||||
fi
|
||||
|
||||
jq_exit_code=$?
|
||||
if [ $jq_exit_code -ne 0 ] || [ -z "$enriched_payload" ]; then
|
||||
echo "::error::Failed to process JSON with jq (exit code: $jq_exit_code). Input file: $TEST_RESULTS_FILE"
|
||||
if [ -s "$TEST_RESULTS_FILE" ]; then
|
||||
echo "Contents of $TEST_RESULTS_FILE that may have caused an error:"
|
||||
head -c 1000 "$TEST_RESULTS_FILE" # Print first 1000 chars
|
||||
echo "" # Newline after head
|
||||
elif [ -f "$TEST_RESULTS_FILE" ]; then
|
||||
echo "$TEST_RESULTS_FILE exists but is empty."
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Enriched payload to send (first 500 chars):"
|
||||
echo "$enriched_payload" | head -c 500
|
||||
echo ""
|
||||
|
||||
echo "Sending data to webhook: $WEBHOOK_URL"
|
||||
http_response_code=$(curl -s -w "%{http_code}" \
|
||||
-X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-GitHub-Event: $GH_EVENT_NAME" \
|
||||
-H "X-GitHub-Run-Id: $GH_RUN_ID" \
|
||||
--data "$enriched_payload" \
|
||||
"$WEBHOOK_URL" \
|
||||
-o curl_response_body.txt 2>curl_stderr.txt)
|
||||
|
||||
curl_stderr_content=$(cat curl_stderr.txt)
|
||||
if [ -n "$curl_stderr_content" ]; then
|
||||
echo "::warning::curl stderr: $curl_stderr_content"
|
||||
fi
|
||||
echo "Webhook response code: $http_response_code"
|
||||
echo "Webhook response body:"
|
||||
cat curl_response_body.txt
|
||||
if [[ "$http_response_code" -ge 200 && "$http_response_code" -lt 300 ]]; then
|
||||
echo "Successfully sent data to webhook."
|
||||
else
|
||||
echo "::error::Webhook call failed with status code $http_response_code."
|
||||
fi
|
||||
- name: Run Workflow Schema Tests
|
||||
if: ${{ inputs.compare_schemas == 'true' }}
|
||||
run: pnpm --filter=n8n-playwright test:workflows:schema
|
||||
env:
|
||||
CURRENTS_RECORD_KEY: ${{ secrets.CURRENTS_RECORD_KEY }}
|
||||
|
|
|
|||
8
.github/workflows/test-workflows-nightly.yml
vendored
8
.github/workflows/test-workflows-nightly.yml
vendored
|
|
@ -11,16 +11,10 @@ on:
|
|||
type: string
|
||||
default: 'master'
|
||||
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
run_workflow_tests:
|
||||
name: Run Workflow Tests
|
||||
uses: ./.github/workflows/test-workflows-callable.yml
|
||||
with:
|
||||
git_ref: ${{ github.event_name == 'schedule' && 'master' || github.event.inputs.git_ref_to_test }}
|
||||
send_webhook_report: false
|
||||
pr_number: ''
|
||||
secrets: inherit
|
||||
secrets: inherit
|
||||
|
|
|
|||
|
|
@ -22,6 +22,4 @@ jobs:
|
|||
uses: ./.github/workflows/test-workflows-callable.yml
|
||||
with:
|
||||
git_ref: ${{ github.event.pull_request.head.sha }}
|
||||
send_webhook_report: true
|
||||
pr_number: ${{ github.event.pull_request.number }}
|
||||
secrets: inherit
|
||||
secrets: inherit
|
||||
|
|
|
|||
|
|
@ -107,6 +107,4 @@ jobs:
|
|||
uses: ./.github/workflows/test-workflows-callable.yml
|
||||
with:
|
||||
git_ref: ${{ needs.handle_comment_command.outputs.git_ref }}
|
||||
send_webhook_report: true
|
||||
pr_number: ${{ needs.handle_comment_command.outputs.pr_number }}
|
||||
secrets: inherit
|
||||
|
|
|
|||
41
.github/workflows/units-tests-reusable.yml
vendored
41
.github/workflows/units-tests-reusable.yml
vendored
|
|
@ -13,63 +13,36 @@ on:
|
|||
required: false
|
||||
type: string
|
||||
default: 22.x
|
||||
cacheKey:
|
||||
description: Cache key for modules and build artifacts.
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
collectCoverage:
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
ignoreTurboCache:
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
skipFrontendTests:
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
secrets:
|
||||
CODECOV_TOKEN:
|
||||
description: 'Codecov upload token.'
|
||||
required: false
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=7168
|
||||
|
||||
jobs:
|
||||
unit-test:
|
||||
name: Unit tests
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
env:
|
||||
TURBO_FORCE: ${{ inputs.ignoreTurboCache }}
|
||||
COVERAGE_ENABLED: ${{ inputs.collectCoverage }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- name: Setup Environment and Build Project
|
||||
uses: n8n-io/n8n/.github/actions/setup-and-build@7e870b8f7f5a39bb8bf82d1f42b6d44febc0082c # v1.100.1
|
||||
- name: Build
|
||||
uses: n8n-io/n8n/.github/actions/setup-nodejs-blacksmith@f5fbbbe0a28a886451c886cac6b49192a39b0eea # v1.104.1
|
||||
with:
|
||||
node-version: ${{ inputs.nodeVersion }}
|
||||
skip-build: ${{ inputs.cacheKey != '' }}
|
||||
|
||||
- name: Restore cached build artifacts only
|
||||
if: ${{ inputs.cacheKey != '' }}
|
||||
uses: useblacksmith/cache/restore@c5fe29eb0efdf1cf4186b9f7fcbbcbc0cf025662 # v5.0.2
|
||||
with:
|
||||
path: ./packages/**/dist
|
||||
key: ${{ inputs.cacheKey }}
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Test Backend
|
||||
run: pnpm test:backend
|
||||
|
||||
- name: Test Nodes
|
||||
run: pnpm test:nodes
|
||||
|
||||
- name: Test Frontend
|
||||
if: ${{ !inputs.skipFrontendTests }}
|
||||
run: pnpm test:frontend
|
||||
- name: Test
|
||||
run: pnpm test:ci
|
||||
|
||||
- name: Upload test results to Codecov
|
||||
if: ${{ !cancelled() }}
|
||||
|
|
|
|||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -33,5 +33,4 @@ test-results/
|
|||
compiled_app_output
|
||||
trivy_report*
|
||||
compiled
|
||||
jest.config.js
|
||||
packages/cli/src/modules/my-feature
|
||||
packages/cli/src/modules/my-feature
|
||||
|
|
|
|||
60
CHANGELOG.md
60
CHANGELOG.md
|
|
@ -1,3 +1,63 @@
|
|||
# [1.105.0](https://github.com/n8n-io/n8n/compare/n8n@1.104.0...n8n@1.105.0) (2025-07-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** Add headers to telemetry cross origin request ([#17631](https://github.com/n8n-io/n8n/issues/17631)) ([251e892](https://github.com/n8n-io/n8n/commit/251e892a09b82b2f1f980d3984e3aef84ed1732e))
|
||||
* **core:** Decouple removing and closing destination from actually deleting it ([#17614](https://github.com/n8n-io/n8n/issues/17614)) ([b09f737](https://github.com/n8n-io/n8n/commit/b09f73701d8b6ce0e3bc4ef28d0f5d9fc7fb5df1))
|
||||
* **core:** Fetching schema files in Docker ([#17623](https://github.com/n8n-io/n8n/issues/17623)) ([5a3b0a2](https://github.com/n8n-io/n8n/commit/5a3b0a24811eea5ebd9d80c26a54fea74665569f))
|
||||
* **core:** Fix getLiveExecutionRowsOnPostgres when there are multiple n8n schemas ([#17635](https://github.com/n8n-io/n8n/issues/17635)) ([9e3bfff](https://github.com/n8n-io/n8n/commit/9e3bfff68d35a2eb21ba43452fc01ee24601c122))
|
||||
* **core:** Ignore pairedItem when checking for incorrect output data from a node ([#17340](https://github.com/n8n-io/n8n/issues/17340)) ([2708fe8](https://github.com/n8n-io/n8n/commit/2708fe81a5323687c59c3d483d6bf3c67464f657))
|
||||
* **core:** Make the module loading for local dev more generic ([#17547](https://github.com/n8n-io/n8n/issues/17547)) ([01b95a9](https://github.com/n8n-io/n8n/commit/01b95a9ee5dd4985e4770ef625ced91158f849da))
|
||||
* **core:** Optimize connection type lookups ([#17585](https://github.com/n8n-io/n8n/issues/17585)) ([70eab1b](https://github.com/n8n-io/n8n/commit/70eab1b2a02d70a46a56e8c993ccc694e38ac2d5))
|
||||
* **core:** Remove non-included nodes regardless of the package when using NODES_INCLUDE ([#17517](https://github.com/n8n-io/n8n/issues/17517)) ([1641d39](https://github.com/n8n-io/n8n/commit/1641d3964b72539354a939400af91e2692368058))
|
||||
* Do not throw on tool errors, instead return error message ([#17558](https://github.com/n8n-io/n8n/issues/17558)) ([f11ec53](https://github.com/n8n-io/n8n/commit/f11ec538dca2938e57302a1bedd5dd7d1e7a9488))
|
||||
* **editor:** Add background same as that of the parent ([#17625](https://github.com/n8n-io/n8n/issues/17625)) ([8660057](https://github.com/n8n-io/n8n/commit/8660057350e21b604b3fb3f627ccd32659058e87))
|
||||
* **editor:** Case-sensitive credential search in `NodeCredentials` component ([#17564](https://github.com/n8n-io/n8n/issues/17564)) ([3ce9a99](https://github.com/n8n-io/n8n/commit/3ce9a998ae454929207dd9add4a67b68dba13bc8))
|
||||
* **editor:** Do not show new NDV for sticky notes ([#17537](https://github.com/n8n-io/n8n/issues/17537)) ([4de3759](https://github.com/n8n-io/n8n/commit/4de3759a59cade3f82c57a1eeba1c6b4a16a3eaf))
|
||||
* **editor:** Fix canvas layouting when tab is not active ([#17638](https://github.com/n8n-io/n8n/issues/17638)) ([2df76e0](https://github.com/n8n-io/n8n/commit/2df76e020ef3a962fc991f2d108a8181914a2dd1))
|
||||
* **editor:** Fix error when there is no path back to referenced node ([#16059](https://github.com/n8n-io/n8n/issues/16059)) ([d6ac924](https://github.com/n8n-io/n8n/commit/d6ac924b3b7d2205cbcc0e5edc7ad407f4fe2a19))
|
||||
* **editor:** Fix layout of binary data preview in the log view ([#17584](https://github.com/n8n-io/n8n/issues/17584)) ([456c4e8](https://github.com/n8n-io/n8n/commit/456c4e8167ed95e5f096daaae9cc46cad90a0981))
|
||||
* **editor:** Fix trimPayloadToSize mutating original objects in AI assistant ([#17498](https://github.com/n8n-io/n8n/issues/17498)) ([1010043](https://github.com/n8n-io/n8n/commit/101004390bf5cdf5f67675dcfccb551f71ea4b70))
|
||||
* **editor:** Hide `What's New` notification in executions demo view ([#17742](https://github.com/n8n-io/n8n/issues/17742)) ([cebb1f6](https://github.com/n8n-io/n8n/commit/cebb1f65669638a6716dbbd2eb9873ae8dbfe108))
|
||||
* **editor:** Improve filter change handling with debounced updates for date fields ([#17618](https://github.com/n8n-io/n8n/issues/17618)) ([ae08917](https://github.com/n8n-io/n8n/commit/ae089173a71b3417ca07ae4bf49d4b0b3d31bf09))
|
||||
* **editor:** Make inline text edit component reactive to prop changes ([#17557](https://github.com/n8n-io/n8n/issues/17557)) ([9c793a4](https://github.com/n8n-io/n8n/commit/9c793a45c562631ec331f65ca871334f5a8a8e2f))
|
||||
* **editor:** Make sure HTML editor field is not editable when workflow is in read only mode ([#17561](https://github.com/n8n-io/n8n/issues/17561)) ([18c02df](https://github.com/n8n-io/n8n/commit/18c02dfa2b5cf76663b4678046a8bcb313fba1f4))
|
||||
* **editor:** Persist SSO protocol setting properly in the UI ([#17572](https://github.com/n8n-io/n8n/issues/17572)) ([4b2be26](https://github.com/n8n-io/n8n/commit/4b2be263790a53bf46b99f3301ddec6a771b2daf))
|
||||
* **editor:** Prevent default action on Enter key in commit and push dialog ([#17578](https://github.com/n8n-io/n8n/issues/17578)) ([e317c92](https://github.com/n8n-io/n8n/commit/e317c929161733a03ff61c07ae6f3ae12cf22ef2))
|
||||
* **editor:** Prevent unnecessary updates on model value change in InlineTextEdit component ([#17553](https://github.com/n8n-io/n8n/issues/17553)) ([832b7fd](https://github.com/n8n-io/n8n/commit/832b7fda3b59cc518624128ca98d26983cb444fd))
|
||||
* **editor:** Remove inline script and style from index.html ([#17531](https://github.com/n8n-io/n8n/issues/17531)) ([0db24ce](https://github.com/n8n-io/n8n/commit/0db24ce71b671f6311fc47ac9553466d34c46ba8))
|
||||
* **editor:** Render HTML in the log view ([#17586](https://github.com/n8n-io/n8n/issues/17586)) ([46635c5](https://github.com/n8n-io/n8n/commit/46635c59418630c2f24fce5cb8c25e425eddc3c2))
|
||||
* **editor:** Tweak configurable node width ([#17512](https://github.com/n8n-io/n8n/issues/17512)) ([3825f8a](https://github.com/n8n-io/n8n/commit/3825f8a806fcc67a33f43ce6ebd71b6a8023d7d8))
|
||||
* **GitHub Document Loader Node:** Fix node loading issue ([#17494](https://github.com/n8n-io/n8n/issues/17494)) ([8fb3d8d](https://github.com/n8n-io/n8n/commit/8fb3d8d5870682af4b8b0c31949b5c1569a70d90))
|
||||
* **Google Gemini Node:** Error when used as tool with "Message a model" operation ([#17491](https://github.com/n8n-io/n8n/issues/17491)) ([f30cc7b](https://github.com/n8n-io/n8n/commit/f30cc7b6cfba6998091f31fcd3012a971b3a2bb8))
|
||||
* **Google Sheets Node:** Get Rows operation returns an empty string when the cell has a value of 0 ([#17642](https://github.com/n8n-io/n8n/issues/17642)) ([9808783](https://github.com/n8n-io/n8n/commit/980878398e9f6b498ba7079492e92bdba6fa6778))
|
||||
* **MySQL Node:** Do not replace $ values with null ([#17327](https://github.com/n8n-io/n8n/issues/17327)) ([4b626e5](https://github.com/n8n-io/n8n/commit/4b626e528219c0610528a1119a2bb60c8442952d))
|
||||
* **OpenAI Node:** Fix memory connector for assistant message ([#17501](https://github.com/n8n-io/n8n/issues/17501)) ([e51b056](https://github.com/n8n-io/n8n/commit/e51b056e3a8fd79c73b0a87eaf6595b7f03d546b))
|
||||
* Prevent error when importing nodes with malformed collection params ([#17580](https://github.com/n8n-io/n8n/issues/17580)) ([4713827](https://github.com/n8n-io/n8n/commit/4713827813809065c8800adc7c0cd4bf42f54eeb))
|
||||
* **RabbitMQ Trigger Node:** Respect the "Delete From Queue When" option with manual executions ([#17554](https://github.com/n8n-io/n8n/issues/17554)) ([2bd0aa3](https://github.com/n8n-io/n8n/commit/2bd0aa38e24dcada0777921d457586d35095ac42))
|
||||
* **Telegram Node:** Determine the MIME type when downloading the file ([#17725](https://github.com/n8n-io/n8n/issues/17725)) ([a9c29e3](https://github.com/n8n-io/n8n/commit/a9c29e340adf370a65222600f3fac6884642c747))
|
||||
* Update packages for security fixes ([#17733](https://github.com/n8n-io/n8n/issues/17733)) ([edeb8ef](https://github.com/n8n-io/n8n/commit/edeb8ef8a437f30a6c37826ad1eccb6a35a4d3bc))
|
||||
* Update settings icons on canvas style ([#17636](https://github.com/n8n-io/n8n/issues/17636)) ([0338ebb](https://github.com/n8n-io/n8n/commit/0338ebb3dde3be4050ee869fb056f67827a764b2))
|
||||
* **Webhook Node:** Don't wrap response in an iframe if it doesn't have HTML ([#17671](https://github.com/n8n-io/n8n/issues/17671)) ([69beafb](https://github.com/n8n-io/n8n/commit/69beafbf7127d6492fc875ab243e6f2e174e61ec))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **core:** Increase Cron observability ([#17626](https://github.com/n8n-io/n8n/issues/17626)) ([08c38a7](https://github.com/n8n-io/n8n/commit/08c38a76f384642c09fab6fc47f76bffd532a5b8))
|
||||
* **editor:** Add dragging and hiding for evaluation table columns ([#17587](https://github.com/n8n-io/n8n/issues/17587)) ([921cdb6](https://github.com/n8n-io/n8n/commit/921cdb6fd0ff11793a2ec08faee28b1c5842e25b))
|
||||
* **editor:** Add follow up question nps ([#17459](https://github.com/n8n-io/n8n/issues/17459)) ([e18ffe8](https://github.com/n8n-io/n8n/commit/e18ffe809c044f2e10564669d96cf79779a8a279))
|
||||
* **editor:** Add settings icons to the node on canvas ([#15467](https://github.com/n8n-io/n8n/issues/15467)) ([a2f21a7](https://github.com/n8n-io/n8n/commit/a2f21a76159e40de97c84c7604d3039d7e9a522e))
|
||||
* **editor:** New users see whatsnew notification only if new ([#17409](https://github.com/n8n-io/n8n/issues/17409)) ([a1d2a55](https://github.com/n8n-io/n8n/commit/a1d2a55f7e6e04389cd8b86aacb9c78f2bffdc41))
|
||||
* **editor:** Release the Focus Panel ([#17734](https://github.com/n8n-io/n8n/issues/17734)) ([a415dbf](https://github.com/n8n-io/n8n/commit/a415dbfd96c429f34e5de0a3572c7338d31321af))
|
||||
* **editor:** Use remote filtering for error workflow search in settings ([#17624](https://github.com/n8n-io/n8n/issues/17624)) ([e1ef35a](https://github.com/n8n-io/n8n/commit/e1ef35a2b4a44c1ff9770b387fec9b5a3a742838))
|
||||
* Proxy all RudderStack frontend telemetry events through the backend ([#17177](https://github.com/n8n-io/n8n/issues/17177)) ([5524b21](https://github.com/n8n-io/n8n/commit/5524b2137a0b54132df7dad1600c2e3054ed78c8))
|
||||
* Respond to chat and wait for response ([#12546](https://github.com/n8n-io/n8n/issues/12546)) ([a98ed2c](https://github.com/n8n-io/n8n/commit/a98ed2ca495d5c86ebb61baad049592ba1bce3a6))
|
||||
* **RSS Read Node:** Add support for custom response fields ([#16875](https://github.com/n8n-io/n8n/issues/16875)) ([d520059](https://github.com/n8n-io/n8n/commit/d520059ec36a9f0a578a60ddd8ea9811e76afd1f))
|
||||
* Track inputs and outputs in Evaluations ([#17404](https://github.com/n8n-io/n8n/issues/17404)) ([c18fabb](https://github.com/n8n-io/n8n/commit/c18fabb419889d35bf70326f83e26300eaba0102))
|
||||
|
||||
|
||||
|
||||
# [1.104.0](https://github.com/n8n-io/n8n/compare/n8n@1.103.0...n8n@1.104.0) (2025-07-21)
|
||||
|
||||
|
||||
|
|
|
|||
270
cypress-playwright-migration.md
Normal file
270
cypress-playwright-migration.md
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
# Cypress to Playwright Migration Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This guide outlines the systematic approach for migrating Cypress tests to Playwright in the n8n codebase, based on successful migrations and lessons learned.
|
||||
|
||||
## 🎯 Migration Principles
|
||||
|
||||
### 1. **Architecture First**
|
||||
- Follow the established 4-layer architecture: Tests → Composables → Page Objects → BasePage
|
||||
- Use existing composables and page objects before creating new ones
|
||||
- Maintain separation of concerns: business logic in composables, UI interactions in page objects
|
||||
|
||||
### 2. **Search Existing Patterns First**
|
||||
- **ALWAYS** search for existing Playwright patterns before implementing new functionality
|
||||
- Look for working examples in existing test files (e.g., `39-projects.spec.ts`)
|
||||
- Check composables and page objects for existing methods
|
||||
- Framework-specific patterns may differ (Cypress display names vs Playwright field names)
|
||||
|
||||
### 3. **Idempotent Test Design**
|
||||
- Design tests to work regardless of initial state
|
||||
- Use fresh project creation for tests that need empty states
|
||||
- Create test prerequisites within the test when needed
|
||||
- Avoid `@db:reset` dependencies in favor of project-based isolation
|
||||
|
||||
## 📋 Pre-Migration Checklist
|
||||
|
||||
### 1. **Environment Setup**
|
||||
```bash
|
||||
# Start isolated test environment
|
||||
cd packages/testing/playwright
|
||||
pnpm start:isolated
|
||||
|
||||
# Run tests with proper environment
|
||||
N8N_BASE_URL=http://localhost:5679 npx playwright test --reporter=list
|
||||
```
|
||||
|
||||
### 2. **Study Existing Patterns**
|
||||
- Review `CONTRIBUTING.md` for architecture guidelines
|
||||
- Examine working test files (e.g., `1-workflows.spec.ts`, `39-projects.spec.ts`)
|
||||
- Check available composables in `composables/` directory
|
||||
- Review page objects in `pages/` directory
|
||||
|
||||
### 3. **Understand Framework Differences**
|
||||
- **Cypress**: Uses display names (`'Internal Integration Secret'`)
|
||||
- **Playwright**: Uses field names (`'apiKey'`)
|
||||
- **Navigation**: Direct page navigation often more reliable than complex UI interactions
|
||||
- **Selectors**: Prefer `data-test-id` over text-based selectors
|
||||
|
||||
## 🔄 Migration Process
|
||||
|
||||
### Step 1: Scaffold the Test File
|
||||
```typescript
|
||||
// 1. Create test file with proper imports
|
||||
import { test, expect } from '../fixtures/base';
|
||||
import {
|
||||
// Import constants from existing patterns
|
||||
NOTION_NODE_NAME,
|
||||
NEW_NOTION_ACCOUNT_NAME,
|
||||
// ... other constants
|
||||
} from '../config/constants';
|
||||
|
||||
// 2. Add beforeEach setup if needed
|
||||
test.describe('Feature Name', () => {
|
||||
test.beforeEach(async ({ api, n8n }) => {
|
||||
await api.enableFeature('sharing');
|
||||
await api.enableFeature('folders');
|
||||
// ... other feature flags
|
||||
await n8n.goHome();
|
||||
});
|
||||
|
||||
// 3. Scaffold all tests from Cypress file
|
||||
test('should do something', async ({ n8n }) => {
|
||||
// TODO: Implement based on Cypress version
|
||||
console.log('Test scaffolded - ready for implementation');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Step 2: Research Existing Patterns
|
||||
```bash
|
||||
# Search for existing implementations
|
||||
grep -r "addCredentialToProject" packages/testing/playwright/
|
||||
grep -r "createProject" packages/testing/playwright/
|
||||
grep -r "workflowComposer" packages/testing/playwright/
|
||||
```
|
||||
|
||||
### Step 3: Implement Working Tests First
|
||||
- Start with tests that have clear existing patterns
|
||||
- Use composables for high-level operations (project creation, navigation)
|
||||
- Use direct DOM interactions for form filling when composables don't match
|
||||
- Implement one test at a time and verify it works
|
||||
|
||||
### Step 4: Handle Complex UI Interactions
|
||||
- **Node Creation Issues**: Close NDV after adding first node to prevent overlay blocking
|
||||
- **Universal Add Button**: Use direct navigation when button interactions fail
|
||||
- **Modal Overlays**: Use route interception for error testing
|
||||
- **Multiple Elements**: Use specific selectors to avoid strict mode violations
|
||||
|
||||
## 🛠️ Common Patterns
|
||||
|
||||
### Project-Based Testing
|
||||
```typescript
|
||||
// ✅ Good: Use existing composable
|
||||
const { projectName } = await n8n.projectComposer.createProject();
|
||||
await n8n.projectComposer.addCredentialToProject(
|
||||
projectName,
|
||||
'Notion API',
|
||||
'apiKey', // Use field name, not display name
|
||||
'test_value'
|
||||
);
|
||||
```
|
||||
|
||||
### Direct Navigation
|
||||
```typescript
|
||||
// ✅ Good: Direct navigation when UI interactions fail
|
||||
await n8n.page.goto('/home/credentials/create');
|
||||
await n8n.page.goto('/workflow/new');
|
||||
```
|
||||
|
||||
### Error Testing with Route Interception
|
||||
```typescript
|
||||
// ✅ Good: Force errors for notification testing
|
||||
await n8n.page.route('**/rest/credentials', route => {
|
||||
route.abort();
|
||||
});
|
||||
```
|
||||
|
||||
### Node Creation with NDV Handling
|
||||
```typescript
|
||||
// ✅ Good: Handle NDV auto-opening
|
||||
await n8n.canvas.addNode(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
await n8n.ndv.close(); // Close NDV that opens automatically
|
||||
await n8n.canvas.addNode(NOTION_NODE_NAME);
|
||||
```
|
||||
|
||||
## 🚨 Common Pitfalls
|
||||
|
||||
### 1. **Not Checking Existing Patterns**
|
||||
```typescript
|
||||
// ❌ Bad: Implementing without checking existing patterns
|
||||
await n8n.page.getByText('Internal Integration Secret').fill('value');
|
||||
|
||||
// ✅ Good: Use existing composable with correct field name
|
||||
await n8n.projectComposer.addCredentialToProject(
|
||||
projectName, 'Notion API', 'apiKey', 'value'
|
||||
);
|
||||
```
|
||||
|
||||
### 2. **Ignoring Framework Differences**
|
||||
```typescript
|
||||
// ❌ Bad: Assuming Cypress patterns work in Playwright
|
||||
await n8n.credentialsModal.connectionParameter('Internal Integration Secret').fill('value');
|
||||
|
||||
// ✅ Good: Use Playwright field names
|
||||
await n8n.page.getByTestId('parameter-input-field').fill('value');
|
||||
```
|
||||
|
||||
### 3. **Complex UI Interactions When Simple Navigation Works**
|
||||
```typescript
|
||||
// ❌ Bad: Complex button clicking when direct navigation works
|
||||
await n8n.workflows.clickAddWorkflowButton();
|
||||
await n8n.page.waitForLoadState();
|
||||
|
||||
// ✅ Good: Direct navigation
|
||||
await n8n.page.goto('/workflow/new');
|
||||
await n8n.page.waitForLoadState('networkidle');
|
||||
```
|
||||
|
||||
### 4. **Not Handling UI Blocking**
|
||||
```typescript
|
||||
// ❌ Bad: Not handling NDV auto-opening
|
||||
await n8n.canvas.addNode(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
await n8n.canvas.addNode(NOTION_NODE_NAME); // This will fail
|
||||
|
||||
// ✅ Good: Close NDV after first node
|
||||
await n8n.canvas.addNode(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
await n8n.ndv.close();
|
||||
await n8n.canvas.addNode(NOTION_NODE_NAME);
|
||||
```
|
||||
|
||||
## 📝 Testing Strategy
|
||||
|
||||
### 1. **Start Simple**
|
||||
- Begin with basic navigation and page verification tests
|
||||
- Use existing composables for common operations
|
||||
- Verify each test works before moving to complex scenarios
|
||||
|
||||
### 2. **Incremental Implementation**
|
||||
- Scaffold all tests first with placeholders
|
||||
- Implement one test at a time
|
||||
- Use `console.log` for placeholder tests to maintain passing test suite
|
||||
|
||||
### 3. **Debugging Approach**
|
||||
```typescript
|
||||
// Add pauses for debugging
|
||||
await n8n.page.pause();
|
||||
|
||||
// Use headed mode for visual debugging
|
||||
SHOW_BROWSER=true npx playwright test
|
||||
|
||||
// Use specific test selection
|
||||
npx playwright test -g "test name" --reporter=list
|
||||
```
|
||||
|
||||
### 4. **Verification Strategy**
|
||||
- Run individual tests during development
|
||||
- Run full test suite after each major change
|
||||
- Use `--reporter=list` for clear output during development
|
||||
|
||||
## 🔧 Environment Configuration
|
||||
|
||||
### VS Code Settings
|
||||
```json
|
||||
{
|
||||
"playwright.env": {
|
||||
"N8N_BASE_URL": "http://localhost:5679",
|
||||
"SHOW_BROWSER": "true",
|
||||
"RESET_E2E_DB": "true"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Package.json Scripts
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"start:isolated": "cd ..; N8N_PORT=5679 N8N_USER_FOLDER=/tmp/n8n-test-$(date +%s) E2E_TESTS=true pnpm start",
|
||||
"test:local": "RESET_E2E_DB=true N8N_BASE_URL=http://localhost:5679 start-server-and-test 'pnpm start:isolated' http://localhost:5679/favicon.ico 'sleep 1 && pnpm test:standard --workers 4 --repeat-each 5'"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 Success Metrics
|
||||
|
||||
### Migration Complete When:
|
||||
- [ ] All tests from Cypress file are scaffolded
|
||||
- [ ] All tests pass consistently
|
||||
- [ ] Tests use existing composables where appropriate
|
||||
- [ ] Tests follow established patterns
|
||||
- [ ] No `@db:reset` dependencies (unless absolutely necessary)
|
||||
- [ ] Tests are idempotent and can run in any order
|
||||
- [ ] Complex UI interactions are handled properly
|
||||
|
||||
### Quality Checklist:
|
||||
- [ ] Tests use proper error handling
|
||||
- [ ] Tests include appropriate assertions
|
||||
- [ ] Tests follow naming conventions
|
||||
- [ ] Tests include proper comments
|
||||
- [ ] Tests use constants for repeated values
|
||||
- [ ] Tests handle dynamic data properly
|
||||
|
||||
## 🎯 Best Practices Summary
|
||||
|
||||
1. **Search First**: Always look for existing patterns before implementing
|
||||
2. **Use Composables**: Leverage existing business logic composables
|
||||
3. **Direct Navigation**: Prefer direct page navigation over complex UI interactions
|
||||
4. **Handle UI Blocking**: Close modals/NDV when adding multiple nodes
|
||||
5. **Framework Awareness**: Understand differences between Cypress and Playwright
|
||||
6. **Incremental Approach**: Implement one test at a time
|
||||
7. **Idempotent Design**: Make tests work regardless of initial state
|
||||
8. **Proper Debugging**: Use pauses and headed mode for troubleshooting
|
||||
|
||||
## 📚 Resources
|
||||
|
||||
- [Playwright Test Documentation](https://playwright.dev/docs/intro)
|
||||
- [n8n Playwright Contributing Guide](./packages/testing/playwright/CONTRIBUTING.md)
|
||||
- [Existing Test Examples](./packages/testing/playwright/tests/)
|
||||
- [Composables Reference](./packages/testing/playwright/composables/)
|
||||
- [Page Objects Reference](./packages/testing/playwright/pages/)
|
||||
|
|
@ -10,7 +10,9 @@ const workflowSharingModal = new WorkflowSharingModal();
|
|||
|
||||
const multipleWorkflowsCount = 5;
|
||||
|
||||
describe('Workflows', () => {
|
||||
// Migrated to Playwright
|
||||
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
|
||||
describe.skip('Workflows', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit(WorkflowsPage.url);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { WorkflowPage, NDV } from '../pages';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
import {
|
||||
MANUAL_TRIGGER_NODE_NAME,
|
||||
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
|
||||
SCHEDULE_TRIGGER_NODE_NAME,
|
||||
} from './../constants';
|
||||
import { WorkflowPage, NDV } from '../pages';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
const ndv = new NDV();
|
||||
|
|
|
|||
|
|
@ -41,33 +41,33 @@ describe('Execution', () => {
|
|||
// Check canvas nodes after 1st step (workflow passed the manual trigger node
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Manual')
|
||||
.within(() => cy.get('svg[data-icon=check]'))
|
||||
.within(() => cy.get('svg[data-icon=node-success]'))
|
||||
.should('exist');
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Wait')
|
||||
.within(() => cy.get('svg[data-icon=check]').should('not.exist'));
|
||||
.within(() => cy.get('svg[data-icon=node-success]').should('not.exist'));
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Wait')
|
||||
.within(() => cy.get('svg[data-icon=refresh-cw]'))
|
||||
.should('exist');
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Set')
|
||||
.within(() => cy.get('svg[data-icon=check]').should('not.exist'));
|
||||
.within(() => cy.get('svg[data-icon=node-success]').should('not.exist'));
|
||||
|
||||
cy.wait(2000);
|
||||
|
||||
// Check canvas nodes after 2nd step (waiting node finished its execution and the http request node is about to start)
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Manual')
|
||||
.within(() => cy.get('svg[data-icon=check]'))
|
||||
.within(() => cy.get('svg[data-icon=node-success]'))
|
||||
.should('exist');
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Wait')
|
||||
.within(() => cy.get('svg[data-icon=check]'))
|
||||
.within(() => cy.get('svg[data-icon=node-success]'))
|
||||
.should('exist');
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Set')
|
||||
.within(() => cy.get('svg[data-icon=check]'))
|
||||
.within(() => cy.get('svg[data-icon=node-success]'))
|
||||
.should('exist');
|
||||
|
||||
successToast().should('be.visible');
|
||||
|
|
@ -101,18 +101,18 @@ describe('Execution', () => {
|
|||
// Check canvas nodes after 1st step (workflow passed the manual trigger node
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Manual')
|
||||
.within(() => cy.get('svg[data-icon=check]'))
|
||||
.within(() => cy.get('svg[data-icon=node-success]'))
|
||||
.should('exist');
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Wait')
|
||||
.within(() => cy.get('svg[data-icon=check]').should('not.exist'));
|
||||
.within(() => cy.get('svg[data-icon=node-success]').should('not.exist'));
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Wait')
|
||||
.within(() => cy.get('svg[data-icon=refresh-cw]'))
|
||||
.should('exist');
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Set')
|
||||
.within(() => cy.get('svg[data-icon=check]').should('not.exist'));
|
||||
.within(() => cy.get('svg[data-icon=node-success]').should('not.exist'));
|
||||
|
||||
successToast().should('be.visible');
|
||||
clearNotifications();
|
||||
|
|
@ -123,7 +123,7 @@ describe('Execution', () => {
|
|||
// Check canvas nodes after workflow stopped
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Manual')
|
||||
.within(() => cy.get('svg[data-icon=check]'))
|
||||
.within(() => cy.get('svg[data-icon=node-success]'))
|
||||
.should('exist');
|
||||
|
||||
workflowPage.getters
|
||||
|
|
@ -132,7 +132,7 @@ describe('Execution', () => {
|
|||
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Set')
|
||||
.within(() => cy.get('svg[data-icon=check]').should('not.exist'));
|
||||
.within(() => cy.get('svg[data-icon=node-success]').should('not.exist'));
|
||||
|
||||
successToast().should('be.visible');
|
||||
|
||||
|
|
@ -181,29 +181,29 @@ describe('Execution', () => {
|
|||
// Check canvas nodes after 1st step (workflow passed the manual trigger node
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Webhook')
|
||||
.within(() => cy.get('svg[data-icon=check]'))
|
||||
.within(() => cy.get('svg[data-icon=node-success]'))
|
||||
.should('exist');
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Wait')
|
||||
.within(() => cy.get('svg[data-icon=check]').should('not.exist'));
|
||||
.within(() => cy.get('svg[data-icon=node-success]').should('not.exist'));
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Wait')
|
||||
.within(() => cy.get('svg[data-icon=refresh-cw]'))
|
||||
.should('exist');
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Set')
|
||||
.within(() => cy.get('svg[data-icon=check]').should('not.exist'));
|
||||
.within(() => cy.get('svg[data-icon=node-success]').should('not.exist'));
|
||||
|
||||
cy.wait(2000);
|
||||
|
||||
// Check canvas nodes after 2nd step (waiting node finished its execution and the http request node is about to start)
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Webhook')
|
||||
.within(() => cy.get('svg[data-icon=check]'))
|
||||
.within(() => cy.get('svg[data-icon=node-success]'))
|
||||
.should('exist');
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Set')
|
||||
.within(() => cy.get('svg[data-icon=check]'))
|
||||
.within(() => cy.get('svg[data-icon=node-success]'))
|
||||
.should('exist');
|
||||
|
||||
successToast().should('be.visible');
|
||||
|
|
@ -578,11 +578,11 @@ describe('Execution', () => {
|
|||
// Check that the previous nodes executed successfully
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('DebugHelper')
|
||||
.within(() => cy.get('svg[data-icon=check]'))
|
||||
.within(() => cy.get('svg[data-icon=node-success]'))
|
||||
.should('exist');
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Filter')
|
||||
.within(() => cy.get('svg[data-icon=check]'))
|
||||
.within(() => cy.get('svg[data-icon=node-success]'))
|
||||
.should('exist');
|
||||
|
||||
errorToast().should('contain', 'Problem in node ‘Telegram‘');
|
||||
|
|
@ -596,7 +596,7 @@ describe('Execution', () => {
|
|||
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Edit Fields')
|
||||
.within(() => cy.get('svg[data-icon=check]'))
|
||||
.within(() => cy.get('svg[data-icon=node-success]'))
|
||||
.should('exist');
|
||||
|
||||
workflowPage.getters.canvasNodeByName('Edit Fields').dblclick();
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import generateOTPToken from 'cypress-otp';
|
||||
|
||||
import { MainSidebar } from './../pages/sidebar/main-sidebar';
|
||||
import { INSTANCE_OWNER, INSTANCE_ADMIN, BACKEND_BASE_URL } from '../constants';
|
||||
import { SigninPage } from '../pages';
|
||||
import { MfaLoginPage } from '../pages/mfa-login';
|
||||
import { successToast } from '../pages/notifications';
|
||||
import { PersonalSettingsPage } from '../pages/settings-personal';
|
||||
import { MainSidebar } from './../pages/sidebar/main-sidebar';
|
||||
|
||||
const MFA_SECRET = 'KVKFKRCPNZQUYMLXOVYDSQKJKZDTSRLD';
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ 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) => {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@ import { clearNotifications } from '../pages/notifications';
|
|||
import {
|
||||
getNpsSurvey,
|
||||
getNpsSurveyClose,
|
||||
getNpsSurveyEmail,
|
||||
getNpsSurveyFeedback,
|
||||
getNpsSurveyRatings,
|
||||
getNpsSurveySubmit,
|
||||
} from '../pages/npsSurvey';
|
||||
import { WorkflowPage } from '../pages/workflow';
|
||||
|
||||
|
|
@ -22,7 +23,7 @@ describe('NpsSurvey', () => {
|
|||
cy.signin(INSTANCE_ADMIN);
|
||||
});
|
||||
|
||||
it('shows nps survey to recently activated user and can submit email ', () => {
|
||||
it('shows nps survey to recently activated user and can submit feedback ', () => {
|
||||
cy.intercept('/rest/settings', { middleware: true }, (req) => {
|
||||
req.on('response', (res) => {
|
||||
if (res.body.data) {
|
||||
|
|
@ -31,6 +32,8 @@ describe('NpsSurvey', () => {
|
|||
config: {
|
||||
key: 'test',
|
||||
url: 'https://telemetry-test.n8n.io',
|
||||
proxy: 'http://localhost:5678/rest/telemetry/proxy',
|
||||
sourceConfig: 'http://localhost:5678/rest/telemetry/rudderstack',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -54,8 +57,8 @@ describe('NpsSurvey', () => {
|
|||
getNpsSurveyRatings().find('button').should('have.length', 11);
|
||||
getNpsSurveyRatings().find('button').first().click();
|
||||
|
||||
getNpsSurveyEmail().find('input').type('test@n8n.io');
|
||||
getNpsSurveyEmail().find('button').click();
|
||||
getNpsSurveyFeedback().find('textarea').type('n8n is the best');
|
||||
getNpsSurveySubmit().find('button').click();
|
||||
|
||||
// test that modal does not show up again until 6 months later
|
||||
workflowPage.actions.visit(true, NOW + ONE_DAY);
|
||||
|
|
@ -77,6 +80,8 @@ describe('NpsSurvey', () => {
|
|||
config: {
|
||||
key: 'test',
|
||||
url: 'https://telemetry-test.n8n.io',
|
||||
proxy: 'http://localhost:5678/rest/telemetry/proxy',
|
||||
sourceConfig: 'http://localhost:5678/rest/telemetry/rudderstack',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -443,7 +443,7 @@ describe('AI Assistant Credential Help', () => {
|
|||
aiAssistant.getters.credentialEditAssistantButton().should('not.exist');
|
||||
|
||||
credentialsModal.getters.credentialAuthTypeRadioButtons().eq(1).click();
|
||||
credentialsModal.getters.credentialInputs().should('have.length', 1);
|
||||
credentialsModal.getters.credentialInputs().should('have.length', 3);
|
||||
aiAssistant.getters.credentialEditAssistantButton().should('exist');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ $input.item()
|
|||
return []
|
||||
`);
|
||||
|
||||
getParameter().get('.cm-lintRange-error').should('have.length', 5);
|
||||
getParameter().get('.cm-lintRange-error').should('have.length.gte', 5);
|
||||
getParameter().contains('all').realHover();
|
||||
cy.get('.cm-tooltip-lint').should(
|
||||
'have.text',
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
"format": "biome format --write .",
|
||||
"format:check": "biome ci .",
|
||||
"lint": "eslint . --quiet",
|
||||
"lintfix": "eslint . --fix",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"develop": "cd ..; pnpm dev:e2e:server",
|
||||
"start": "cd ..; pnpm start"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -305,7 +305,7 @@ export class NDV extends BasePage {
|
|||
this.actions.typeIntoParameterInput(fieldName, invalidExpression ?? "{{ $('unknown')", {
|
||||
parseSpecialCharSequences: false,
|
||||
});
|
||||
this.actions.validateExpressionPreview(fieldName, "node doesn't exist");
|
||||
this.actions.validateExpressionPreview(fieldName, 'No path back to node');
|
||||
},
|
||||
openSettings: () => {
|
||||
this.getters.nodeSettingsTab().click();
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ export const getNpsSurvey = () => cy.getByTestId('nps-survey-modal');
|
|||
|
||||
export const getNpsSurveyRatings = () => cy.getByTestId('nps-survey-ratings');
|
||||
|
||||
export const getNpsSurveyEmail = () => cy.getByTestId('nps-survey-email');
|
||||
export const getNpsSurveyFeedback = () => cy.getByTestId('nps-survey-feedback');
|
||||
export const getNpsSurveySubmit = () => cy.getByTestId('nps-survey-feedback-button');
|
||||
|
||||
export const getNpsSurveyClose = () =>
|
||||
cy.getByTestId('nps-survey-modal').find('button.el-drawer__close-btn');
|
||||
|
|
|
|||
|
|
@ -14,9 +14,13 @@ export const installFirstCommunityNode = (nodeName: string) => {
|
|||
};
|
||||
|
||||
export const confirmCommunityNodeUpdate = () => {
|
||||
cy.getByTestId('communityPackageManageConfirm-modal').find('button').eq(1).click();
|
||||
cy.getByTestId('communityPackageManageConfirm-modal')
|
||||
.contains('button', 'Confirm update')
|
||||
.click();
|
||||
};
|
||||
|
||||
export const confirmCommunityNodeUninstall = () => {
|
||||
cy.getByTestId('communityPackageManageConfirm-modal').find('button').eq(1).click();
|
||||
cy.getByTestId('communityPackageManageConfirm-modal')
|
||||
.contains('button', 'Confirm uninstall')
|
||||
.click();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ Cypress.Commands.add('grantBrowserPermissions', (...permissions: string[]) => {
|
|||
});
|
||||
|
||||
Cypress.Commands.add('readClipboard', () =>
|
||||
cy.window().then((win) => win.navigator.clipboard.readText()),
|
||||
cy.window().then(async (win) => await win.navigator.clipboard.readText()),
|
||||
);
|
||||
|
||||
Cypress.Commands.add('paste', { prevSubject: true }, (selector, pastePayload) => {
|
||||
|
|
|
|||
31
package.json
31
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-monorepo",
|
||||
"version": "1.104.0",
|
||||
"version": "1.105.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=22.16",
|
||||
|
|
@ -11,9 +11,6 @@
|
|||
"prepare": "node scripts/prepare.mjs",
|
||||
"preinstall": "node scripts/block-npm-install.js",
|
||||
"build": "turbo run build",
|
||||
"build:backend": "turbo run build:backend",
|
||||
"build:frontend": "turbo run build:frontend",
|
||||
"build:nodes": "turbo run build:nodes",
|
||||
"build:n8n": "node scripts/build-n8n.mjs",
|
||||
"build:deploy": "node scripts/build-n8n.mjs",
|
||||
"build:docker": "node scripts/build-n8n.mjs && node scripts/dockerize-n8n.mjs",
|
||||
|
|
@ -28,16 +25,13 @@
|
|||
"dev:e2e": "cd cypress && pnpm run test:e2e:dev",
|
||||
"debug:flaky:e2e": "cd cypress && pnpm run test:flaky",
|
||||
"dev:e2e:server": "run-p start dev:fe:editor",
|
||||
"clean": "turbo run clean --parallel",
|
||||
"clean": "turbo run clean",
|
||||
"reset": "node scripts/ensure-zx.mjs && zx scripts/reset.mjs",
|
||||
"format": "turbo run format && node scripts/format.mjs",
|
||||
"format:check": "turbo run format:check",
|
||||
"lint": "turbo run lint",
|
||||
"lintfix": "turbo run lintfix",
|
||||
"lint:backend": "turbo run lint:backend",
|
||||
"lint:nodes": "turbo run lint:nodes",
|
||||
"lint:frontend": "turbo run lint:frontend",
|
||||
"lint:testing": "turbo run lint:testing",
|
||||
"lint:affected": "turbo run lint --affected",
|
||||
"lint:fix": "turbo run lint:fix",
|
||||
"optimize-svg": "find ./packages -name '*.svg' ! -name 'pipedrive.svg' -print0 | xargs -0 -P16 -L20 npx svgo",
|
||||
"setup-backend-module": "node scripts/ensure-zx.mjs && zx scripts/backend-module/setup.mjs",
|
||||
"start": "run-script-os",
|
||||
|
|
@ -45,12 +39,11 @@
|
|||
"start:tunnel": "./packages/cli/bin/n8n start --tunnel",
|
||||
"start:windows": "cd packages/cli/bin && n8n",
|
||||
"test": "JEST_JUNIT_CLASSNAME={filepath} turbo run test",
|
||||
"test:backend": "turbo run test:backend --concurrency=1",
|
||||
"test:frontend": "turbo run test:frontend --concurrency=1",
|
||||
"test:nodes": "turbo run test:nodes --concurrency=1",
|
||||
"test:ci": "turbo run test --continue --concurrency=1",
|
||||
"test:affected": "turbo run test --affected --concurrency=1",
|
||||
"test:with:docker": "pnpm --filter=n8n-playwright run test:standard",
|
||||
"test:show:report": "pnpm --filter=n8n-playwright exec playwright show-report",
|
||||
"watch": "turbo run watch --parallel",
|
||||
"watch": "turbo run watch",
|
||||
"webhook": "./packages/cli/bin/n8n webhook",
|
||||
"worker": "./packages/cli/bin/n8n worker"
|
||||
},
|
||||
|
|
@ -91,10 +84,11 @@
|
|||
],
|
||||
"overrides": {
|
||||
"@azure/identity": "^4.3.0",
|
||||
"@n8n/typeorm>@sentry/node": "catalog:",
|
||||
"@types/node": "^20.17.50",
|
||||
"chokidar": "^4.0.1",
|
||||
"esbuild": "^0.24.0",
|
||||
"multer": "^2.0.1",
|
||||
"multer": "^2.0.2",
|
||||
"prebuild-install": "7.1.3",
|
||||
"pug": "^3.0.3",
|
||||
"semver": "^7.5.4",
|
||||
|
|
@ -108,7 +102,8 @@
|
|||
"brace-expansion@1": "1.1.12",
|
||||
"brace-expansion@2": "2.0.2",
|
||||
"date-fns": "2.30.0",
|
||||
"date-fns-tz": "2.0.0"
|
||||
"date-fns-tz": "2.0.0",
|
||||
"form-data": "4.0.4"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"bull@4.16.4": "patches/bull@4.16.4.patch",
|
||||
|
|
@ -122,7 +117,9 @@
|
|||
"js-base64": "patches/js-base64.patch",
|
||||
"ics": "patches/ics.patch",
|
||||
"minifaker": "patches/minifaker.patch",
|
||||
"z-vue-scan": "patches/z-vue-scan.patch"
|
||||
"z-vue-scan": "patches/z-vue-scan.patch",
|
||||
"@lezer/highlight": "patches/@lezer__highlight.patch",
|
||||
"v-code-diff": "patches/v-code-diff.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
packages/@n8n/ai-workflow-builder.ee/jest.config.js
Normal file
2
packages/@n8n/ai-workflow-builder.ee/jest.config.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/** @type {import('jest').Config} */
|
||||
module.exports = require('../../../jest.config');
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/ai-workflow-builder",
|
||||
"version": "0.14.0",
|
||||
"version": "0.15.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"typecheck": "tsc --noEmit",
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
"test:coverage": "jest --coverage",
|
||||
"test:watch": "jest --watch",
|
||||
"lint": "eslint . --quiet",
|
||||
"lintfix": "eslint . --fix",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"watch": "tsc-watch -p tsconfig.build.json --onCompilationComplete \"tsc-alias -p tsconfig.build.json\"",
|
||||
"deps:graph": "madge src/index.ts --image deps-graph.svg",
|
||||
"deps:graph:service": "madge src/ai-workflow-builder-agent.service.ts --image deps-service.svg",
|
||||
|
|
|
|||
|
|
@ -167,10 +167,10 @@ export class AiWorkflowBuilderService {
|
|||
return this.agent;
|
||||
}
|
||||
|
||||
async *chat(payload: ChatPayload, user?: IUser) {
|
||||
async *chat(payload: ChatPayload, user?: IUser, abortSignal?: AbortSignal) {
|
||||
const agent = await this.getAgent(user);
|
||||
|
||||
for await (const output of agent.chat(payload, user?.id?.toString())) {
|
||||
for await (const output of agent.chat(payload, user?.id?.toString(), abortSignal)) {
|
||||
yield output;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||
import type { AIMessage, ToolMessage } from '@langchain/core/messages';
|
||||
import { HumanMessage, RemoveMessage } from '@langchain/core/messages';
|
||||
import type { ToolMessage } from '@langchain/core/messages';
|
||||
import { AIMessage, HumanMessage, RemoveMessage } from '@langchain/core/messages';
|
||||
import type { RunnableConfig } from '@langchain/core/runnables';
|
||||
import type { LangChainTracer } from '@langchain/core/tracers/tracer_langchain';
|
||||
import { StateGraph, MemorySaver, END } from '@langchain/langgraph';
|
||||
import type { Logger } from '@n8n/backend-common';
|
||||
|
|
@ -180,71 +181,73 @@ export class WorkflowBuilderAgent {
|
|||
: crypto.randomUUID();
|
||||
}
|
||||
|
||||
async *chat(payload: ChatPayload, userId?: string) {
|
||||
private getDefaultWorkflowJSON(payload: ChatPayload): SimpleWorkflow {
|
||||
return (
|
||||
(payload.workflowContext?.currentWorkflow as SimpleWorkflow) ?? {
|
||||
nodes: [],
|
||||
connections: {},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async *chat(payload: ChatPayload, userId?: string, abortSignal?: AbortSignal) {
|
||||
const agent = this.createWorkflow().compile({ checkpointer: this.checkpointer });
|
||||
const workflowId = payload.workflowContext?.currentWorkflow?.id;
|
||||
// Generate thread ID from workflowId and userId
|
||||
// This ensures one session per workflow per user
|
||||
const threadId = WorkflowBuilderAgent.generateThreadId(workflowId, userId);
|
||||
|
||||
// Configure thread for checkpointing
|
||||
const threadConfig = {
|
||||
const threadConfig: RunnableConfig = {
|
||||
configurable: {
|
||||
thread_id: threadId,
|
||||
},
|
||||
};
|
||||
const streamConfig = {
|
||||
...threadConfig,
|
||||
streamMode: ['updates', 'custom'],
|
||||
recursionLimit: 30,
|
||||
signal: abortSignal,
|
||||
callbacks: this.tracer ? [this.tracer] : undefined,
|
||||
} as RunnableConfig;
|
||||
|
||||
// Check if this is a subsequent message
|
||||
// If so, update the workflowJSON with the current editor state
|
||||
const existingCheckpoint = await this.checkpointer.getTuple(threadConfig);
|
||||
|
||||
let stream;
|
||||
|
||||
if (!existingCheckpoint?.checkpoint) {
|
||||
// First message - use initial state
|
||||
const initialState: typeof WorkflowState.State = {
|
||||
const stream = await agent.stream(
|
||||
{
|
||||
messages: [new HumanMessage({ content: payload.message })],
|
||||
workflowJSON: (payload.workflowContext?.currentWorkflow as SimpleWorkflow) ?? {
|
||||
nodes: [],
|
||||
connections: {},
|
||||
},
|
||||
workflowJSON: this.getDefaultWorkflowJSON(payload),
|
||||
workflowOperations: [],
|
||||
workflowContext: payload.workflowContext,
|
||||
};
|
||||
},
|
||||
streamConfig,
|
||||
);
|
||||
|
||||
stream = await agent.stream(initialState, {
|
||||
...threadConfig,
|
||||
streamMode: ['updates', 'custom'],
|
||||
recursionLimit: 30,
|
||||
callbacks: this.tracer ? [this.tracer] : undefined,
|
||||
});
|
||||
} else {
|
||||
// Subsequent message - update the state with current workflow
|
||||
const stateUpdate: Partial<typeof WorkflowState.State> = {
|
||||
messages: [new HumanMessage({ content: payload.message })],
|
||||
workflowOperations: [], // Clear any pending operations from previous message
|
||||
workflowContext: payload.workflowContext,
|
||||
workflowJSON: { nodes: [], connections: {} }, // Default to empty workflow
|
||||
};
|
||||
|
||||
if (payload.workflowContext?.currentWorkflow) {
|
||||
stateUpdate.workflowJSON = payload.workflowContext?.currentWorkflow as SimpleWorkflow;
|
||||
try {
|
||||
const streamProcessor = createStreamProcessor(stream);
|
||||
for await (const output of streamProcessor) {
|
||||
yield output;
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
error &&
|
||||
typeof error === 'object' &&
|
||||
'message' in error &&
|
||||
typeof error.message === 'string' &&
|
||||
// This is naive, but it's all we get from LangGraph AbortError
|
||||
['Abort', 'Aborted'].includes(error.message)
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const messages = (await agent.getState(threadConfig)).values.messages as Array<
|
||||
AIMessage | HumanMessage | ToolMessage
|
||||
>;
|
||||
|
||||
// Stream with just the new message
|
||||
stream = await agent.stream(stateUpdate, {
|
||||
...threadConfig,
|
||||
streamMode: ['updates', 'custom'],
|
||||
recursionLimit: 80,
|
||||
callbacks: this.tracer ? [this.tracer] : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
// Use the stream processor utility to handle chunk processing
|
||||
const streamProcessor = createStreamProcessor(stream);
|
||||
|
||||
for await (const output of streamProcessor) {
|
||||
yield output;
|
||||
// Handle abort errors gracefully
|
||||
const abortedAiMessage = new AIMessage({
|
||||
content: '[Task aborted]',
|
||||
id: crypto.randomUUID(),
|
||||
});
|
||||
// TODO: Should we clear tool calls that are in progress?
|
||||
await agent.updateState(threadConfig, { messages: [...messages, abortedAiMessage] });
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -256,7 +259,7 @@ export class WorkflowBuilderAgent {
|
|||
|
||||
if (workflowId) {
|
||||
const threadId = WorkflowBuilderAgent.generateThreadId(workflowId, userId);
|
||||
const threadConfig = {
|
||||
const threadConfig: RunnableConfig = {
|
||||
configurable: {
|
||||
thread_id: threadId,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@
|
|||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
},
|
||||
"tsBuildInfoFile": "dist/typecheck.tsbuildinfo"
|
||||
"tsBuildInfoFile": "dist/typecheck.tsbuildinfo",
|
||||
"types": ["node", "jest"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "test/**/*.ts", "evaluations/**/*.ts"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/api-types",
|
||||
"version": "0.38.0",
|
||||
"version": "0.39.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
"format": "biome format --write .",
|
||||
"format:check": "biome ci .",
|
||||
"lint": "eslint . --quiet",
|
||||
"lintfix": "eslint . --fix",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"watch": "tsc -p tsconfig.build.json --watch",
|
||||
"test": "jest",
|
||||
"test:dev": "jest --watch"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ export interface IVersionNotificationSettings {
|
|||
export interface ITelemetryClientConfig {
|
||||
url: string;
|
||||
key: string;
|
||||
proxy: string;
|
||||
sourceConfig: string;
|
||||
}
|
||||
|
||||
export interface ITelemetrySettings {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/backend-common",
|
||||
"version": "0.14.0",
|
||||
"version": "0.15.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
"format": "biome format --write .",
|
||||
"format:check": "biome ci .",
|
||||
"lint": "eslint . --quiet",
|
||||
"lintfix": "eslint . --fix",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"watch": "tsc -p tsconfig.build.json --watch",
|
||||
"test": "jest",
|
||||
"test:dev": "jest --watch"
|
||||
|
|
@ -29,9 +29,12 @@
|
|||
"n8n-workflow": "workspace:^",
|
||||
"picocolors": "catalog:",
|
||||
"reflect-metadata": "catalog:",
|
||||
"winston": "3.14.2"
|
||||
"winston": "3.14.2",
|
||||
"yargs-parser": "21.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@n8n/typescript-config": "workspace:*"
|
||||
"@n8n/typescript-config": "workspace:*",
|
||||
"@types/yargs-parser": "21.0.0",
|
||||
"zod": "catalog:"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
117
packages/@n8n/backend-common/src/__tests__/cli-parser.test.ts
Normal file
117
packages/@n8n/backend-common/src/__tests__/cli-parser.test.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import { mock } from 'jest-mock-extended';
|
||||
import z from 'zod';
|
||||
|
||||
import { CliParser } from '../cli-parser';
|
||||
|
||||
describe('parse', () => {
|
||||
it('should parse `argv` without flags schema', () => {
|
||||
const cliParser = new CliParser(mock());
|
||||
const result = cliParser.parse({ argv: ['node', 'script.js', 'arg1', 'arg2'] });
|
||||
expect(result).toEqual({ flags: {}, args: ['arg1', 'arg2'] });
|
||||
});
|
||||
|
||||
it('should parse `argv` with flags schema', () => {
|
||||
const cliParser = new CliParser(mock());
|
||||
const flagsSchema = z.object({
|
||||
verbose: z.boolean().optional(),
|
||||
name: z.string().optional(),
|
||||
});
|
||||
|
||||
const result = cliParser.parse({
|
||||
argv: ['node', 'script.js', '--verbose', '--name', 'test', 'arg1'],
|
||||
flagsSchema,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
flags: { verbose: true, name: 'test' },
|
||||
args: ['arg1'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should ignore flags not defined in schema', () => {
|
||||
const cliParser = new CliParser(mock());
|
||||
const flagsSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
// ignored is absent
|
||||
});
|
||||
|
||||
const result = cliParser.parse({
|
||||
argv: ['node', 'script.js', '--name', 'test', '--ignored', 'value', 'arg1'],
|
||||
flagsSchema,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
flags: {
|
||||
name: 'test',
|
||||
// ignored is absent
|
||||
},
|
||||
args: ['arg1'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle a numeric value for `--id` flag', () => {
|
||||
const cliParser = new CliParser(mock());
|
||||
const result = cliParser.parse({
|
||||
argv: ['node', 'script.js', '--id', '123', 'arg1'],
|
||||
flagsSchema: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
flags: { id: '123' },
|
||||
args: ['arg1'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle positional arguments', () => {
|
||||
const cliParser = new CliParser(mock());
|
||||
const result = cliParser.parse({
|
||||
argv: ['node', 'script.js', '123', 'true'],
|
||||
});
|
||||
|
||||
expect(result.args).toEqual(['123', 'true']);
|
||||
expect(typeof result.args[0]).toBe('string');
|
||||
expect(typeof result.args[1]).toBe('string');
|
||||
});
|
||||
|
||||
it('should handle required flags with aliases', () => {
|
||||
const cliParser = new CliParser(mock());
|
||||
const flagsSchema = z.object({
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
// @ts-expect-error zod was monkey-patched to support aliases
|
||||
flagsSchema.shape.name._def._alias = 'n';
|
||||
|
||||
const result = cliParser.parse({
|
||||
argv: ['node', 'script.js', '-n', 'test', 'arg1'],
|
||||
flagsSchema,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
flags: { name: 'test' },
|
||||
args: ['arg1'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle optional flags with aliases', () => {
|
||||
const cliParser = new CliParser(mock());
|
||||
const flagsSchema = z.object({
|
||||
name: z.optional(z.string()),
|
||||
});
|
||||
|
||||
// @ts-expect-error zod was monkey-patched to support aliases
|
||||
flagsSchema.shape.name._def.innerType._def._alias = 'n';
|
||||
|
||||
const result = cliParser.parse({
|
||||
argv: ['node', 'script.js', '-n', 'test', 'arg1'],
|
||||
flagsSchema,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
flags: { name: 'test' },
|
||||
args: ['arg1'],
|
||||
});
|
||||
});
|
||||
});
|
||||
63
packages/@n8n/backend-common/src/cli-parser.ts
Normal file
63
packages/@n8n/backend-common/src/cli-parser.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { Service } from '@n8n/di';
|
||||
import argvParser from 'yargs-parser';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { Logger } from './logging';
|
||||
|
||||
type CliInput<Flags extends z.ZodRawShape> = {
|
||||
argv: string[];
|
||||
flagsSchema?: z.ZodObject<Flags>;
|
||||
description?: string;
|
||||
examples?: string[];
|
||||
};
|
||||
|
||||
type ParsedArgs<Flags = Record<string, unknown>> = {
|
||||
flags: Flags;
|
||||
args: string[];
|
||||
};
|
||||
|
||||
@Service()
|
||||
export class CliParser {
|
||||
constructor(private readonly logger: Logger) {}
|
||||
|
||||
parse<Flags extends z.ZodRawShape>(
|
||||
input: CliInput<Flags>,
|
||||
): ParsedArgs<z.infer<z.ZodObject<Flags>>> {
|
||||
// eslint-disable-next-line id-denylist
|
||||
const { _: rest, ...rawFlags } = argvParser(input.argv, { string: ['id'] });
|
||||
|
||||
let flags = {} as z.infer<z.ZodObject<Flags>>;
|
||||
if (input.flagsSchema) {
|
||||
for (const key in input.flagsSchema.shape) {
|
||||
const flagSchema = input.flagsSchema.shape[key];
|
||||
let schemaDef = flagSchema._def as z.ZodTypeDef & {
|
||||
typeName: string;
|
||||
innerType?: z.ZodType;
|
||||
_alias?: string;
|
||||
};
|
||||
|
||||
if (schemaDef.typeName === 'ZodOptional' && schemaDef.innerType) {
|
||||
schemaDef = schemaDef.innerType._def as typeof schemaDef;
|
||||
}
|
||||
|
||||
const alias = schemaDef._alias;
|
||||
if (alias?.length && !(key in rawFlags) && rawFlags[alias]) {
|
||||
rawFlags[key] = rawFlags[alias] as unknown;
|
||||
}
|
||||
}
|
||||
|
||||
flags = input.flagsSchema.parse(rawFlags);
|
||||
}
|
||||
|
||||
const args = rest.map(String).slice(2);
|
||||
|
||||
this.logger.debug('Received CLI command', {
|
||||
execPath: rest[0],
|
||||
scriptPath: rest[1],
|
||||
args,
|
||||
flags,
|
||||
});
|
||||
|
||||
return { flags, args };
|
||||
}
|
||||
}
|
||||
|
|
@ -7,3 +7,4 @@ export { Logger } from './logging/logger';
|
|||
export { ModuleRegistry } from './modules/module-registry';
|
||||
export { ModulesConfig, ModuleName } from './modules/modules.config';
|
||||
export { isContainedWithin, safeJoinPath } from './utils/path-util';
|
||||
export { CliParser } from './cli-parser';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { ModuleMetadata } from '@n8n/decorators';
|
||||
import type { EntityClass, ModuleSettings } from '@n8n/decorators';
|
||||
import { Container, Service } from '@n8n/di';
|
||||
import { existsSync } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { MissingModuleError } from './errors/missing-module.error';
|
||||
|
|
@ -54,11 +55,13 @@ export class ModuleRegistry {
|
|||
// docker + tests
|
||||
const n8nPackagePath = require.resolve('n8n/package.json');
|
||||
const n8nRoot = path.dirname(n8nPackagePath);
|
||||
const dir = process.env.NODE_ENV === 'test' ? 'src' : 'dist';
|
||||
const srcDirExists = existsSync(path.join(n8nRoot, 'src'));
|
||||
const dir = process.env.NODE_ENV === 'test' && srcDirExists ? 'src' : 'dist';
|
||||
modulesDir = path.join(n8nRoot, dir, 'modules');
|
||||
} catch {
|
||||
// local dev
|
||||
modulesDir = path.resolve(__dirname, '../../../../cli/dist/modules');
|
||||
// n8n binary is inside the bin folder, so we need to go up two levels
|
||||
modulesDir = path.resolve(process.argv[1], '../../dist/modules');
|
||||
}
|
||||
|
||||
for (const moduleName of modules ?? this.eligibleModules) {
|
||||
|
|
|
|||
|
|
@ -8,5 +8,12 @@
|
|||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
"include": ["src/**/*.ts"],
|
||||
"references": [
|
||||
{ "path": "../../workflow/tsconfig.build.cjs.json" },
|
||||
{ "path": "../config/tsconfig.build.json" },
|
||||
{ "path": "../constants/tsconfig.build.json" },
|
||||
{ "path": "../decorators/tsconfig.build.json" },
|
||||
{ "path": "../di/tsconfig.build.json" }
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/backend-test-utils",
|
||||
"version": "0.7.0",
|
||||
"version": "0.8.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
|
@ -9,10 +9,10 @@
|
|||
"format": "biome format --write .",
|
||||
"format:check": "biome ci .",
|
||||
"lint": "eslint . --quiet",
|
||||
"lintfix": "eslint . --fix",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"watch": "tsc -p tsconfig.build.json --watch",
|
||||
"test": "jest",
|
||||
"test:dev": "jest --watch"
|
||||
"test": "echo \"WARNING: no test specified\" && exit 0",
|
||||
"test:dev": "echo \"WARNING: no test specified\" && exit 0"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"module": "src/index.ts",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/n8n-benchmark",
|
||||
"version": "1.19.0",
|
||||
"version": "1.20.0",
|
||||
"description": "Cli for running benchmark tests for n8n",
|
||||
"main": "dist/index",
|
||||
"scripts": {
|
||||
|
|
@ -8,9 +8,9 @@
|
|||
"format": "biome format --write .",
|
||||
"format:check": "biome ci .",
|
||||
"lint": "eslint . --quiet",
|
||||
"lintfix": "eslint . --fix",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"start": "./bin/n8n-benchmark",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"test": "echo \"WARNING: no test specified\" && exit 0",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"benchmark": "zx scripts/run.mjs",
|
||||
"benchmark-in-cloud": "pnpm benchmark --env cloud",
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
services:
|
||||
mockapi:
|
||||
image: wiremock/wiremock:3.9.1
|
||||
ports:
|
||||
- '8088:8080'
|
||||
volumes:
|
||||
- ${MOCK_API_DATA_PATH}/mappings:/home/wiremock/mappings
|
||||
|
||||
postgres:
|
||||
image: postgres:16.4
|
||||
restart: always
|
||||
user: root:root
|
||||
environment:
|
||||
- POSTGRES_DB=n8n
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=password
|
||||
- PGDATA=/var/lib/postgresql/data/pgdata
|
||||
volumes:
|
||||
- ${RUN_DIR}/postgres:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U postgres']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
n8n:
|
||||
image: ghcr.io/n8n-io/n8n:${N8N_VERSION:-latest}
|
||||
user: root:root
|
||||
environment:
|
||||
- N8N_DIAGNOSTICS_ENABLED=false
|
||||
- N8N_USER_FOLDER=/n8n
|
||||
- DB_TYPE=postgresdb
|
||||
- DB_POSTGRESDB_HOST=postgres
|
||||
- DB_POSTGRESDB_PASSWORD=password
|
||||
# Task Runner config
|
||||
- N8N_RUNNERS_ENABLED=true
|
||||
- N8N_RUNNERS_MODE=internal
|
||||
# Enable Insights
|
||||
- N8N_ENABLED_MODULES=insights
|
||||
ports:
|
||||
- 5678:5678
|
||||
volumes:
|
||||
- ${RUN_DIR}/n8n:/n8n
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
mockapi:
|
||||
condition: service_started
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'wget --spider -q http://n8n:5678/healthz || exit 1']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
benchmark:
|
||||
image: ghcr.io/n8n-io/n8n-benchmark:${N8N_BENCHMARK_VERSION:-latest}
|
||||
depends_on:
|
||||
n8n:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- N8N_BASE_URL=http://n8n:5678
|
||||
- K6_API_TOKEN=${K6_API_TOKEN}
|
||||
- BENCHMARK_RESULT_WEBHOOK_URL=${BENCHMARK_RESULT_WEBHOOK_URL}
|
||||
- BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER=${BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
#!/usr/bin/env zx
|
||||
|
||||
import path from 'path';
|
||||
import { fs } from 'zx';
|
||||
|
||||
/**
|
||||
* Creates the needed directories so the permissions get set correctly.
|
||||
*/
|
||||
export function setup({ runDir }) {
|
||||
const neededDirs = ['n8n', 'postgres'];
|
||||
|
||||
for (const dir of neededDirs) {
|
||||
fs.ensureDirSync(path.join(runDir, dir));
|
||||
}
|
||||
}
|
||||
|
|
@ -35,8 +35,6 @@ services:
|
|||
# Task Runner config
|
||||
- N8N_RUNNERS_ENABLED=true
|
||||
- N8N_RUNNERS_MODE=internal
|
||||
# Disable Insights
|
||||
- N8N_DISABLED_MODULES=insights
|
||||
ports:
|
||||
- 5678:5678
|
||||
volumes:
|
||||
|
|
|
|||
|
|
@ -1,217 +0,0 @@
|
|||
services:
|
||||
mockapi:
|
||||
image: wiremock/wiremock:3.9.1
|
||||
ports:
|
||||
- '8088:8080'
|
||||
volumes:
|
||||
- ${MOCK_API_DATA_PATH}/mappings:/home/wiremock/mappings
|
||||
|
||||
redis:
|
||||
image: redis:6.2.14-alpine
|
||||
restart: always
|
||||
ports:
|
||||
- 6379:6379
|
||||
healthcheck:
|
||||
test: ['CMD', 'redis-cli', 'ping']
|
||||
interval: 1s
|
||||
timeout: 3s
|
||||
|
||||
postgres:
|
||||
image: postgres:16.4
|
||||
restart: always
|
||||
environment:
|
||||
- POSTGRES_DB=n8n
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=password
|
||||
- PGDATA=/var/lib/postgresql/data/pgdata
|
||||
volumes:
|
||||
- ${RUN_DIR}/postgres:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U postgres']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
n8n_worker1:
|
||||
image: ghcr.io/n8n-io/n8n:${N8N_VERSION:-latest}
|
||||
environment:
|
||||
- N8N_DIAGNOSTICS_ENABLED=false
|
||||
- N8N_USER_FOLDER=/n8n/worker1
|
||||
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
|
||||
- N8N_LICENSE_CERT=${N8N_LICENSE_CERT}
|
||||
- N8N_LICENSE_ACTIVATION_KEY=${N8N_LICENSE_ACTIVATION_KEY}
|
||||
- N8N_LICENSE_TENANT_ID=${N8N_LICENSE_TENANT_ID}
|
||||
# Scaling mode config
|
||||
- EXECUTIONS_MODE=queue
|
||||
- QUEUE_BULL_REDIS_HOST=redis
|
||||
- QUEUE_HEALTH_CHECK_ACTIVE=true
|
||||
- N8N_CONCURRENCY_PRODUCTION_LIMIT=10
|
||||
# DB config
|
||||
- DB_TYPE=postgresdb
|
||||
- DB_POSTGRESDB_HOST=postgres
|
||||
- DB_POSTGRESDB_PASSWORD=password
|
||||
# Task Runner config
|
||||
- N8N_RUNNERS_ENABLED=true
|
||||
- N8N_RUNNERS_MODE=internal
|
||||
# Enable Insights
|
||||
- N8N_ENABLED_MODULES=insights
|
||||
command: worker
|
||||
volumes:
|
||||
- ${RUN_DIR}/n8n-worker1:/n8n
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'wget --spider -q http://localhost:5678/healthz || exit 1']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
n8n_worker2:
|
||||
image: ghcr.io/n8n-io/n8n:${N8N_VERSION:-latest}
|
||||
environment:
|
||||
- N8N_DIAGNOSTICS_ENABLED=false
|
||||
- N8N_USER_FOLDER=/n8n/worker2
|
||||
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
|
||||
- N8N_LICENSE_CERT=${N8N_LICENSE_CERT}
|
||||
- N8N_LICENSE_ACTIVATION_KEY=${N8N_LICENSE_ACTIVATION_KEY}
|
||||
- N8N_LICENSE_TENANT_ID=${N8N_LICENSE_TENANT_ID}
|
||||
# Scaling mode config
|
||||
- EXECUTIONS_MODE=queue
|
||||
- QUEUE_BULL_REDIS_HOST=redis
|
||||
- QUEUE_HEALTH_CHECK_ACTIVE=true
|
||||
- N8N_CONCURRENCY_PRODUCTION_LIMIT=10
|
||||
# DB config
|
||||
- DB_TYPE=postgresdb
|
||||
- DB_POSTGRESDB_HOST=postgres
|
||||
- DB_POSTGRESDB_PASSWORD=password
|
||||
# Task Runner config
|
||||
- N8N_RUNNERS_ENABLED=true
|
||||
- N8N_RUNNERS_MODE=internal
|
||||
# Enable Insights
|
||||
- N8N_ENABLED_MODULES=insights
|
||||
command: worker
|
||||
volumes:
|
||||
- ${RUN_DIR}/n8n-worker2:/n8n
|
||||
depends_on:
|
||||
# We let the worker 1 start first so it can run the DB migrations
|
||||
n8n_worker1:
|
||||
condition: service_healthy
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'wget --spider -q http://localhost:5678/healthz || exit 1']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
n8n_main2:
|
||||
image: ghcr.io/n8n-io/n8n:${N8N_VERSION:-latest}
|
||||
environment:
|
||||
- N8N_DIAGNOSTICS_ENABLED=false
|
||||
- N8N_USER_FOLDER=/n8n
|
||||
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
|
||||
- N8N_LICENSE_CERT=${N8N_LICENSE_CERT}
|
||||
- N8N_LICENSE_ACTIVATION_KEY=${N8N_LICENSE_ACTIVATION_KEY}
|
||||
- N8N_LICENSE_TENANT_ID=${N8N_LICENSE_TENANT_ID}
|
||||
# Scaling mode config
|
||||
- N8N_PROXY_HOPS=1
|
||||
- EXECUTIONS_MODE=queue
|
||||
- QUEUE_BULL_REDIS_HOST=redis
|
||||
- N8N_MULTI_MAIN_SETUP_ENABLED=true
|
||||
# DB config
|
||||
- DB_TYPE=postgresdb
|
||||
- DB_POSTGRESDB_HOST=postgres
|
||||
- DB_POSTGRESDB_PASSWORD=password
|
||||
# Task Runner config
|
||||
- N8N_RUNNERS_ENABLED=true
|
||||
- N8N_RUNNERS_MODE=internal
|
||||
# Enable Insights
|
||||
- N8N_ENABLED_MODULES=insights
|
||||
volumes:
|
||||
- ${RUN_DIR}/n8n-main2:/n8n
|
||||
depends_on:
|
||||
n8n_worker1:
|
||||
condition: service_healthy
|
||||
n8n_worker2:
|
||||
condition: service_healthy
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
mockapi:
|
||||
condition: service_started
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'wget --spider -q http://n8n_main2:5678/healthz || exit 1']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
n8n_main1:
|
||||
image: ghcr.io/n8n-io/n8n:${N8N_VERSION:-latest}
|
||||
environment:
|
||||
- N8N_DIAGNOSTICS_ENABLED=false
|
||||
- N8N_USER_FOLDER=/n8n
|
||||
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
|
||||
- N8N_LICENSE_CERT=${N8N_LICENSE_CERT}
|
||||
- N8N_LICENSE_ACTIVATION_KEY=${N8N_LICENSE_ACTIVATION_KEY}
|
||||
- N8N_LICENSE_TENANT_ID=${N8N_LICENSE_TENANT_ID}
|
||||
# Scaling mode config
|
||||
- N8N_PROXY_HOPS=1
|
||||
- EXECUTIONS_MODE=queue
|
||||
- QUEUE_BULL_REDIS_HOST=redis
|
||||
- N8N_MULTI_MAIN_SETUP_ENABLED=true
|
||||
# DB config
|
||||
- DB_TYPE=postgresdb
|
||||
- DB_POSTGRESDB_HOST=postgres
|
||||
- DB_POSTGRESDB_PASSWORD=password
|
||||
# Task Runner config
|
||||
- N8N_RUNNERS_ENABLED=true
|
||||
- N8N_RUNNERS_MODE=internal
|
||||
# Enable Insights
|
||||
- N8N_ENABLED_MODULES=insights
|
||||
volumes:
|
||||
- ${RUN_DIR}/n8n-main1:/n8n
|
||||
depends_on:
|
||||
n8n_worker1:
|
||||
condition: service_healthy
|
||||
n8n_worker2:
|
||||
condition: service_healthy
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
mockapi:
|
||||
condition: service_started
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'wget --spider -q http://n8n_main1:5678/healthz || exit 1']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
# Load balancer that acts as an entry point for n8n
|
||||
n8n:
|
||||
image: nginx:1.27.2
|
||||
ports:
|
||||
- '5678:80'
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||
depends_on:
|
||||
n8n_main1:
|
||||
condition: service_healthy
|
||||
n8n_main2:
|
||||
condition: service_healthy
|
||||
|
||||
benchmark:
|
||||
image: ghcr.io/n8n-io/n8n-benchmark:${N8N_BENCHMARK_VERSION:-latest}
|
||||
depends_on:
|
||||
- n8n
|
||||
environment:
|
||||
- N8N_BASE_URL=http://n8n:80
|
||||
- K6_API_TOKEN=${K6_API_TOKEN}
|
||||
- BENCHMARK_RESULT_WEBHOOK_URL=${BENCHMARK_RESULT_WEBHOOK_URL}
|
||||
- BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER=${BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
events {}
|
||||
|
||||
http {
|
||||
client_max_body_size 50M;
|
||||
access_log off;
|
||||
error_log /dev/stderr warn;
|
||||
|
||||
upstream backend {
|
||||
server n8n_main1:5678;
|
||||
server n8n_main2:5678;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
location / {
|
||||
proxy_pass http://backend;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
#!/usr/bin/env zx
|
||||
|
||||
import path from 'path';
|
||||
import { fs } from 'zx';
|
||||
|
||||
/**
|
||||
* Creates the needed directories so the permissions get set correctly.
|
||||
*/
|
||||
export function setup({ runDir }) {
|
||||
const neededDirs = ['n8n-worker1', 'n8n-worker2', 'n8n-main1', 'n8n-main2', 'postgres'];
|
||||
|
||||
for (const dir of neededDirs) {
|
||||
fs.ensureDirSync(path.join(runDir, dir));
|
||||
}
|
||||
}
|
||||
|
|
@ -53,8 +53,6 @@ services:
|
|||
# Task Runner config
|
||||
- N8N_RUNNERS_ENABLED=true
|
||||
- N8N_RUNNERS_MODE=internal
|
||||
# Disable Insights
|
||||
- N8N_DISABLED_MODULES=insights
|
||||
command: worker
|
||||
volumes:
|
||||
- ${RUN_DIR}/n8n-worker1:/n8n
|
||||
|
|
@ -90,8 +88,6 @@ services:
|
|||
# Task Runner config
|
||||
- N8N_RUNNERS_ENABLED=true
|
||||
- N8N_RUNNERS_MODE=internal
|
||||
# Disable Insights
|
||||
- N8N_DISABLED_MODULES=insights
|
||||
command: worker
|
||||
volumes:
|
||||
- ${RUN_DIR}/n8n-worker2:/n8n
|
||||
|
|
@ -130,8 +126,6 @@ services:
|
|||
# Task Runner config
|
||||
- N8N_RUNNERS_ENABLED=true
|
||||
- N8N_RUNNERS_MODE=internal
|
||||
# Disable Insights
|
||||
- N8N_DISABLED_MODULES=insights
|
||||
volumes:
|
||||
- ${RUN_DIR}/n8n-main2:/n8n
|
||||
depends_on:
|
||||
|
|
@ -172,8 +166,6 @@ services:
|
|||
# Task Runner config
|
||||
- N8N_RUNNERS_ENABLED=true
|
||||
- N8N_RUNNERS_MODE=internal
|
||||
# Disable Insights
|
||||
- N8N_DISABLED_MODULES=insights
|
||||
volumes:
|
||||
- ${RUN_DIR}/n8n-main1:/n8n
|
||||
depends_on:
|
||||
|
|
|
|||
|
|
@ -51,8 +51,8 @@ services:
|
|||
# Task Runner config
|
||||
- N8N_RUNNERS_ENABLED=true
|
||||
- N8N_RUNNERS_MODE=internal
|
||||
# Enable Insights
|
||||
- N8N_ENABLED_MODULES=insights
|
||||
# Disable Insights
|
||||
- N8N_DISABLED_MODULES=insights
|
||||
command: worker
|
||||
volumes:
|
||||
- ${RUN_DIR}/n8n-worker1:/n8n
|
||||
|
|
@ -86,8 +86,8 @@ services:
|
|||
# Task Runner config
|
||||
- N8N_RUNNERS_ENABLED=true
|
||||
- N8N_RUNNERS_MODE=internal
|
||||
# Enable Insights
|
||||
- N8N_ENABLED_MODULES=insights
|
||||
# Disable Insights
|
||||
- N8N_DISABLED_MODULES=insights
|
||||
command: worker
|
||||
volumes:
|
||||
- ${RUN_DIR}/n8n-worker2:/n8n
|
||||
|
|
@ -122,8 +122,8 @@ services:
|
|||
# Task Runner config
|
||||
- N8N_RUNNERS_ENABLED=true
|
||||
- N8N_RUNNERS_MODE=internal
|
||||
# Enable Insights
|
||||
- N8N_ENABLED_MODULES=insights
|
||||
# Disable Insights
|
||||
- N8N_DISABLED_MODULES=insights
|
||||
ports:
|
||||
- 5678:5678
|
||||
volumes:
|
||||
|
|
@ -51,8 +51,6 @@ services:
|
|||
# Task Runner config
|
||||
- N8N_RUNNERS_ENABLED=true
|
||||
- N8N_RUNNERS_MODE=internal
|
||||
# Disable Insights
|
||||
- N8N_DISABLED_MODULES=insights
|
||||
command: worker
|
||||
volumes:
|
||||
- ${RUN_DIR}/n8n-worker1:/n8n
|
||||
|
|
@ -86,8 +84,6 @@ services:
|
|||
# Task Runner config
|
||||
- N8N_RUNNERS_ENABLED=true
|
||||
- N8N_RUNNERS_MODE=internal
|
||||
# Disable Insights
|
||||
- N8N_DISABLED_MODULES=insights
|
||||
command: worker
|
||||
volumes:
|
||||
- ${RUN_DIR}/n8n-worker2:/n8n
|
||||
|
|
@ -122,8 +118,6 @@ services:
|
|||
# Task Runner config
|
||||
- N8N_RUNNERS_ENABLED=true
|
||||
- N8N_RUNNERS_MODE=internal
|
||||
# Disable Insights
|
||||
- N8N_DISABLED_MODULES=insights
|
||||
ports:
|
||||
- 5678:5678
|
||||
volumes:
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
services:
|
||||
mockapi:
|
||||
image: wiremock/wiremock:3.9.1
|
||||
ports:
|
||||
- '8088:8080'
|
||||
volumes:
|
||||
- ${MOCK_API_DATA_PATH}/mappings:/home/wiremock/mappings
|
||||
|
||||
n8n:
|
||||
image: ghcr.io/n8n-io/n8n:${N8N_VERSION:-latest}
|
||||
user: root:root
|
||||
environment:
|
||||
- N8N_DIAGNOSTICS_ENABLED=false
|
||||
- N8N_USER_FOLDER=/n8n
|
||||
# Enable Insights
|
||||
- N8N_ENABLED_MODULES=insights
|
||||
ports:
|
||||
- 5678:5678
|
||||
volumes:
|
||||
- ${RUN_DIR}:/n8n
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'wget --spider -q http://n8n:5678/healthz || exit 1']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
depends_on:
|
||||
mockapi:
|
||||
condition: service_started
|
||||
|
||||
benchmark:
|
||||
image: ghcr.io/n8n-io/n8n-benchmark:${N8N_BENCHMARK_VERSION:-latest}
|
||||
depends_on:
|
||||
n8n:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- N8N_BASE_URL=http://n8n:5678
|
||||
- K6_API_TOKEN=${K6_API_TOKEN}
|
||||
- BENCHMARK_RESULT_WEBHOOK_URL=${BENCHMARK_RESULT_WEBHOOK_URL}
|
||||
- BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER=${BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
#!/usr/bin/env zx
|
||||
|
||||
import path from 'path';
|
||||
import { fs } from 'zx';
|
||||
|
||||
/**
|
||||
* Creates the needed directories so the permissions get set correctly.
|
||||
*/
|
||||
export function setup({ runDir }) {
|
||||
const neededDirs = ['n8n'];
|
||||
|
||||
for (const dir of neededDirs) {
|
||||
fs.ensureDirSync(path.join(runDir, dir));
|
||||
}
|
||||
}
|
||||
|
|
@ -12,8 +12,6 @@ services:
|
|||
environment:
|
||||
- N8N_DIAGNOSTICS_ENABLED=false
|
||||
- N8N_USER_FOLDER=/n8n
|
||||
# Disable Insights
|
||||
- N8N_DISABLED_MODULES=insights
|
||||
ports:
|
||||
- 5678:5678
|
||||
volumes:
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
services:
|
||||
mockapi:
|
||||
image: wiremock/wiremock:3.9.1
|
||||
ports:
|
||||
- '8088:8080'
|
||||
volumes:
|
||||
- ${MOCK_API_DATA_PATH}/mappings:/home/wiremock/mappings
|
||||
|
||||
n8n:
|
||||
image: ghcr.io/n8n-io/n8n:${N8N_VERSION:-latest}
|
||||
user: root:root
|
||||
environment:
|
||||
- N8N_DIAGNOSTICS_ENABLED=false
|
||||
- N8N_USER_FOLDER=/n8n
|
||||
- DB_SQLITE_POOL_SIZE=3
|
||||
- DB_SQLITE_ENABLE_WAL=true
|
||||
# Task Runner config
|
||||
- N8N_RUNNERS_ENABLED=true
|
||||
- N8N_RUNNERS_MODE=internal
|
||||
# Enable Insights
|
||||
- N8N_ENABLED_MODULES=insights
|
||||
ports:
|
||||
- 5678:5678
|
||||
volumes:
|
||||
- ${RUN_DIR}:/n8n
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'wget --spider -q http://n8n:5678/healthz || exit 1']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
depends_on:
|
||||
mockapi:
|
||||
condition: service_started
|
||||
|
||||
benchmark:
|
||||
image: ghcr.io/n8n-io/n8n-benchmark:${N8N_BENCHMARK_VERSION:-latest}
|
||||
depends_on:
|
||||
n8n:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- N8N_BASE_URL=http://n8n:5678
|
||||
- K6_API_TOKEN=${K6_API_TOKEN}
|
||||
- BENCHMARK_RESULT_WEBHOOK_URL=${BENCHMARK_RESULT_WEBHOOK_URL}
|
||||
- BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER=${BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
#!/usr/bin/env zx
|
||||
|
||||
import path from 'path';
|
||||
import { fs } from 'zx';
|
||||
|
||||
/**
|
||||
* Creates the needed directories so the permissions get set correctly.
|
||||
*/
|
||||
export function setup({ runDir }) {
|
||||
const neededDirs = ['n8n'];
|
||||
|
||||
for (const dir of neededDirs) {
|
||||
fs.ensureDirSync(path.join(runDir, dir));
|
||||
}
|
||||
}
|
||||
|
|
@ -17,8 +17,6 @@ services:
|
|||
# Task Runner config
|
||||
- N8N_RUNNERS_ENABLED=true
|
||||
- N8N_RUNNERS_MODE=internal
|
||||
# Disable Insights
|
||||
- N8N_DISABLED_MODULES=insights
|
||||
ports:
|
||||
- 5678:5678
|
||||
volumes:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/client-oauth2",
|
||||
"version": "0.27.0",
|
||||
"version": "0.28.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
"format": "biome format --write src test",
|
||||
"format:check": "biome ci src test",
|
||||
"lint": "eslint . --quiet",
|
||||
"lintfix": "eslint . --fix",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"watch": "tsc -p tsconfig.build.json --watch",
|
||||
"test": "jest",
|
||||
"test:dev": "jest --watch"
|
||||
|
|
|
|||
|
|
@ -42,8 +42,9 @@ export class ResponseError extends Error {
|
|||
readonly status: number,
|
||||
readonly body: unknown,
|
||||
readonly code = 'ESTATUS',
|
||||
readonly message = `HTTP status ${status}`,
|
||||
) {
|
||||
super(`HTTP status ${status}`);
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -133,6 +134,11 @@ export class ClientOAuth2 {
|
|||
return qs.parse(body) as T;
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported content type: ${contentType}`);
|
||||
throw new ResponseError(
|
||||
response.status,
|
||||
body,
|
||||
undefined,
|
||||
`Unsupported content type: ${contentType}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
"build": "tsc -p tsconfig.build.json",
|
||||
"test": "jest",
|
||||
"lint": "eslint . --quiet",
|
||||
"lintfix": "eslint . --fix",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"format": "biome format --write src test",
|
||||
"format:check": "biome ci src test"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/config",
|
||||
"version": "1.47.0",
|
||||
"version": "1.48.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
"format": "biome format --write src test",
|
||||
"format:check": "biome ci src test",
|
||||
"lint": "eslint . --quiet",
|
||||
"lintfix": "eslint . --fix",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"watch": "tsc -p tsconfig.build.json --watch",
|
||||
"test": "jest",
|
||||
"test:dev": "jest --watch"
|
||||
|
|
|
|||
8
packages/@n8n/config/src/configs/ai.config.ts
Normal file
8
packages/@n8n/config/src/configs/ai.config.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { Config, Env } from '../decorators';
|
||||
|
||||
@Config
|
||||
export class AiConfig {
|
||||
/** Whether AI features are enabled. */
|
||||
@Env('N8N_AI_ENABLED')
|
||||
enabled: boolean = false;
|
||||
}
|
||||
|
|
@ -20,10 +20,23 @@ export const LOG_SCOPES = [
|
|||
'workflow-activation',
|
||||
'ssh-client',
|
||||
'data-store',
|
||||
'cron',
|
||||
'community-nodes',
|
||||
] as const;
|
||||
|
||||
export type LogScope = (typeof LOG_SCOPES)[number];
|
||||
|
||||
@Config
|
||||
export class CronLoggingConfig {
|
||||
/**
|
||||
* Interval in minutes to log currently active cron jobs. Set to `0` to disable.
|
||||
*
|
||||
* @example `N8N_LOG_CRON_ACTIVE_INTERVAL=30` will log active crons every 30 minutes.
|
||||
*/
|
||||
@Env('N8N_LOG_CRON_ACTIVE_INTERVAL')
|
||||
activeInterval: number = 0;
|
||||
}
|
||||
|
||||
@Config
|
||||
class FileLoggingConfig {
|
||||
/**
|
||||
|
|
@ -80,6 +93,9 @@ export class LoggingConfig {
|
|||
@Nested
|
||||
file: FileLoggingConfig;
|
||||
|
||||
@Nested
|
||||
cron: CronLoggingConfig;
|
||||
|
||||
/**
|
||||
* Scopes to filter logs by. Nothing is filtered by default.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Config, Env, Nested } from '../decorators';
|
||||
import { Config, Env } from '../decorators';
|
||||
|
||||
function isStringArray(input: unknown): input is string[] {
|
||||
return Array.isArray(input) && input.every((item) => typeof item === 'string');
|
||||
|
|
@ -20,33 +20,6 @@ class JsonStringArray extends Array<string> {
|
|||
}
|
||||
}
|
||||
|
||||
@Config
|
||||
class CommunityPackagesConfig {
|
||||
/** Whether to enable community packages */
|
||||
@Env('N8N_COMMUNITY_PACKAGES_ENABLED')
|
||||
enabled: boolean = true;
|
||||
|
||||
/** NPM registry URL to pull community packages from */
|
||||
@Env('N8N_COMMUNITY_PACKAGES_REGISTRY')
|
||||
registry: string = 'https://registry.npmjs.org';
|
||||
|
||||
/** Whether to reinstall any missing community packages */
|
||||
@Env('N8N_REINSTALL_MISSING_PACKAGES')
|
||||
reinstallMissing: boolean = false;
|
||||
|
||||
/** Whether to block installation of not verified packages */
|
||||
@Env('N8N_UNVERIFIED_PACKAGES_ENABLED')
|
||||
unverifiedEnabled: boolean = true;
|
||||
|
||||
/** Whether to enable and show search suggestion of packages verified by n8n */
|
||||
@Env('N8N_VERIFIED_PACKAGES_ENABLED')
|
||||
verifiedEnabled: boolean = true;
|
||||
|
||||
/** Whether to load community packages */
|
||||
@Env('N8N_COMMUNITY_PACKAGES_PREVENT_LOADING')
|
||||
preventLoading: boolean = false;
|
||||
}
|
||||
|
||||
@Config
|
||||
export class NodesConfig {
|
||||
/** Node types to load. Includes all if unspecified. @example '["n8n-nodes-base.hackerNews"]' */
|
||||
|
|
@ -64,7 +37,4 @@ export class NodesConfig {
|
|||
/** Whether to enable Python execution on the Code node. */
|
||||
@Env('N8N_PYTHON_ENABLED')
|
||||
pythonEnabled: boolean = true;
|
||||
|
||||
@Nested
|
||||
communityPackages: CommunityPackagesConfig;
|
||||
}
|
||||
|
|
|
|||
8
packages/@n8n/config/src/configs/redis.config.ts
Normal file
8
packages/@n8n/config/src/configs/redis.config.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { Config, Env } from '../decorators';
|
||||
|
||||
@Config
|
||||
export class RedisConfig {
|
||||
/** Prefix for all Redis keys managed by n8n. */
|
||||
@Env('N8N_REDIS_KEY_PREFIX')
|
||||
prefix: string = 'n8n';
|
||||
}
|
||||
|
|
@ -38,4 +38,8 @@ export class SecurityConfig {
|
|||
*/
|
||||
@Env('N8N_CONTENT_SECURITY_POLICY_REPORT_ONLY')
|
||||
contentSecurityPolicyReportOnly: boolean = false;
|
||||
|
||||
/** Whether to disable iframe sandboxing for webhooks */
|
||||
@Env('N8N_INSECURE_DISABLE_WEBHOOK_IFRAME_SANDBOX')
|
||||
disableIframeSandboxing: boolean = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { AiAssistantConfig } from './configs/ai-assistant.config';
|
||||
import { AiConfig } from './configs/ai.config';
|
||||
import { AuthConfig } from './configs/auth.config';
|
||||
import { CacheConfig } from './configs/cache.config';
|
||||
import { CredentialsConfig } from './configs/credentials.config';
|
||||
|
|
@ -21,6 +22,7 @@ import { NodesConfig } from './configs/nodes.config';
|
|||
import { PartialExecutionsConfig } from './configs/partial-executions.config';
|
||||
import { PersonalizationConfig } from './configs/personalization.config';
|
||||
import { PublicApiConfig } from './configs/public-api.config';
|
||||
import { RedisConfig } from './configs/redis.config';
|
||||
import { TaskRunnersConfig } from './configs/runners.config';
|
||||
import { ScalingModeConfig } from './configs/scaling-mode.config';
|
||||
import { SecurityConfig } from './configs/security.config';
|
||||
|
|
@ -49,6 +51,7 @@ export { MfaConfig } from './configs/mfa.config';
|
|||
export { HiringBannerConfig } from './configs/hiring-banner.config';
|
||||
export { PersonalizationConfig } from './configs/personalization.config';
|
||||
export { NodesConfig } from './configs/nodes.config';
|
||||
export { CronLoggingConfig } from './configs/logging.config';
|
||||
|
||||
const protocolSchema = z.enum(['http', 'https']);
|
||||
|
||||
|
|
@ -195,4 +198,14 @@ export class GlobalConfig {
|
|||
/** Public URL where the editor is accessible. Also used for emails sent from n8n. */
|
||||
@Env('N8N_EDITOR_BASE_URL')
|
||||
editorBaseUrl: string = '';
|
||||
|
||||
/** URLs to external frontend hooks files, separated by semicolons. */
|
||||
@Env('EXTERNAL_FRONTEND_HOOKS_URLS')
|
||||
externalFrontendHooksUrls: string = '';
|
||||
|
||||
@Nested
|
||||
redis: RedisConfig;
|
||||
|
||||
@Nested
|
||||
ai: AiConfig;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,14 +138,6 @@ describe('GlobalConfig', () => {
|
|||
files: [],
|
||||
},
|
||||
nodes: {
|
||||
communityPackages: {
|
||||
enabled: true,
|
||||
registry: 'https://registry.npmjs.org',
|
||||
reinstallMissing: false,
|
||||
unverifiedEnabled: true,
|
||||
verifiedEnabled: true,
|
||||
preventLoading: false,
|
||||
},
|
||||
errorTriggerType: 'n8n-nodes-base.errorTrigger',
|
||||
include: [],
|
||||
exclude: [],
|
||||
|
|
@ -275,6 +267,9 @@ describe('GlobalConfig', () => {
|
|||
location: 'logs/n8n.log',
|
||||
},
|
||||
scopes: [],
|
||||
cron: {
|
||||
activeInterval: 0,
|
||||
},
|
||||
},
|
||||
multiMainSetup: {
|
||||
enabled: false,
|
||||
|
|
@ -300,6 +295,7 @@ describe('GlobalConfig', () => {
|
|||
daysAbandonedWorkflow: 90,
|
||||
contentSecurityPolicy: '{}',
|
||||
contentSecurityPolicyReportOnly: false,
|
||||
disableIframeSandboxing: false,
|
||||
},
|
||||
executions: {
|
||||
pruneData: true,
|
||||
|
|
@ -348,6 +344,13 @@ describe('GlobalConfig', () => {
|
|||
loginLabel: '',
|
||||
},
|
||||
},
|
||||
redis: {
|
||||
prefix: 'n8n',
|
||||
},
|
||||
externalFrontendHooksUrls: '',
|
||||
ai: {
|
||||
enabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
it('should use all default values when no env variables are defined', () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/constants",
|
||||
"version": "0.9.0",
|
||||
"version": "0.10.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
"format": "biome format --write .",
|
||||
"format:check": "biome ci .",
|
||||
"lint": "eslint . --quiet",
|
||||
"lintfix": "eslint . --fix",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"watch": "tsc -p tsconfig.build.json --watch"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
|
|
|
|||
13
packages/@n8n/create-node/README.md
Normal file
13
packages/@n8n/create-node/README.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# @n8n/create-node
|
||||
|
||||
Scaffold a new community n8n node
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
npm create @n8n/node
|
||||
# or
|
||||
pnpm create @n8n/node
|
||||
# or
|
||||
yarn create @n8n/node
|
||||
```
|
||||
15
packages/@n8n/create-node/bin/create.js
Executable file
15
packages/@n8n/create-node/bin/create.js
Executable file
|
|
@ -0,0 +1,15 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { createRequire } from 'node:module';
|
||||
import path from 'node:path';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const cliBin = require.resolve('@n8n/node-cli/bin/n8n-node.js');
|
||||
|
||||
const result = spawnSync('node', [cliBin, 'create', ...process.argv.slice(2)], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
process.exit(result.status ?? 1);
|
||||
25
packages/@n8n/create-node/package.json
Normal file
25
packages/@n8n/create-node/package.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"name": "@n8n/create-node",
|
||||
"version": "0.1.0",
|
||||
"description": "Official CLI to create new community nodes for n8n",
|
||||
"bin": {
|
||||
"create-n8n-node": "./bin/create.js"
|
||||
},
|
||||
"files": [
|
||||
"bin",
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"publish:dry": "pnpm run build && pnpm pub --dry-run",
|
||||
"start": "./bin/create.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/n8n-io/n8n"
|
||||
},
|
||||
"dependencies": {
|
||||
"@n8n/node-cli": "workspace:*"
|
||||
}
|
||||
}
|
||||
11
packages/@n8n/create-node/tsconfig.json
Normal file
11
packages/@n8n/create-node/tsconfig.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "@n8n/typescript-config/modern/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"types": ["vite/client", "vitest/globals"],
|
||||
"isolatedModules": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/db",
|
||||
"version": "0.15.0",
|
||||
"version": "0.16.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
"format": "biome format --write .",
|
||||
"format:check": "biome ci .",
|
||||
"lint": "eslint . --quiet",
|
||||
"lintfix": "eslint . --fix",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"watch": "tsc -p tsconfig.build.json --watch",
|
||||
"test": "jest",
|
||||
"test:dev": "jest --watch"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Column, Entity, ManyToOne, OneToOne } from '@n8n/typeorm';
|
||||
import type { IDataObject } from 'n8n-workflow';
|
||||
import type { IDataObject, JsonObject } from 'n8n-workflow';
|
||||
|
||||
import { WithStringId, DateTimeColumn, JsonColumn } from './abstract-entity';
|
||||
import type { ExecutionEntity } from './execution-entity';
|
||||
|
|
@ -54,4 +54,10 @@ export class TestCaseExecution extends WithStringId {
|
|||
|
||||
@JsonColumn({ nullable: true })
|
||||
metrics: TestCaseRunMetrics;
|
||||
|
||||
@JsonColumn({ nullable: true })
|
||||
inputs: JsonObject | null;
|
||||
|
||||
@JsonColumn({ nullable: true })
|
||||
outputs: JsonObject | null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
import type { MigrationContext, ReversibleMigration } from '../migration-types';
|
||||
|
||||
export class AddInputsOutputsToTestCaseExecution1752669793000 implements ReversibleMigration {
|
||||
async up({ schemaBuilder: { addColumns, column } }: MigrationContext) {
|
||||
await addColumns('test_case_execution', [column('inputs').json, column('outputs').json]);
|
||||
}
|
||||
|
||||
async down({ schemaBuilder: { dropColumns } }: MigrationContext) {
|
||||
await dropColumns('test_case_execution', ['inputs', 'outputs']);
|
||||
}
|
||||
}
|
||||
|
|
@ -88,6 +88,7 @@ import { AddWorkflowArchivedColumn1745934666076 } from '../common/1745934666076-
|
|||
import { DropRoleTable1745934666077 } from '../common/1745934666077-DropRoleTable';
|
||||
import { AddProjectDescriptionColumn1747824239000 } from '../common/1747824239000-AddProjectDescriptionColumn';
|
||||
import { AddLastActiveAtColumnToUser1750252139166 } from '../common/1750252139166-AddLastActiveAtColumnToUser';
|
||||
import { AddInputsOutputsToTestCaseExecution1752669793000 } from '../common/1752669793000-AddInputsOutputsToTestCaseExecution';
|
||||
import type { Migration } from '../migration-types';
|
||||
import { UpdateParentFolderIdColumn1740445074052 } from '../mysqldb/1740445074052-UpdateParentFolderIdColumn';
|
||||
|
||||
|
|
@ -183,4 +184,5 @@ export const mysqlMigrations: Migration[] = [
|
|||
ClearEvaluation1745322634000,
|
||||
AddProjectDescriptionColumn1747824239000,
|
||||
AddLastActiveAtColumnToUser1750252139166,
|
||||
AddInputsOutputsToTestCaseExecution1752669793000,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { AddMfaColumns1690000000030 } from './../common/1690000000040-AddMfaColumns';
|
||||
import { AddInputsOutputsToTestCaseExecution1752669793000 } from './../common/1752669793000-AddInputsOutputsToTestCaseExecution';
|
||||
import { InitialMigration1587669153312 } from './1587669153312-InitialMigration';
|
||||
import { WebhookModel1589476000887 } from './1589476000887-WebhookModel';
|
||||
import { CreateIndexStoppedAt1594828256133 } from './1594828256133-CreateIndexStoppedAt';
|
||||
|
|
@ -181,4 +182,5 @@ export const postgresMigrations: Migration[] = [
|
|||
ClearEvaluation1745322634000,
|
||||
AddProjectDescriptionColumn1747824239000,
|
||||
AddLastActiveAtColumnToUser1750252139166,
|
||||
AddInputsOutputsToTestCaseExecution1752669793000,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -86,7 +86,9 @@ import { DropRoleTable1745934666077 } from '../common/1745934666077-DropRoleTabl
|
|||
import { CreateDataStoreTables1747814180618 } from '../common/1747814180618-CreateDataStoreTables';
|
||||
import { AddProjectDescriptionColumn1747824239000 } from '../common/1747824239000-AddProjectDescriptionColumn';
|
||||
import { AddLastActiveAtColumnToUser1750252139166 } from '../common/1750252139166-AddLastActiveAtColumnToUser';
|
||||
import { AddInputsOutputsToTestCaseExecution1752669793000 } from '../common/1752669793000-AddInputsOutputsToTestCaseExecution';
|
||||
import type { Migration } from '../migration-types';
|
||||
|
||||
const sqliteMigrations: Migration[] = [
|
||||
InitialMigration1588102412422,
|
||||
WebhookModel1592445003908,
|
||||
|
|
@ -176,6 +178,7 @@ const sqliteMigrations: Migration[] = [
|
|||
CreateDataStoreTables1747814180618,
|
||||
AddProjectDescriptionColumn1747824239000,
|
||||
AddLastActiveAtColumnToUser1750252139166,
|
||||
AddInputsOutputsToTestCaseExecution1752669793000,
|
||||
];
|
||||
|
||||
export { sqliteMigrations };
|
||||
|
|
|
|||
|
|
@ -922,7 +922,7 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
|
|||
async getLiveExecutionRowsOnPostgres() {
|
||||
const tableName = `${this.globalConfig.database.tablePrefix}execution_entity`;
|
||||
|
||||
const pgSql = `SELECT n_live_tup as result FROM pg_stat_all_tables WHERE relname = '${tableName}';`;
|
||||
const pgSql = `SELECT n_live_tup as result FROM pg_stat_all_tables WHERE relname = '${tableName}' and schemaname = '${this.globalConfig.database.postgresdb.schema}';`;
|
||||
|
||||
try {
|
||||
const rows = (await this.query(pgSql)) as Array<{ result: string }>;
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export class TestRunRepository extends Repository<TestRun> {
|
|||
super(TestRun, dataSource.manager);
|
||||
}
|
||||
|
||||
async createTestRun(workflowId: string) {
|
||||
async createTestRun(workflowId: string): Promise<TestRun> {
|
||||
const testRun = this.create({
|
||||
status: 'new',
|
||||
workflow: {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ import type {
|
|||
FolderWithWorkflowAndSubFolderCount,
|
||||
ListQuery,
|
||||
} from '../entities/types-db';
|
||||
import { buildWorkflowsByNodesQuery } from '../utils/build-workflows-by-nodes-query';
|
||||
import { isStringArray } from '../utils/is-string-array';
|
||||
import { TimedQuery } from '../utils/timed-query';
|
||||
|
||||
type ResourceType = 'folder' | 'workflow';
|
||||
|
||||
|
|
@ -371,6 +373,7 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
|
|||
});
|
||||
}
|
||||
|
||||
@TimedQuery()
|
||||
async getMany(workflowIds: string[], options: ListQuery.Options = {}) {
|
||||
if (workflowIds.length === 0) {
|
||||
return [];
|
||||
|
|
@ -710,4 +713,22 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
|
|||
{ parentFolder: toFolderId === PROJECT_ROOT ? null : { id: toFolderId } },
|
||||
);
|
||||
}
|
||||
|
||||
async findWorkflowsWithNodeType(nodeTypes: string[]) {
|
||||
if (!nodeTypes?.length) return [];
|
||||
|
||||
const qb = this.createQueryBuilder('workflow');
|
||||
|
||||
const { whereClause, parameters } = buildWorkflowsByNodesQuery(
|
||||
nodeTypes,
|
||||
this.globalConfig.database.type,
|
||||
);
|
||||
|
||||
const workflows: Array<{ id: string; name: string; active: boolean }> = await qb
|
||||
.select(['workflow.id', 'workflow.name', 'workflow.active'])
|
||||
.where(whereClause, parameters)
|
||||
.getMany();
|
||||
|
||||
return workflows;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
import { buildWorkflowsByNodesQuery } from '../build-workflows-by-nodes-query';
|
||||
|
||||
describe('WorkflowRepository', () => {
|
||||
describe('filterWorkflowsByNodesConstructWhereClause', () => {
|
||||
it('should return the correct WHERE clause and parameters for sqlite', () => {
|
||||
const nodeTypes = ['HTTP Request', 'Set'];
|
||||
const expectedInQuery =
|
||||
"FROM json_each(workflow.nodes) WHERE json_extract(json_each.value, '$.type')";
|
||||
const expectedParameters = {
|
||||
nodeType0: 'HTTP Request',
|
||||
nodeType1: 'Set',
|
||||
nodeTypes,
|
||||
};
|
||||
|
||||
const { whereClause, parameters } = buildWorkflowsByNodesQuery(nodeTypes, 'sqlite');
|
||||
|
||||
expect(whereClause).toContain(expectedInQuery);
|
||||
expect(parameters).toEqual(expectedParameters);
|
||||
});
|
||||
|
||||
it('should return the correct WHERE clause and parameters for postgresdb', () => {
|
||||
const nodeTypes = ['HTTP Request', 'Set'];
|
||||
const expectedInQuery = 'FROM jsonb_array_elements(workflow.nodes::jsonb) AS node';
|
||||
const expectedParameters = { nodeTypes };
|
||||
|
||||
const { whereClause, parameters } = buildWorkflowsByNodesQuery(nodeTypes, 'postgresdb');
|
||||
|
||||
expect(whereClause).toContain(expectedInQuery);
|
||||
expect(parameters).toEqual(expectedParameters);
|
||||
});
|
||||
|
||||
it('should return the correct WHERE clause and parameters for mysqldb', () => {
|
||||
const nodeTypes = ['HTTP Request', 'Set'];
|
||||
const expectedWhereClause =
|
||||
"(JSON_SEARCH(JSON_EXTRACT(workflow.nodes, '$[*].type'), 'one', :nodeType0) IS NOT NULL OR JSON_SEARCH(JSON_EXTRACT(workflow.nodes, '$[*].type'), 'one', :nodeType1) IS NOT NULL)";
|
||||
const expectedParameters = {
|
||||
nodeType0: 'HTTP Request',
|
||||
nodeType1: 'Set',
|
||||
nodeTypes,
|
||||
};
|
||||
|
||||
const { whereClause, parameters } = buildWorkflowsByNodesQuery(nodeTypes, 'mysqldb');
|
||||
|
||||
expect(whereClause).toEqual(expectedWhereClause);
|
||||
expect(parameters).toEqual(expectedParameters);
|
||||
});
|
||||
});
|
||||
});
|
||||
56
packages/@n8n/db/src/utils/build-workflows-by-nodes-query.ts
Normal file
56
packages/@n8n/db/src/utils/build-workflows-by-nodes-query.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* Builds the WHERE clause and parameters for a query to find workflows by node types
|
||||
*/
|
||||
export function buildWorkflowsByNodesQuery(
|
||||
nodeTypes: string[],
|
||||
dbType: 'postgresdb' | 'mysqldb' | 'mariadb' | 'sqlite',
|
||||
) {
|
||||
let whereClause: string;
|
||||
|
||||
const parameters: Record<string, string | string[]> = { nodeTypes };
|
||||
|
||||
switch (dbType) {
|
||||
case 'postgresdb':
|
||||
whereClause = `EXISTS (
|
||||
SELECT 1
|
||||
FROM jsonb_array_elements(workflow.nodes::jsonb) AS node
|
||||
WHERE node->>'type' = ANY(:nodeTypes)
|
||||
)`;
|
||||
break;
|
||||
case 'mysqldb':
|
||||
case 'mariadb': {
|
||||
const conditions = nodeTypes
|
||||
.map(
|
||||
(_, i) =>
|
||||
`JSON_SEARCH(JSON_EXTRACT(workflow.nodes, '$[*].type'), 'one', :nodeType${i}) IS NOT NULL`,
|
||||
)
|
||||
.join(' OR ');
|
||||
|
||||
whereClause = `(${conditions})`;
|
||||
|
||||
nodeTypes.forEach((nodeType, index) => {
|
||||
parameters[`nodeType${index}`] = nodeType;
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'sqlite': {
|
||||
const conditions = nodeTypes
|
||||
.map(
|
||||
(_, i) =>
|
||||
`EXISTS (SELECT 1 FROM json_each(workflow.nodes) WHERE json_extract(json_each.value, '$.type') = :nodeType${i})`,
|
||||
)
|
||||
.join(' OR ');
|
||||
|
||||
whereClause = `(${conditions})`;
|
||||
|
||||
nodeTypes.forEach((nodeType, index) => {
|
||||
parameters[`nodeType${index}`] = nodeType;
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error('Unsupported database type');
|
||||
}
|
||||
|
||||
return { whereClause, parameters };
|
||||
}
|
||||
26
packages/@n8n/db/src/utils/timed-query.ts
Normal file
26
packages/@n8n/db/src/utils/timed-query.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { Logger } from '@n8n/backend-common';
|
||||
import { Timed } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
/**
|
||||
* Decorator that warns when database queries exceed a duration threshold.
|
||||
*
|
||||
* For options, see `@n8n/decorators/src/timed.ts`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* @Service()
|
||||
* class UserRepository {
|
||||
* @TimedQuery()
|
||||
* async findUsers() {
|
||||
* // will log warning if execution takes > 100ms
|
||||
* }
|
||||
*
|
||||
* @TimedQuery({ threshold: 50, logArgs: true })
|
||||
* async findUserById(id: string) {
|
||||
* // will log warning if execution takes >50ms, including args
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const TimedQuery = Timed(Container.get(Logger), 'Slow database query');
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/decorators",
|
||||
"version": "0.14.0",
|
||||
"version": "0.15.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
"format": "biome format --write .",
|
||||
"format:check": "biome ci .",
|
||||
"lint": "eslint . --quiet",
|
||||
"lintfix": "eslint . --fix",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"watch": "tsc -p tsconfig.build.json --watch",
|
||||
"test": "jest",
|
||||
"test:dev": "jest --watch"
|
||||
|
|
|
|||
|
|
@ -9,3 +9,4 @@ export * from './pubsub';
|
|||
export { Redactable } from './redactable';
|
||||
export * from './shutdown';
|
||||
export * from './module/module-metadata';
|
||||
export { Timed, TimedOptions } from './timed';
|
||||
|
|
|
|||
42
packages/@n8n/decorators/src/timed.ts
Normal file
42
packages/@n8n/decorators/src/timed.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
export interface TimedOptions {
|
||||
/** Duration (in ms) above which to log a warning. Defaults to `100`. */
|
||||
threshold?: number;
|
||||
/** Whether to include method parameters in the log. Defaults to `false`. */
|
||||
logArgs?: boolean;
|
||||
}
|
||||
|
||||
interface Logger {
|
||||
warn(message: string, meta?: object): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory to create decorators to warn when method calls exceed a duration threshold.
|
||||
*/
|
||||
export const Timed =
|
||||
(logger: Logger, msg = 'Slow method call') =>
|
||||
(options: TimedOptions = {}): MethodDecorator =>
|
||||
(_target, propertyKey, descriptor: PropertyDescriptor) => {
|
||||
const originalMethod = descriptor.value as (...args: unknown[]) => unknown;
|
||||
const thresholdMs = options.threshold ?? 100;
|
||||
const logArgs = options.logArgs ?? false;
|
||||
|
||||
descriptor.value = async function (...args: unknown[]) {
|
||||
const methodName = `${this.constructor.name}.${String(propertyKey)}`;
|
||||
const start = performance.now();
|
||||
const result = await originalMethod.apply(this, args);
|
||||
const durationMs = performance.now() - start;
|
||||
|
||||
if (durationMs > thresholdMs) {
|
||||
logger.warn(msg, {
|
||||
method: methodName,
|
||||
durationMs: Math.round(durationMs),
|
||||
thresholdMs,
|
||||
params: logArgs ? args : '[hidden]',
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/di",
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
"format": "biome format --write .",
|
||||
"format:check": "biome ci .",
|
||||
"lint": "eslint . --quiet",
|
||||
"lintfix": "eslint . --fix",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"watch": "tsc -p tsconfig.build.json --watch",
|
||||
"test": "jest",
|
||||
"test:dev": "jest --watch"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/errors",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
|
@ -9,10 +9,10 @@
|
|||
"format": "biome format --write .",
|
||||
"format:check": "biome ci .",
|
||||
"lint": "eslint . --quiet",
|
||||
"lintfix": "eslint . --fix",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"watch": "tsc -p tsconfig.build.json --watch",
|
||||
"test": "jest",
|
||||
"test:dev": "jest --watch"
|
||||
"test": "echo \"WARNING: no test specified\" && exit 0",
|
||||
"test:dev": "echo \"WARNING: no test specified\" && exit 0"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"module": "src/index.ts",
|
||||
|
|
@ -21,6 +21,10 @@
|
|||
"dist/**/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@n8n/typescript-config": "workspace:*"
|
||||
"@n8n/typescript-config": "workspace:*",
|
||||
"@sentry/node": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"callsites": "catalog:"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { Event } from '@sentry/node';
|
||||
import callsites from 'callsites';
|
||||
|
||||
import type { ErrorLevel, ReportingOptions } from './error.types';
|
||||
import type { ErrorLevel, ReportingOptions } from './types';
|
||||
|
||||
/**
|
||||
* @deprecated Use `UserError`, `OperationalError` or `UnexpectedError` instead.
|
||||
|
|
@ -26,9 +26,11 @@ export class ApplicationError extends Error {
|
|||
|
||||
try {
|
||||
const filePath = callsites()[2].getFileName() ?? '';
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const match = /packages\/([^\/]+)\//.exec(filePath)?.[1];
|
||||
|
||||
if (match) this.tags.packageName = match;
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user