mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 22:24:05 -08:00
Merge branch 'master' of github.com:n8n-io/n8n into feat-Add-Sysdig-credentials
This commit is contained in:
commit
ef213beffb
2
.github/scripts/package.json
vendored
2
.github/scripts/package.json
vendored
|
@ -5,7 +5,7 @@
|
|||
"debug": "4.3.4",
|
||||
"glob": "10.3.10",
|
||||
"p-limit": "3.1.0",
|
||||
"picocolors": "1.0.0",
|
||||
"picocolors": "1.0.1",
|
||||
"semver": "7.5.4",
|
||||
"tempfile": "5.0.0",
|
||||
"typescript": "*"
|
||||
|
|
3
.github/workflows/check-pr-title.yml
vendored
3
.github/workflows/check-pr-title.yml
vendored
|
@ -7,8 +7,7 @@ on:
|
|||
- edited
|
||||
- synchronize
|
||||
branches:
|
||||
- '**'
|
||||
- '!release/*'
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
check-pr-title:
|
||||
|
|
2
.github/workflows/chromatic.yml
vendored
2
.github/workflows/chromatic.yml
vendored
|
@ -4,7 +4,7 @@ on:
|
|||
workflow_dispatch:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
branch:
|
||||
branches:
|
||||
- 'master'
|
||||
paths:
|
||||
- packages/design-system/**
|
||||
|
|
4
.github/workflows/ci-postgres-mysql.yml
vendored
4
.github/workflows/ci-postgres-mysql.yml
vendored
|
@ -8,6 +8,10 @@ on:
|
|||
paths:
|
||||
- packages/cli/src/databases/**
|
||||
- .github/workflows/ci-postgres-mysql.yml
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
branches:
|
||||
- 'release/*'
|
||||
|
||||
concurrency:
|
||||
group: db-${{ github.event.pull_request.number || github.ref }}
|
||||
|
|
7
.github/workflows/ci-pull-requests.yml
vendored
7
.github/workflows/ci-pull-requests.yml
vendored
|
@ -1,6 +1,10 @@
|
|||
name: Build, unit test and lint branch
|
||||
|
||||
on: [pull_request]
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
- '!release/*'
|
||||
|
||||
jobs:
|
||||
install-and-build:
|
||||
|
@ -9,7 +13,6 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
repository: n8n-io/n8n
|
||||
ref: refs/pull/${{ github.event.pull_request.number }}/merge
|
||||
|
||||
- run: corepack enable
|
||||
|
|
43
.github/workflows/docker-images-benchmark.yml
vendored
Normal file
43
.github/workflows/docker-images-benchmark.yml
vendored
Normal file
|
@ -0,0 +1,43 @@
|
|||
name: Benchmark Docker Image CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'packages/@n8n/benchmark/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'pnpm-workspace.yaml'
|
||||
- '.github/workflows/docker-images-benchmark.yml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: .
|
||||
file: ./packages/@n8n/benchmark/Dockerfile
|
||||
platforms: linux/amd64
|
||||
provenance: false
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository_owner }}/n8n-benchmark:latest
|
5
.github/workflows/docker-images-nightly.yml
vendored
5
.github/workflows/docker-images-nightly.yml
vendored
|
@ -6,10 +6,6 @@ on:
|
|||
- cron: '0 1 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
repository:
|
||||
description: 'GitHub repository to create image off.'
|
||||
required: true
|
||||
default: 'n8n-io/n8n'
|
||||
branch:
|
||||
description: 'GitHub branch to create image off.'
|
||||
required: true
|
||||
|
@ -49,7 +45,6 @@ jobs:
|
|||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
repository: ${{ github.event.inputs.repository || 'n8n-io/n8n' }}
|
||||
ref: ${{ github.event.inputs.branch || 'master' }}
|
||||
|
||||
- name: Set up QEMU
|
||||
|
|
48
.github/workflows/docker-images.yml
vendored
48
.github/workflows/docker-images.yml
vendored
|
@ -1,48 +0,0 @@
|
|||
name: Docker Image CI
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
|
||||
- name: Get the version
|
||||
id: vars
|
||||
run: echo ::set-output name=tag::$(echo ${GITHUB_REF:14})
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: ./docker/images/n8n
|
||||
build-args: |
|
||||
N8N_VERSION=${{ steps.vars.outputs.tag }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
provenance: false
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_USERNAME }}/n8n:${{ steps.vars.outputs.tag }}
|
||||
ghcr.io/${{ github.repository_owner }}/n8n:${{ steps.vars.outputs.tag }}
|
11
.github/workflows/e2e-reusable.yml
vendored
11
.github/workflows/e2e-reusable.yml
vendored
|
@ -22,11 +22,6 @@ on:
|
|||
required: false
|
||||
default: 'browsers:node18.12.0-chrome107'
|
||||
type: string
|
||||
cache-key:
|
||||
description: 'Cache key for modules and build artifacts.'
|
||||
required: false
|
||||
default: ${{ github.sha }}-${{ inputs.run-env }}-e2e-modules
|
||||
type: string
|
||||
record:
|
||||
description: 'Record test run.'
|
||||
required: false
|
||||
|
@ -78,7 +73,6 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
repository: n8n-io/n8n
|
||||
ref: ${{ inputs.branch }}
|
||||
|
||||
- name: Checkout PR
|
||||
|
@ -111,7 +105,7 @@ jobs:
|
|||
/github/home/.cache
|
||||
/github/home/.pnpm-store
|
||||
./packages/**/dist
|
||||
key: ${{ inputs.cache-key }}
|
||||
key: ${{ github.sha }}-e2e
|
||||
|
||||
testing:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -128,7 +122,6 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
repository: n8n-io/n8n
|
||||
ref: ${{ inputs.branch }}
|
||||
|
||||
- name: Checkout PR
|
||||
|
@ -146,7 +139,7 @@ jobs:
|
|||
/github/home/.cache
|
||||
/github/home/.pnpm-store
|
||||
./packages/**/dist
|
||||
key: ${{ inputs.cache-key }}
|
||||
key: ${{ github.sha }}-e2e
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
|
4
.github/workflows/e2e-tests-pr.yml
vendored
4
.github/workflows/e2e-tests-pr.yml
vendored
|
@ -3,8 +3,9 @@ name: PR E2E
|
|||
on:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
branch:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'release/*'
|
||||
|
||||
concurrency:
|
||||
group: e2e-${{ github.event.pull_request.number || github.ref }}
|
||||
|
@ -18,7 +19,6 @@ jobs:
|
|||
with:
|
||||
pr_number: ${{ github.event.pull_request.number }}
|
||||
user: ${{ github.event.pull_request.user.login || 'PR User' }}
|
||||
spec: 'e2e/*'
|
||||
secrets:
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
|
||||
|
|
1
.github/workflows/linting-reusable.yml
vendored
1
.github/workflows/linting-reusable.yml
vendored
|
@ -21,7 +21,6 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
repository: n8n-io/n8n
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- run: corepack enable
|
||||
|
|
6
.github/workflows/release-create-pr.yml
vendored
6
.github/workflows/release-create-pr.yml
vendored
|
@ -56,12 +56,12 @@ jobs:
|
|||
git push -f origin refs/remotes/origin/${{ github.event.inputs.base-branch }}:refs/heads/release/${{ env.NEXT_RELEASE }}
|
||||
|
||||
- name: Push the release branch, and Create the PR
|
||||
uses: peter-evans/create-pull-request@v5
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
base: 'release/${{ env.NEXT_RELEASE }}'
|
||||
branch: '${{ env.NEXT_RELEASE }}-pr'
|
||||
branch: 'release-pr/${{ env.NEXT_RELEASE }}'
|
||||
commit-message: ':rocket: Release ${{ env.NEXT_RELEASE }}'
|
||||
delete-branch: true
|
||||
labels: 'release'
|
||||
labels: release,release:${{ github.event.inputs.release-type }}
|
||||
title: ':rocket: Release ${{ env.NEXT_RELEASE }}'
|
||||
body-path: 'CHANGELOG-${{ env.NEXT_RELEASE }}.md'
|
||||
|
|
111
.github/workflows/release-publish.yml
vendored
111
.github/workflows/release-publish.yml
vendored
|
@ -8,18 +8,15 @@ on:
|
|||
- 'release/*'
|
||||
|
||||
jobs:
|
||||
publish-release:
|
||||
if: github.event.pull_request.merged == true
|
||||
publish-to-npm:
|
||||
name: Publish to NPM
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
|
||||
timeout-minutes: 60
|
||||
if: github.event.pull_request.merged == true
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
|
||||
outputs:
|
||||
release: ${{ steps.set-release.outputs.release }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.1
|
||||
|
@ -51,25 +48,97 @@ jobs:
|
|||
pnpm publish -r --publish-branch ${{github.event.pull_request.base.ref}} --access public --tag rc --no-git-checks
|
||||
npm dist-tag rm n8n rc
|
||||
|
||||
- id: set-release
|
||||
run: echo "release=${{ env.RELEASE }}" >> $GITHUB_OUTPUT
|
||||
|
||||
publish-to-docker-hub:
|
||||
name: Publish to DockerHub
|
||||
needs: [publish-to-npm]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.merged == true
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: ./docker/images/n8n
|
||||
build-args: |
|
||||
N8N_VERSION=${{ needs.publish-to-npm.outputs.release }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
provenance: false
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_USERNAME }}/n8n:${{ needs.publish-to-npm.outputs.release }}
|
||||
ghcr.io/${{ github.repository_owner }}/n8n:${{ needs.publish-to-npm.outputs.release }}
|
||||
|
||||
create-github-release:
|
||||
name: Create a GitHub Release
|
||||
needs: [publish-to-npm, publish-to-docker-hub]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.merged == true
|
||||
timeout-minutes: 5
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Create a Release on GitHub
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
commit: ${{github.event.pull_request.base.ref}}
|
||||
tag: 'n8n@${{env.RELEASE}}'
|
||||
tag: 'n8n@${{ needs.publish-to-npm.outputs.release }}'
|
||||
prerelease: true
|
||||
makeLatest: false
|
||||
body: ${{github.event.pull_request.body}}
|
||||
|
||||
trigger-release-note:
|
||||
name: Trigger a release note
|
||||
needs: [publish-to-npm, create-github-release]
|
||||
if: github.event.pull_request.merged == true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger a release note
|
||||
continue-on-error: true
|
||||
run: curl -u docsWorkflows:${{ secrets.N8N_WEBHOOK_DOCS_PASSWORD }} --request GET 'https://internal.users.n8n.cloud/webhook/trigger-release-note' --header 'Content-Type:application/json' --data '{"version":"${{env.RELEASE}}"}'
|
||||
run: curl -u docsWorkflows:${{ secrets.N8N_WEBHOOK_DOCS_PASSWORD }} --request GET 'https://internal.users.n8n.cloud/webhook/trigger-release-note' --header 'Content-Type:application/json' --data '{"version":"${{ needs.publish-to-npm.outputs.release }}"}'
|
||||
|
||||
# - name: Merge Release into 'master'
|
||||
# run: |
|
||||
# git fetch origin
|
||||
# git checkout --track origin/master
|
||||
# git config user.name "Jan Oberhauser"
|
||||
# git config user.email jan.oberhauser@gmail.com
|
||||
# git merge --ff n8n@${{env.RELEASE}}
|
||||
# git push origin master
|
||||
# git push origin :${{github.event.pull_request.base.ref}}
|
||||
merge-back-into-master:
|
||||
name: Merge back into master
|
||||
needs: [publish-to-npm, create-github-release]
|
||||
if: ${{ github.event.pull_request.merged == true && !contains(github.event.pull_request.labels.*.name, 'release:patch') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: |
|
||||
git checkout --track origin/master
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
|
||||
git merge --ff n8n@${{ needs.publish-to-npm.outputs.release }}
|
||||
git push origin master
|
||||
git push origin :${{github.event.pull_request.base.ref}}
|
||||
|
|
1
.github/workflows/test-workflows.yml
vendored
1
.github/workflows/test-workflows.yml
vendored
|
@ -73,6 +73,7 @@ jobs:
|
|||
env:
|
||||
N8N_ENCRYPTION_KEY: ${{secrets.ENCRYPTION_KEY}}
|
||||
SKIP_STATISTICS_EVENTS: true
|
||||
DB_SQLITE_POOL_SIZE: 4
|
||||
# -
|
||||
# name: Export credentials
|
||||
# if: always()
|
||||
|
|
2
.github/workflows/units-tests-reusable.yml
vendored
2
.github/workflows/units-tests-reusable.yml
vendored
|
@ -36,7 +36,6 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
repository: n8n-io/n8n
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- run: corepack enable
|
||||
|
@ -50,6 +49,7 @@ jobs:
|
|||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup build cache
|
||||
if: inputs.collectCoverage != true
|
||||
uses: rharkor/caching-for-turbo@v1.5
|
||||
|
||||
- name: Build
|
||||
|
|
166
CHANGELOG.md
166
CHANGELOG.md
|
@ -1,3 +1,169 @@
|
|||
# [1.56.0](https://github.com/n8n-io/n8n/compare/n8n@1.55.0...n8n@1.56.0) (2024-08-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Better errors in Switch, If and Filter nodes ([#10457](https://github.com/n8n-io/n8n/issues/10457)) ([aea82cb](https://github.com/n8n-io/n8n/commit/aea82cb74421d516919742127daf669808b57604))
|
||||
* **Calendly Trigger Node:** Fix issue with webhook url matching ([#10378](https://github.com/n8n-io/n8n/issues/10378)) ([09c3a8b](https://github.com/n8n-io/n8n/commit/09c3a8b36733a9634ef5948922d6aa7a19bbb592))
|
||||
* **core:** Fix payload property in `workflow-post-execute` event ([#10413](https://github.com/n8n-io/n8n/issues/10413)) ([d98e29e](https://github.com/n8n-io/n8n/commit/d98e29e3d53de87aec276260615fa60473a2692f))
|
||||
* **core:** Fix XSS validation and separate URL validation ([#10424](https://github.com/n8n-io/n8n/issues/10424)) ([91467ab](https://github.com/n8n-io/n8n/commit/91467ab325e4c71c20c522f3143246d270101626))
|
||||
* **core:** Replace `sanitize-html` with `xss` in XSS validator constraint ([#10479](https://github.com/n8n-io/n8n/issues/10479)) ([5dea51a](https://github.com/n8n-io/n8n/commit/5dea51aad7d9e7ffc676d16f4bbbdecce5876f0b))
|
||||
* **core:** Use class-validator with XSS check for survey answers ([#10490](https://github.com/n8n-io/n8n/issues/10490)) ([547a606](https://github.com/n8n-io/n8n/commit/547a60642ce9e54819d4e600c822d87dabd59b2e))
|
||||
* **core:** Use explicit types in configs to ensure valid decorator metadata ([#10433](https://github.com/n8n-io/n8n/issues/10433)) ([2043daa](https://github.com/n8n-io/n8n/commit/2043daa2570bc04b0b8d41f277901a8cc8a7b98f))
|
||||
* **editor:** Add workflow scopes when initializing workflow ([#10455](https://github.com/n8n-io/n8n/issues/10455)) ([b857c2c](https://github.com/n8n-io/n8n/commit/b857c2cda0a9e4386a540d5e1e741570d9453588))
|
||||
* **editor:** Buffer json chunks in stream response ([#10439](https://github.com/n8n-io/n8n/issues/10439)) ([37797f3](https://github.com/n8n-io/n8n/commit/37797f38d81b12d030ba85034baeb49192ea575c))
|
||||
* **editor:** Fix flaky mapping tests ([#10453](https://github.com/n8n-io/n8n/issues/10453)) ([fc6d413](https://github.com/n8n-io/n8n/commit/fc6d4138d58282f676b32f1a6011b1b6d0184bf2))
|
||||
* **editor:** Fix overflow in AI Assistant chat messages ([#10491](https://github.com/n8n-io/n8n/issues/10491)) ([4a6ca63](https://github.com/n8n-io/n8n/commit/4a6ca632100731f85875c639f2164bf1ef415009))
|
||||
* **editor:** Highlight matching type in filter component ([#10425](https://github.com/n8n-io/n8n/issues/10425)) ([6bca879](https://github.com/n8n-io/n8n/commit/6bca879d4ae30c7f9a35e8d6672de42cf93be727))
|
||||
* **editor:** Show item count in output panel schema view ([#10426](https://github.com/n8n-io/n8n/issues/10426)) ([4dee7cc](https://github.com/n8n-io/n8n/commit/4dee7cc36e5f7768d0b71095b194bf357c92e941))
|
||||
* **editor:** Truncate long data pill labels in schema view ([#10427](https://github.com/n8n-io/n8n/issues/10427)) ([1bf2f4f](https://github.com/n8n-io/n8n/commit/1bf2f4f6171d666391bb3a3a312468bc083446e3))
|
||||
* Filter component - improve errors ([#10456](https://github.com/n8n-io/n8n/issues/10456)) ([61ac0c7](https://github.com/n8n-io/n8n/commit/61ac0c77755210f570b887951fe6bbec1a323811))
|
||||
* **Google Sheets Node:** Better error when column to match on is empty ([#10442](https://github.com/n8n-io/n8n/issues/10442)) ([ce46bf5](https://github.com/n8n-io/n8n/commit/ce46bf516a86d9779f37dd75b0c680d26d88e15d))
|
||||
* **Google Sheets Node:** Update name and hint for useAppend option ([#10443](https://github.com/n8n-io/n8n/issues/10443)) ([c5a0c04](https://github.com/n8n-io/n8n/commit/c5a0c049eaf44419c690d151de42fb0c10bd406e))
|
||||
* **Google Sheets Node:** Update to returnAllMatches option ([#10440](https://github.com/n8n-io/n8n/issues/10440)) ([f7fb02e](https://github.com/n8n-io/n8n/commit/f7fb02e92a756781f8e35bbbfc25d71c12cb70af))
|
||||
* **Invoice Ninja Node:** Fix payment types ([#10462](https://github.com/n8n-io/n8n/issues/10462)) ([129245d](https://github.com/n8n-io/n8n/commit/129245da10be1d645f61e929e40b128bd7813f17))
|
||||
* **n8n Form Trigger Node:** Show basic authentication modal on wrong credentials ([#10423](https://github.com/n8n-io/n8n/issues/10423)) ([0dc3e99](https://github.com/n8n-io/n8n/commit/0dc3e99b26bec45e747d83f383cfe5169d89e6b7))
|
||||
* **OpenAI Node:** Throw node operations error in case of openAi client error ([#10448](https://github.com/n8n-io/n8n/issues/10448)) ([0d3ed46](https://github.com/n8n-io/n8n/commit/0d3ed461996bbad06015c455f133baab6506437f))
|
||||
* Project Viewer always seeing a connection error when testing credentials ([#10417](https://github.com/n8n-io/n8n/issues/10417)) ([613cdd2](https://github.com/n8n-io/n8n/commit/613cdd2ba2c0f224c4857a5fc3eea36dbd683049))
|
||||
* Remove unimplemented Postgres credentials options ([#10461](https://github.com/n8n-io/n8n/issues/10461)) ([17ac784](https://github.com/n8n-io/n8n/commit/17ac7844f29d819b91dfaf90b9fe386d98060c42))
|
||||
* Rename Assistant back ([#10481](https://github.com/n8n-io/n8n/issues/10481)) ([c410aed](https://github.com/n8n-io/n8n/commit/c410aed4c22182943dc80ede63acda00b7898e10))
|
||||
* Require mfa code to change email ([#10354](https://github.com/n8n-io/n8n/issues/10354)) ([39c8e50](https://github.com/n8n-io/n8n/commit/39c8e50ad0513649f5a8cef911b7d6cdd61c2372))
|
||||
* **Respond to Webhook Node:** Fix issue preventing the chat trigger from working ([#9886](https://github.com/n8n-io/n8n/issues/9886)) ([9d6ad88](https://github.com/n8n-io/n8n/commit/9d6ad88c14a88fd0dfcb4f9981e38d19cf5f3067))
|
||||
* Show input names when node has multiple inputs ([#10434](https://github.com/n8n-io/n8n/issues/10434)) ([973956c](https://github.com/n8n-io/n8n/commit/973956cc26c78c329ff6eb6934d4f0a24060c87c))
|
||||
* **Toggl Trigger Node:** Update API version ([#10207](https://github.com/n8n-io/n8n/issues/10207)) ([9bdb1d6](https://github.com/n8n-io/n8n/commit/9bdb1d6dca43fe491c5eb96f093b7eec4509eaff))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **core:** Support bidirectional communication between specific mains and specific workers ([#10377](https://github.com/n8n-io/n8n/issues/10377)) ([d0fc9de](https://github.com/n8n-io/n8n/commit/d0fc9dee0e17211c1ed130b19286e9573c9ebfbd))
|
||||
* **Facebook Graph API Node:** Update node to support API v18 - v20 ([#10419](https://github.com/n8n-io/n8n/issues/10419)) ([e7ee10f](https://github.com/n8n-io/n8n/commit/e7ee10f243663d899d32e61bc6264b4df444e2af))
|
||||
|
||||
|
||||
|
||||
# [1.55.0](https://github.com/n8n-io/n8n/compare/n8n@1.54.0...n8n@1.55.0) (2024-08-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Add better error handling for chat errors ([#10408](https://github.com/n8n-io/n8n/issues/10408)) ([f82b6e4](https://github.com/n8n-io/n8n/commit/f82b6e4ba9bf527b3a4c17872162d9ae124ead0d))
|
||||
* **AI Agent Node:** Fix issues with some tools not populating ([#10406](https://github.com/n8n-io/n8n/issues/10406)) ([51a1edd](https://github.com/n8n-io/n8n/commit/51a1eddbf00393f3881c340cf37cfcca59566c99))
|
||||
* **core:** Account for cancelling an execution with no workers available ([#10343](https://github.com/n8n-io/n8n/issues/10343)) ([b044e78](https://github.com/n8n-io/n8n/commit/b044e783e73a499dbd7532a5d489a782d3d021da))
|
||||
* **core:** Account for owner when filtering by project ID in `GET /workflows` in Public API ([#10379](https://github.com/n8n-io/n8n/issues/10379)) ([5ac65b3](https://github.com/n8n-io/n8n/commit/5ac65b36bcb1351c6233b951f064f60862f790a5))
|
||||
* **core:** Enforce shutdown timer and sequence on `SIGINT` for main ([#10346](https://github.com/n8n-io/n8n/issues/10346)) ([5255793](https://github.com/n8n-io/n8n/commit/5255793afee5653d8356b8e4d2e1009d5cf36164))
|
||||
* **core:** Filter out prototype and constructor lookups in expressions ([#10382](https://github.com/n8n-io/n8n/issues/10382)) ([8e7d29a](https://github.com/n8n-io/n8n/commit/8e7d29ad3c4872b1cc147dfcfe9a864ba916692f))
|
||||
* **core:** Fix duplicate Redis publisher ([#10392](https://github.com/n8n-io/n8n/issues/10392)) ([45813de](https://github.com/n8n-io/n8n/commit/45813debc963096f63cc0aabe82d9d9f853a39d7))
|
||||
* **core:** Fix worker shutdown errors when active executions ([#10353](https://github.com/n8n-io/n8n/issues/10353)) ([e071b73](https://github.com/n8n-io/n8n/commit/e071b73bab34edd4b3e6aef6497514acc504cdc6))
|
||||
* **core:** Prevent XSS in user update endpoints ([#10338](https://github.com/n8n-io/n8n/issues/10338)) ([7898498](https://github.com/n8n-io/n8n/commit/78984986a6b4add89df9743b94c113046f1d5ee8))
|
||||
* **core:** Prevent XSS via static cache dir ([#10339](https://github.com/n8n-io/n8n/issues/10339)) ([4f392b5](https://github.com/n8n-io/n8n/commit/4f392b5e3e0ee166e85a2e060b3ec7fcf145229b))
|
||||
* **core:** Rate limit MFA activation and verification endpoints ([#10330](https://github.com/n8n-io/n8n/issues/10330)) ([b6c47c0](https://github.com/n8n-io/n8n/commit/b6c47c0e3214878d42980d5c9535df52b3984b3c))
|
||||
* **editor:** Connect up new project viewer role to the FE ([#9913](https://github.com/n8n-io/n8n/issues/9913)) ([117e2d9](https://github.com/n8n-io/n8n/commit/117e2d968fcc535f32c583ac8f2dc8a84e8cd6bd))
|
||||
* **editor:** Enable credential sharing between all types of projects ([#10233](https://github.com/n8n-io/n8n/issues/10233)) ([1cf48cc](https://github.com/n8n-io/n8n/commit/1cf48cc3019c1cf27e2f3c9affd18426237e9064))
|
||||
* **editor:** Fix rendering of SVG icons in public chat on iOS ([#10381](https://github.com/n8n-io/n8n/issues/10381)) ([7ab3811](https://github.com/n8n-io/n8n/commit/7ab38114dbf3881afba39287a061446ec4bf0431))
|
||||
* **editor:** Fixing XSS vulnerability in toast messages ([#10329](https://github.com/n8n-io/n8n/issues/10329)) ([38bdd9f](https://github.com/n8n-io/n8n/commit/38bdd9f5d0d9ca06fab1a7e1a3e7a4a648a6a89a))
|
||||
* **editor:** Revert change that hid swagger docs in the ui ([#10350](https://github.com/n8n-io/n8n/issues/10350)) ([bae49d3](https://github.com/n8n-io/n8n/commit/bae49d3198d4bcc27e7996cd4f7be3132becc98e))
|
||||
* **n8n Form Trigger Node:** Fix issue preventing v1 node from working ([#10364](https://github.com/n8n-io/n8n/issues/10364)) ([9b647a9](https://github.com/n8n-io/n8n/commit/9b647a9837434e8b75e3ad754ff5136bb5ac760d))
|
||||
* Require mfa code for password change if its enabled ([#10341](https://github.com/n8n-io/n8n/issues/10341)) ([9d7caac](https://github.com/n8n-io/n8n/commit/9d7caacc699f10962783393925a980ec6f1ca975))
|
||||
* Require mfa code to disable mfa ([#10345](https://github.com/n8n-io/n8n/issues/10345)) ([3384f52](https://github.com/n8n-io/n8n/commit/3384f52a35b835ba1d8633dc94bab0ad6e7023b3))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add Ask assistant behind feature flag ([#9995](https://github.com/n8n-io/n8n/issues/9995)) ([5ed2a77](https://github.com/n8n-io/n8n/commit/5ed2a77740db1f02b27c571f4dfdfa206ebdb19c))
|
||||
* **AI Transform Node:** New node ([#10405](https://github.com/n8n-io/n8n/issues/10405)) ([4d222ac](https://github.com/n8n-io/n8n/commit/4d222ac19d943b69fd9f87abe5e5c5f5141eed8d))
|
||||
* **AI Transform Node:** New node ([#9990](https://github.com/n8n-io/n8n/issues/9990)) ([0de9d56](https://github.com/n8n-io/n8n/commit/0de9d56619ed1c055407353046b8a9ebe78da527))
|
||||
* **core:** Allow overriding npm registry for community packages ([#10325](https://github.com/n8n-io/n8n/issues/10325)) ([33a2703](https://github.com/n8n-io/n8n/commit/33a2703429d9eaa41f72d2e7d2da5be60b6c620f))
|
||||
* **editor:** Add schema view to expression modal ([#9976](https://github.com/n8n-io/n8n/issues/9976)) ([71b6c67](https://github.com/n8n-io/n8n/commit/71b6c671797024d7b516352fa9b7ecda101ce3b2))
|
||||
* **MySQL Node:** Return decimal types as numbers ([#10313](https://github.com/n8n-io/n8n/issues/10313)) ([f744d7c](https://github.com/n8n-io/n8n/commit/f744d7c100be68669d9a3efd0033dd371a3cfaf7))
|
||||
* **Okta Node:** Add Okta Node ([#10278](https://github.com/n8n-io/n8n/issues/10278)) ([5cac0f3](https://github.com/n8n-io/n8n/commit/5cac0f339d649cfe5857d33738210cbc1599370b))
|
||||
|
||||
|
||||
|
||||
# [1.54.0](https://github.com/n8n-io/n8n/compare/n8n@1.53.0...n8n@1.54.0) (2024-08-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** Ensure OAuth token data is not stubbed in source control ([#10302](https://github.com/n8n-io/n8n/issues/10302)) ([98115e9](https://github.com/n8n-io/n8n/commit/98115e95df8289a8ec400a570a7f256382f8e286))
|
||||
* **core:** Fix expressions in webhook nodes(Form, Webhook) to access previous node's data ([#10247](https://github.com/n8n-io/n8n/issues/10247)) ([88a1701](https://github.com/n8n-io/n8n/commit/88a170176a3447e7f847e9cf145aeb867b1c5fcf))
|
||||
* **core:** Fix user telemetry bugs ([#10293](https://github.com/n8n-io/n8n/issues/10293)) ([42a0b59](https://github.com/n8n-io/n8n/commit/42a0b594d6ea2527c55a2aa9976c904cf70ecf92))
|
||||
* **core:** Make execution and its data creation atomic ([#10276](https://github.com/n8n-io/n8n/issues/10276)) ([ae50bb9](https://github.com/n8n-io/n8n/commit/ae50bb95a8e5bf1cdbf9483da54b84094b82e260))
|
||||
* **core:** Make OAuth1/OAuth2 callback not require auth ([#10263](https://github.com/n8n-io/n8n/issues/10263)) ([a8e2774](https://github.com/n8n-io/n8n/commit/a8e2774f5382e202556b5506c7788265786aa973))
|
||||
* **core:** Revert transactions until we remove the legacy sqlite driver ([#10299](https://github.com/n8n-io/n8n/issues/10299)) ([1eba7c3](https://github.com/n8n-io/n8n/commit/1eba7c3c763ac5b6b28c1c6fc43fc8c215249292))
|
||||
* **core:** Surface enterprise trial error message ([#10267](https://github.com/n8n-io/n8n/issues/10267)) ([432ac1d](https://github.com/n8n-io/n8n/commit/432ac1da59e173ce4c0f2abbc416743d9953ba70))
|
||||
* **core:** Upgrade tournament to address some XSS vulnerabilities ([#10277](https://github.com/n8n-io/n8n/issues/10277)) ([43ae159](https://github.com/n8n-io/n8n/commit/43ae159ea40c574f8e41bdfd221ab2bf3268eee7))
|
||||
* **core:** VM2 sandbox should not throw on `new Promise` ([#10298](https://github.com/n8n-io/n8n/issues/10298)) ([7e95f9e](https://github.com/n8n-io/n8n/commit/7e95f9e2e40a99871f1b6abcdacb39ac5f857332))
|
||||
* **core:** Webhook and form baseUrl missing ([#10290](https://github.com/n8n-io/n8n/issues/10290)) ([8131d66](https://github.com/n8n-io/n8n/commit/8131d66f8ca1b1da00597a12859ee4372148a0c9))
|
||||
* **editor:** Enable moving resources only if team projects are available by the license ([#10271](https://github.com/n8n-io/n8n/issues/10271)) ([42ba884](https://github.com/n8n-io/n8n/commit/42ba8841c401126c77158a53dc8fcbb45dfce8fd))
|
||||
* **editor:** Fix execution retry button ([#10275](https://github.com/n8n-io/n8n/issues/10275)) ([55f2ffe](https://github.com/n8n-io/n8n/commit/55f2ffe256c91a028cee95c3bbb37a093a1c0f81))
|
||||
* **editor:** Update design system Avatar component to show initials also when only firstName or lastName is given ([#10308](https://github.com/n8n-io/n8n/issues/10308)) ([46bbf09](https://github.com/n8n-io/n8n/commit/46bbf09beacad12472d91786b91d845fe2afb26d))
|
||||
* **editor:** Update tags filter/editor to not show non existing tag as a selectable option ([#10297](https://github.com/n8n-io/n8n/issues/10297)) ([557a76e](https://github.com/n8n-io/n8n/commit/557a76ec2326de72fb7a8b46fc4353f8fd9b591d))
|
||||
* **Invoice Ninja Node:** Fix payment types ([#10196](https://github.com/n8n-io/n8n/issues/10196)) ([c5acbb7](https://github.com/n8n-io/n8n/commit/c5acbb7ec0d24ec9b30c221fa3b2fb615fb9ec7f))
|
||||
* Loop node no input data shown ([#10224](https://github.com/n8n-io/n8n/issues/10224)) ([c8ee852](https://github.com/n8n-io/n8n/commit/c8ee852159207be0cfe2c3e0ee8e7b29d838aa35))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **core:** Allow filtering executions and users by project in Public API ([#10250](https://github.com/n8n-io/n8n/issues/10250)) ([7056e50](https://github.com/n8n-io/n8n/commit/7056e50b006bda665f64ce6234c5c1967891c415))
|
||||
* **core:** Allow transferring credentials in Public API ([#10259](https://github.com/n8n-io/n8n/issues/10259)) ([07d7b24](https://github.com/n8n-io/n8n/commit/07d7b247f02a9d7185beca7817deb779a3d665dd))
|
||||
* **core:** Show sub-node error on the logs pane. Open logs pane on sub-node error ([#10248](https://github.com/n8n-io/n8n/issues/10248)) ([57d1c9a](https://github.com/n8n-io/n8n/commit/57d1c9a99e97308f2f1b8ae05ac3861a835e8e5a))
|
||||
* **core:** Support community packages in scaling-mode ([#10228](https://github.com/n8n-io/n8n/issues/10228)) ([88086a4](https://github.com/n8n-io/n8n/commit/88086a41ff5b804b35aa9d9503dc2d48836fe4ec))
|
||||
* **core:** Support create, delete, edit role for users in Public API ([#10279](https://github.com/n8n-io/n8n/issues/10279)) ([84efbd9](https://github.com/n8n-io/n8n/commit/84efbd9b9c51f536b21a4f969ab607d277bef692))
|
||||
* **core:** Support create, read, update, delete projects in Public API ([#10269](https://github.com/n8n-io/n8n/issues/10269)) ([489ce10](https://github.com/n8n-io/n8n/commit/489ce100634c3af678fb300e9a39d273042542e6))
|
||||
* **editor:** Auto-add LLM chain for new LLM nodes on empty canvas ([#10245](https://github.com/n8n-io/n8n/issues/10245)) ([06419d9](https://github.com/n8n-io/n8n/commit/06419d9483ae916e79aace6d8c17e265b419b15d))
|
||||
* **Elasticsearch Node:** Add bulk operations for Elasticsearch ([#9940](https://github.com/n8n-io/n8n/issues/9940)) ([bf8f848](https://github.com/n8n-io/n8n/commit/bf8f848645dfd31527713a55bd1fc93865327017))
|
||||
* **Lemlist Trigger Node:** Update Trigger events ([#10311](https://github.com/n8n-io/n8n/issues/10311)) ([15f10ec](https://github.com/n8n-io/n8n/commit/15f10ec325cb5eda0f952bed3a5f171dd91bc639))
|
||||
* **MongoDB Node:** Add projection to query options on Find ([#9972](https://github.com/n8n-io/n8n/issues/9972)) ([0a84e0d](https://github.com/n8n-io/n8n/commit/0a84e0d8b047669f5cf023c21383d01c929c5b4f))
|
||||
* **Postgres Chat Memory, Redis Chat Memory, Xata:** Add support for context window length ([#10203](https://github.com/n8n-io/n8n/issues/10203)) ([e3edeaa](https://github.com/n8n-io/n8n/commit/e3edeaa03526f041d15d1099ea91869e38a0decc))
|
||||
* **Stripe Trigger Node:** Add Stripe webhook descriptions based on the workflow ID and name ([#9956](https://github.com/n8n-io/n8n/issues/9956)) ([3433465](https://github.com/n8n-io/n8n/commit/34334651e0e6874736a437a894176bed4590e5a7))
|
||||
* **Webflow Node:** Update to use the v2 API ([#9996](https://github.com/n8n-io/n8n/issues/9996)) ([6d8323f](https://github.com/n8n-io/n8n/commit/6d8323fadea8af04483eb1a873df0cf3ccc2a891))
|
||||
|
||||
|
||||
|
||||
# [1.53.0](https://github.com/n8n-io/n8n/compare/n8n@1.52.0...n8n@1.53.0) (2024-07-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Better error message when calling data transformation functions on a null value ([#10210](https://github.com/n8n-io/n8n/issues/10210)) ([1718125](https://github.com/n8n-io/n8n/commit/1718125c6d8589cf24dc8d34f6808dd6f1802691))
|
||||
* **core:** Fix missing successful items on continueErrorOutput with multiple outputs ([#10218](https://github.com/n8n-io/n8n/issues/10218)) ([1a7713e](https://github.com/n8n-io/n8n/commit/1a7713ef263680da43f08b6c8a15aee7a0341493))
|
||||
* **core:** Flush instance stopped event immediately ([#10238](https://github.com/n8n-io/n8n/issues/10238)) ([d6770b5](https://github.com/n8n-io/n8n/commit/d6770b5fcaec6438d677b918aaeb1669ad7424c2))
|
||||
* **core:** Restore log event `n8n.workflow.failed` ([#10253](https://github.com/n8n-io/n8n/issues/10253)) ([3e96b29](https://github.com/n8n-io/n8n/commit/3e96b293329525c9d4b2fcef87b3803e458c8e7f))
|
||||
* **core:** Upgrade @n8n/vm2 to address CVE‑2023‑37466 ([#10265](https://github.com/n8n-io/n8n/issues/10265)) ([2a09a03](https://github.com/n8n-io/n8n/commit/2a09a036d2e916acff7ee50904f1d011a93758e1))
|
||||
* **editor:** Defer `User saved credentials` telemetry event for OAuth credentials ([#10215](https://github.com/n8n-io/n8n/issues/10215)) ([40a5226](https://github.com/n8n-io/n8n/commit/40a5226e24448a4428143e69d80ebc78238365a1))
|
||||
* **editor:** Fix custom API call notice ([#10227](https://github.com/n8n-io/n8n/issues/10227)) ([5b47c8b](https://github.com/n8n-io/n8n/commit/5b47c8b57b25528cd2d6f97bc6d98707d47f35bc))
|
||||
* **editor:** Fix issue with existing credential not opening in HTTP agent tool ([#10167](https://github.com/n8n-io/n8n/issues/10167)) ([906b4c3](https://github.com/n8n-io/n8n/commit/906b4c3c7b2919111cf23eaa12b3c4d507969179))
|
||||
* **editor:** Fix parameter input glitch when there was an error loading remote options ([#10209](https://github.com/n8n-io/n8n/issues/10209)) ([c0e3743](https://github.com/n8n-io/n8n/commit/c0e37439a87105a0e66c8ebced42c06dab30dc5e))
|
||||
* **editor:** Fix workflow execution list scrolling after filter change ([#10226](https://github.com/n8n-io/n8n/issues/10226)) ([7e64358](https://github.com/n8n-io/n8n/commit/7e643589c67adc0218216ec4b89a95f0edfedbee))
|
||||
* **Google BigQuery Node:** Send timeoutMs in query, pagination support ([#10205](https://github.com/n8n-io/n8n/issues/10205)) ([f5722e8](https://github.com/n8n-io/n8n/commit/f5722e8823ccd2bc2b5f43ba3c849797d5690a93))
|
||||
* **Google Sheets Node:** Add column names row if sheet is empty ([#10200](https://github.com/n8n-io/n8n/issues/10200)) ([82eba9f](https://github.com/n8n-io/n8n/commit/82eba9fc5ff49b8e2a9db93c10b253fb67a8c644))
|
||||
* **Google Sheets Node:** Do not insert row_number as a new column, do not checkForSchemaChanges in update operation ([#10201](https://github.com/n8n-io/n8n/issues/10201)) ([5136d10](https://github.com/n8n-io/n8n/commit/5136d10ca3492f92af67d4a1d4abc774419580cc))
|
||||
* **Google Sheets Node:** Fix Google Sheet URL regex ([#10195](https://github.com/n8n-io/n8n/issues/10195)) ([e6fd996](https://github.com/n8n-io/n8n/commit/e6fd996973d4f40facf0ebf1eea3cc26acd0603d))
|
||||
* **HTTP Request Node:** Resolve max pages expression ([#10192](https://github.com/n8n-io/n8n/issues/10192)) ([bfc8e1b](https://github.com/n8n-io/n8n/commit/bfc8e1b56f7714e1f52aae747d58d686b86e60f0))
|
||||
* **LinkedIn Node:** Fix issue with some characters cutting off posts early ([#10185](https://github.com/n8n-io/n8n/issues/10185)) ([361b5e7](https://github.com/n8n-io/n8n/commit/361b5e7c37ba49b68dcf5b8122621aad4d8d96e0))
|
||||
* **Postgres Node:** Expressions in query parameters for Postgres executeQuery operation ([#10217](https://github.com/n8n-io/n8n/issues/10217)) ([519fc4d](https://github.com/n8n-io/n8n/commit/519fc4d75325a80b84cc4dcacf52d6f4c02e3a44))
|
||||
* **Postgres Node:** Option to treat query parameters enclosed in single quotas as text ([#10214](https://github.com/n8n-io/n8n/issues/10214)) ([00ec253](https://github.com/n8n-io/n8n/commit/00ec2533374d3def465efee718592fc4001d5602))
|
||||
* **Read/Write Files from Disk Node:** Notice update in file selector, replace backslashes with forward slashes if windows path ([#10186](https://github.com/n8n-io/n8n/issues/10186)) ([3eac673](https://github.com/n8n-io/n8n/commit/3eac673b17986c5c74bd2adb5ad589ba0ca55319))
|
||||
* **Text Classifier Node:** Use proper documentation URL and respect continueOnFail ([#10216](https://github.com/n8n-io/n8n/issues/10216)) ([452f52c](https://github.com/n8n-io/n8n/commit/452f52c124017e002e86c547ba42b1633b14beed))
|
||||
* **Trello Node:** Use body for POST requests ([#10189](https://github.com/n8n-io/n8n/issues/10189)) ([7775d50](https://github.com/n8n-io/n8n/commit/7775d5059b7f69d9af22e7ad7d12c6cf9092a4e5))
|
||||
* **Wait Node:** Authentication fix ([#10236](https://github.com/n8n-io/n8n/issues/10236)) ([f87854f](https://github.com/n8n-io/n8n/commit/f87854f8db360b7b870583753fcfb4af95adab8c))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Calendly Trigger Node:** Add OAuth Credentials Support ([#10251](https://github.com/n8n-io/n8n/issues/10251)) ([326c983](https://github.com/n8n-io/n8n/commit/326c983915a2c382e32398358e7dcadd022c0b77))
|
||||
* **core:** Allow filtering workflows by project and transferring workflows in Public API ([#10231](https://github.com/n8n-io/n8n/issues/10231)) ([d719899](https://github.com/n8n-io/n8n/commit/d719899223907b20a17883a35e4ef637a3453532))
|
||||
* **editor:** Show new executions as `Queued` in the UI, until they actually start ([#10204](https://github.com/n8n-io/n8n/issues/10204)) ([44728d7](https://github.com/n8n-io/n8n/commit/44728d72423f5549dda09589f4a618ebd80899cb))
|
||||
* **HTTP Request Node:** Add option to disable lowercase headers ([#10154](https://github.com/n8n-io/n8n/issues/10154)) ([5aba69b](https://github.com/n8n-io/n8n/commit/5aba69bcf4d232d9860f3cd9fe57cb8839a2f96f))
|
||||
* **Information Extractor Node:** Add new simplified AI-node for information extraction ([#10149](https://github.com/n8n-io/n8n/issues/10149)) ([3d235b0](https://github.com/n8n-io/n8n/commit/3d235b0b2df756df35ac60e3dcd87ad183a07167))
|
||||
* Introduce Google Cloud Platform as external secrets provider ([#10146](https://github.com/n8n-io/n8n/issues/10146)) ([3ccb9df](https://github.com/n8n-io/n8n/commit/3ccb9df2f902e46f8cbb9c46c0727f29d752a773))
|
||||
* **n8n Form Trigger Node:** Improvements ([#10092](https://github.com/n8n-io/n8n/issues/10092)) ([711b667](https://github.com/n8n-io/n8n/commit/711b667ebefe55740e5eb39f1f0f24ceee10e7b0))
|
||||
* Recovery option for jsonParse helper ([#10182](https://github.com/n8n-io/n8n/issues/10182)) ([d165b33](https://github.com/n8n-io/n8n/commit/d165b33ceac4d24d0fc290bffe63b5f551204e38))
|
||||
* **Sentiment Analysis Node:** Implement Sentiment Analysis node ([#10184](https://github.com/n8n-io/n8n/issues/10184)) ([8ef0a0c](https://github.com/n8n-io/n8n/commit/8ef0a0c58ac2a84aad649ccbe72aa907d005cc44))
|
||||
* **Shopify Node:** Update Shopify API version ([#10155](https://github.com/n8n-io/n8n/issues/10155)) ([e2ee915](https://github.com/n8n-io/n8n/commit/e2ee91569a382bfbf787cf45204c72c821a860a0))
|
||||
* Support create, read, delete variables in Public API ([#10241](https://github.com/n8n-io/n8n/issues/10241)) ([af695eb](https://github.com/n8n-io/n8n/commit/af695ebf934526d926ea87fe87df61aa73d70979))
|
||||
|
||||
|
||||
|
||||
# [1.52.0](https://github.com/n8n-io/n8n/compare/n8n@1.51.0...n8n@1.52.0) (2024-07-24)
|
||||
|
||||
|
||||
|
|
|
@ -95,8 +95,8 @@ development environment ready in minutes.
|
|||
## License
|
||||
|
||||
n8n is [fair-code](https://faircode.io) distributed under the
|
||||
[**Sustainable Use License**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md) and the
|
||||
[**n8n Enterprise License**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE_EE.md).
|
||||
[**Sustainable Use License**](https://github.com/n8n-io/n8n/blob/master/LICENSE.md) and the
|
||||
[**n8n Enterprise License**](https://github.com/n8n-io/n8n/blob/master/LICENSE_EE.md).
|
||||
|
||||
Proprietary licenses are available for enterprise customers. [Get in touch](mailto:license@n8n.io)
|
||||
|
||||
|
|
3
cypress/composables/modals/save-changes-modal.ts
Normal file
3
cypress/composables/modals/save-changes-modal.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export function getSaveChangesModal() {
|
||||
return cy.get('.el-overlay').contains('Save changes before leaving?');
|
||||
}
|
|
@ -1,41 +1,45 @@
|
|||
import { CredentialsModal, WorkflowPage } from '../pages';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
const credentialsModal = new CredentialsModal();
|
||||
|
||||
export const getHomeButton = () => cy.getByTestId('project-home-menu-item');
|
||||
export const getMenuItems = () => cy.getByTestId('project-menu-item');
|
||||
export const getAddProjectButton = () => cy.getByTestId('add-project-menu-item');
|
||||
export const getAddProjectButton = () =>
|
||||
cy.getByTestId('add-project-menu-item').should('contain', 'Add project').should('be.visible');
|
||||
export const getProjectTabs = () => cy.getByTestId('project-tabs').find('a');
|
||||
export const getProjectTabWorkflows = () => getProjectTabs().filter('a[href$="/workflows"]');
|
||||
export const getProjectTabCredentials = () => getProjectTabs().filter('a[href$="/credentials"]');
|
||||
export const getProjectTabSettings = () => getProjectTabs().filter('a[href$="/settings"]');
|
||||
export const getProjectSettingsNameInput = () => cy.getByTestId('project-settings-name-input');
|
||||
export const getProjectSettingsNameInput = () =>
|
||||
cy.getByTestId('project-settings-name-input').find('input');
|
||||
export const getProjectSettingsSaveButton = () => cy.getByTestId('project-settings-save-button');
|
||||
export const getProjectSettingsCancelButton = () =>
|
||||
cy.getByTestId('project-settings-cancel-button');
|
||||
export const getProjectSettingsDeleteButton = () =>
|
||||
cy.getByTestId('project-settings-delete-button');
|
||||
export const getProjectMembersSelect = () => cy.getByTestId('project-members-select');
|
||||
export const addProjectMember = (email: string) => {
|
||||
export const addProjectMember = (email: string, role?: string) => {
|
||||
getProjectMembersSelect().click();
|
||||
getProjectMembersSelect().get('.el-select-dropdown__item').contains(email.toLowerCase()).click();
|
||||
|
||||
if (role) {
|
||||
cy.getByTestId(`user-list-item-${email}`)
|
||||
.find('[data-test-id="projects-settings-user-role-select"]')
|
||||
.click();
|
||||
getVisibleSelect().find('li').contains(role).click();
|
||||
}
|
||||
};
|
||||
export const getProjectNameInput = () => cy.get('#projectName').find('input');
|
||||
export const getResourceMoveModal = () => cy.getByTestId('project-move-resource-modal');
|
||||
export const getResourceMoveConfirmModal = () =>
|
||||
cy.getByTestId('project-move-resource-confirm-modal');
|
||||
export const getProjectMoveSelect = () => cy.getByTestId('project-move-resource-modal-select');
|
||||
|
||||
export function createProject(name: string) {
|
||||
getAddProjectButton().should('be.visible').click();
|
||||
getAddProjectButton().click();
|
||||
|
||||
getProjectNameInput()
|
||||
.should('be.visible')
|
||||
.should('be.focused')
|
||||
.should('have.value', 'My project')
|
||||
.clear()
|
||||
.type(name);
|
||||
getProjectSettingsNameInput().should('be.visible').clear().type(name);
|
||||
getProjectSettingsSaveButton().click();
|
||||
}
|
||||
|
||||
|
@ -46,7 +50,7 @@ export function createWorkflow(fixtureKey: string, name: string) {
|
|||
workflowPage.actions.zoomToFit();
|
||||
}
|
||||
|
||||
export function createCredential(name: string) {
|
||||
export function createCredential(name: string, closeModal = true) {
|
||||
credentialsModal.getters.newCredentialModal().should('be.visible');
|
||||
credentialsModal.getters.newCredentialTypeSelect().should('be.visible');
|
||||
credentialsModal.getters.newCredentialTypeOption('Notion API').click();
|
||||
|
@ -54,13 +58,8 @@ export function createCredential(name: string) {
|
|||
credentialsModal.getters.connectionParameter('Internal Integration Secret').type('1234567890');
|
||||
credentialsModal.actions.setName(name);
|
||||
credentialsModal.actions.save();
|
||||
credentialsModal.actions.close();
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
createProject: (name: string) => {
|
||||
getAddProjectButton().click();
|
||||
getProjectSettingsNameInput().type(name);
|
||||
getProjectSettingsSaveButton().click();
|
||||
},
|
||||
};
|
||||
if (closeModal) {
|
||||
credentialsModal.actions.close();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ export const INSTANCE_MEMBERS = [
|
|||
export const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger';
|
||||
export const MANUAL_TRIGGER_NODE_DISPLAY_NAME = 'When clicking ‘Test workflow’';
|
||||
export const MANUAL_CHAT_TRIGGER_NODE_NAME = 'Chat Trigger';
|
||||
export const CHAT_TRIGGER_NODE_DISPLAY_NAME = 'When chat message received';
|
||||
export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger';
|
||||
export const CODE_NODE_NAME = 'Code';
|
||||
export const SET_NODE_NAME = 'Set';
|
||||
|
@ -57,6 +58,7 @@ export const AI_TOOL_CODE_NODE_NAME = 'Code Tool';
|
|||
export const AI_TOOL_WIKIPEDIA_NODE_NAME = 'Wikipedia';
|
||||
export const AI_TOOL_HTTP_NODE_NAME = 'HTTP Request Tool';
|
||||
export const AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME = 'OpenAI Chat Model';
|
||||
export const AI_MEMORY_POSTGRES_NODE_NAME = 'Postgres Chat Memory';
|
||||
export const AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME = 'Auto-fixing Output Parser';
|
||||
export const WEBHOOK_NODE_NAME = 'Webhook';
|
||||
|
||||
|
|
|
@ -11,6 +11,21 @@ describe('Inline expression editor', () => {
|
|||
cy.on('uncaught:exception', (error) => error.name !== 'ExpressionError');
|
||||
});
|
||||
|
||||
describe('Basic UI functionality', () => {
|
||||
it('should open and close inline expression preview', () => {
|
||||
WorkflowPage.actions.zoomToFit();
|
||||
WorkflowPage.actions.openNode('Schedule');
|
||||
WorkflowPage.actions.openInlineExpressionEditor();
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('123');
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^123$/);
|
||||
// click outside to close
|
||||
ndv.getters.outputPanel().click();
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Static data', () => {
|
||||
beforeEach(() => {
|
||||
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
||||
|
|
|
@ -145,7 +145,16 @@ describe('Canvas Actions', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should delete connections by pressing the delete button', () => {
|
||||
it('should delete node by pressing keyboard backspace', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.getters.canvasNodeByName(CODE_NODE_NAME).click();
|
||||
cy.get('body').type('{backspace}');
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||
});
|
||||
|
||||
it('should delete connections by clicking on the delete button', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
|
|
|
@ -40,11 +40,13 @@ describe('Data mapping', () => {
|
|||
|
||||
ndv.actions.mapDataFromHeader(1, 'value');
|
||||
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.timestamp }}');
|
||||
ndv.getters.inlineExpressionEditorInput().type('{esc}');
|
||||
ndv.getters.parameterExpressionPreview('value').should('include.text', '2024');
|
||||
|
||||
ndv.actions.mapDataFromHeader(2, 'value');
|
||||
ndv.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.should('have.text', "{{ $json.timestamp }} {{ $json['Readable date'] }}");
|
||||
.should('have.text', "{{ $json['Readable date'] }}{{ $json.timestamp }}");
|
||||
});
|
||||
|
||||
it('maps expressions from table json, and resolves value based on hover', () => {
|
||||
|
@ -133,6 +135,7 @@ describe('Data mapping', () => {
|
|||
|
||||
ndv.actions.mapToParameter('value');
|
||||
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }}');
|
||||
ndv.getters.inlineExpressionEditorInput().type('{esc}');
|
||||
ndv.getters.parameterExpressionPreview('value').should('include.text', '0');
|
||||
|
||||
ndv.getters
|
||||
|
@ -145,8 +148,9 @@ describe('Data mapping', () => {
|
|||
ndv.actions.mapToParameter('value');
|
||||
ndv.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.should('have.text', '{{ $json.input[0].count }} {{ $json.input }}');
|
||||
ndv.actions.validateExpressionPreview('value', '0 [object Object]');
|
||||
.should('have.text', '{{ $json.input }}{{ $json.input[0].count }}');
|
||||
ndv.getters.inlineExpressionEditorInput().type('{esc}');
|
||||
ndv.actions.validateExpressionPreview('value', '[object Object]0');
|
||||
});
|
||||
|
||||
it('maps expressions from schema view', () => {
|
||||
|
@ -163,6 +167,7 @@ describe('Data mapping', () => {
|
|||
|
||||
ndv.actions.mapToParameter('value');
|
||||
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }}');
|
||||
ndv.getters.inlineExpressionEditorInput().type('{esc}');
|
||||
ndv.actions.validateExpressionPreview('value', '0');
|
||||
|
||||
ndv.getters.inputDataContainer().find('span').contains('input').realMouseDown();
|
||||
|
@ -170,8 +175,8 @@ describe('Data mapping', () => {
|
|||
ndv.actions.mapToParameter('value');
|
||||
ndv.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.should('have.text', '{{ $json.input[0].count }} {{ $json.input }}');
|
||||
ndv.actions.validateExpressionPreview('value', '0 [object Object]');
|
||||
.should('have.text', '{{ $json.input }}{{ $json.input[0].count }}');
|
||||
ndv.actions.validateExpressionPreview('value', '[object Object]0');
|
||||
});
|
||||
|
||||
it('maps expressions from previous nodes', () => {
|
||||
|
@ -192,6 +197,7 @@ describe('Data mapping', () => {
|
|||
ndv.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.should('have.text', `{{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input[0].count }}`);
|
||||
ndv.getters.inlineExpressionEditorInput().type('{esc}');
|
||||
|
||||
ndv.actions.switchInputMode('Table');
|
||||
ndv.actions.selectInputNode(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
|
@ -200,17 +206,17 @@ describe('Data mapping', () => {
|
|||
.inlineExpressionEditorInput()
|
||||
.should(
|
||||
'have.text',
|
||||
`{{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input[0].count }} {{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input }}`,
|
||||
`{{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input }}{{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input[0].count }}`,
|
||||
);
|
||||
|
||||
ndv.actions.selectInputNode('Set');
|
||||
|
||||
ndv.getters.executingLoader().should('not.exist');
|
||||
ndv.getters.inputDataContainer().should('exist');
|
||||
ndv.actions.validateExpressionPreview('value', '0 [object Object]');
|
||||
ndv.actions.validateExpressionPreview('value', '[object Object]0');
|
||||
|
||||
ndv.getters.inputTbodyCell(2, 0).realHover();
|
||||
ndv.actions.validateExpressionPreview('value', '1 [object Object]');
|
||||
ndv.actions.validateExpressionPreview('value', '[object Object]1');
|
||||
});
|
||||
|
||||
it('maps keys to path', () => {
|
||||
|
@ -271,12 +277,12 @@ describe('Data mapping', () => {
|
|||
|
||||
ndv.actions.typeIntoParameterInput('value', 'fun');
|
||||
ndv.actions.clearParameterInput('value'); // keep focus on param
|
||||
cy.wait(300);
|
||||
|
||||
ndv.getters.inputDataContainer().should('exist').find('span').contains('count').realMouseDown();
|
||||
|
||||
ndv.actions.mapToParameter('value');
|
||||
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }}');
|
||||
ndv.getters.inlineExpressionEditorInput().type('{esc}');
|
||||
ndv.actions.validateExpressionPreview('value', '0');
|
||||
|
||||
ndv.getters.inputDataContainer().find('span').contains('input').realMouseDown();
|
||||
|
@ -284,8 +290,8 @@ describe('Data mapping', () => {
|
|||
ndv.actions.mapToParameter('value');
|
||||
ndv.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.should('have.text', '{{ $json.input[0].count }} {{ $json.input }}');
|
||||
ndv.actions.validateExpressionPreview('value', '0 [object Object]');
|
||||
.should('have.text', '{{ $json.input }}{{ $json.input[0].count }}');
|
||||
ndv.actions.validateExpressionPreview('value', '[object Object]0');
|
||||
});
|
||||
|
||||
it('renders expression preview when a previous node is selected', () => {
|
||||
|
@ -342,4 +348,31 @@ describe('Data mapping', () => {
|
|||
.invoke('css', 'border')
|
||||
.should('include', 'dashed rgb(90, 76, 194)');
|
||||
});
|
||||
|
||||
it('maps expressions to a specific location in the editor', () => {
|
||||
cy.fixture('Test_workflow_3.json').then((data) => {
|
||||
cy.get('body').paste(JSON.stringify(data));
|
||||
});
|
||||
workflowPage.actions.zoomToFit();
|
||||
|
||||
workflowPage.actions.openNode('Set');
|
||||
ndv.actions.typeIntoParameterInput('value', '=');
|
||||
ndv.getters.inlineExpressionEditorInput().find('.cm-content').paste('hello world\n\nnewline');
|
||||
ndv.getters.inlineExpressionEditorInput().type('{esc}');
|
||||
|
||||
ndv.getters.inputDataContainer().should('exist').find('span').contains('count').realMouseDown();
|
||||
ndv.actions.mapToParameter('value');
|
||||
ndv.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.should('have.text', '{{ $json.input[0].count }}hello worldnewline');
|
||||
ndv.getters.inlineExpressionEditorInput().type('{esc}');
|
||||
ndv.actions.validateExpressionPreview('value', '0hello world\n\nnewline');
|
||||
|
||||
ndv.getters.inputDataContainer().find('span').contains('input').realMouseDown();
|
||||
ndv.actions.mapToParameter('value', 'center');
|
||||
|
||||
ndv.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.should('have.text', '{{ $json.input[0].count }}hello world{{ $json.input }}newline');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
WorkflowSharingModal,
|
||||
WorkflowsPage,
|
||||
} from '../pages';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
import { getVisibleDropdown, getVisibleSelect } from '../utils';
|
||||
import * as projects from '../composables/projects';
|
||||
|
||||
/**
|
||||
|
@ -192,11 +192,79 @@ describe('Sharing', { disableAutoLogin: true }, () => {
|
|||
credentialsModal.actions.saveSharing();
|
||||
credentialsModal.actions.close();
|
||||
});
|
||||
|
||||
it('credentials should work between team and personal projects', () => {
|
||||
cy.resetDatabase();
|
||||
cy.enableFeature('sharing');
|
||||
cy.enableFeature('advancedPermissions');
|
||||
cy.enableFeature('projectRole:admin');
|
||||
cy.enableFeature('projectRole:editor');
|
||||
cy.changeQuota('maxTeamProjects', -1);
|
||||
|
||||
cy.signinAsOwner();
|
||||
cy.visit('/');
|
||||
|
||||
projects.createProject('Development');
|
||||
|
||||
projects.getHomeButton().click();
|
||||
workflowsPage.getters.newWorkflowButtonCard().click();
|
||||
projects.createWorkflow('Test_workflow_1.json', 'Test workflow');
|
||||
|
||||
projects.getHomeButton().click();
|
||||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
projects.createCredential('Notion API');
|
||||
|
||||
credentialsPage.getters.credentialCard('Notion API').click();
|
||||
credentialsModal.actions.changeTab('Sharing');
|
||||
credentialsModal.getters.usersSelect().click();
|
||||
getVisibleSelect()
|
||||
.find('li')
|
||||
.should('have.length', 4)
|
||||
.filter(':contains("Development")')
|
||||
.should('have.length', 1)
|
||||
.click();
|
||||
credentialsModal.getters.saveButton().click();
|
||||
credentialsModal.actions.close();
|
||||
|
||||
projects.getProjectTabWorkflows().click();
|
||||
workflowsPage.getters.workflowCardActions('Test workflow').click();
|
||||
getVisibleDropdown().find('li').contains('Share').click();
|
||||
|
||||
workflowSharingModal.getters.usersSelect().filter(':visible').click();
|
||||
getVisibleSelect().find('li').should('have.length', 3).first().click();
|
||||
workflowSharingModal.getters.saveButton().click();
|
||||
|
||||
projects.getMenuItems().first().click();
|
||||
workflowsPage.getters.newWorkflowButtonCard().click();
|
||||
projects.createWorkflow('Test_workflow_1.json', 'Test workflow 2');
|
||||
workflowPage.actions.openShareModal();
|
||||
workflowSharingModal.getters.usersSelect().should('not.exist');
|
||||
|
||||
cy.get('body').type('{esc}');
|
||||
|
||||
projects.getMenuItems().first().click();
|
||||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.createCredentialButton().click();
|
||||
projects.createCredential('Notion API 2', false);
|
||||
credentialsModal.actions.changeTab('Sharing');
|
||||
credentialsModal.getters.usersSelect().click();
|
||||
getVisibleSelect().find('li').should('have.length', 4).first().click();
|
||||
credentialsModal.getters.saveButton().click();
|
||||
credentialsModal.actions.close();
|
||||
|
||||
credentialsPage.getters
|
||||
.credentialCards()
|
||||
.should('have.length', 2)
|
||||
.filter(':contains("Owned by me")')
|
||||
.should('have.length', 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Credential Usage in Cross Shared Workflows', () => {
|
||||
beforeEach(() => {
|
||||
cy.resetDatabase();
|
||||
cy.enableFeature('sharing');
|
||||
cy.enableFeature('advancedPermissions');
|
||||
cy.enableFeature('projectRole:admin');
|
||||
cy.enableFeature('projectRole:editor');
|
||||
|
@ -207,23 +275,18 @@ describe('Credential Usage in Cross Shared Workflows', () => {
|
|||
});
|
||||
|
||||
it('should only show credentials from the same team project', () => {
|
||||
cy.enableFeature('advancedPermissions');
|
||||
cy.enableFeature('projectRole:admin');
|
||||
cy.enableFeature('projectRole:editor');
|
||||
cy.changeQuota('maxTeamProjects', -1);
|
||||
|
||||
// Create a notion credential in the home project
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
|
||||
// Create a notion credential in one project
|
||||
projects.actions.createProject('Development');
|
||||
projects.createProject('Development');
|
||||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
|
||||
// Create a notion credential in another project
|
||||
projects.actions.createProject('Test');
|
||||
projects.createProject('Test');
|
||||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
|
@ -238,10 +301,36 @@ describe('Credential Usage in Cross Shared Workflows', () => {
|
|||
getVisibleSelect().find('li').should('have.length', 2);
|
||||
});
|
||||
|
||||
it('should only show credentials in their personal project for members', () => {
|
||||
// Create a notion credential as the owner
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
|
||||
// Create another notion credential as the owner, but share it with member
|
||||
// 0
|
||||
credentialsPage.getters.createCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API', false);
|
||||
credentialsModal.actions.changeTab('Sharing');
|
||||
credentialsModal.actions.addUser(INSTANCE_MEMBERS[0].email);
|
||||
credentialsModal.actions.saveSharing();
|
||||
|
||||
// As the member, create a new notion credential and a workflow
|
||||
cy.signinAsMember();
|
||||
cy.visit(credentialsPage.url);
|
||||
credentialsPage.getters.createCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
cy.visit(workflowsPage.url);
|
||||
workflowsPage.actions.createWorkflowFromCard();
|
||||
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
|
||||
|
||||
// Only the own credential the shared one (+ the 'Create new' option)
|
||||
// should be in the dropdown
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').should('have.length', 3);
|
||||
});
|
||||
|
||||
it('should only show credentials in their personal project for members if the workflow was shared with them', () => {
|
||||
const workflowName = 'Test workflow';
|
||||
cy.enableFeature('sharing');
|
||||
cy.reload();
|
||||
|
||||
// Create a notion credential as the owner and a workflow that is shared
|
||||
// with member 0
|
||||
|
@ -272,7 +361,6 @@ describe('Credential Usage in Cross Shared Workflows', () => {
|
|||
|
||||
it("should show all credentials from all personal projects the workflow's been shared into for the global owner", () => {
|
||||
const workflowName = 'Test workflow';
|
||||
cy.enableFeature('sharing');
|
||||
|
||||
// As member 1, create a new notion credential. This should not show up.
|
||||
cy.signinAsMember(1);
|
||||
|
@ -317,8 +405,6 @@ describe('Credential Usage in Cross Shared Workflows', () => {
|
|||
});
|
||||
|
||||
it('should show all personal credentials if the global owner owns the workflow', () => {
|
||||
cy.enableFeature('sharing');
|
||||
|
||||
// As member 0, create a new notion credential.
|
||||
cy.signinAsMember();
|
||||
cy.visit(credentialsPage.url);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { WorkflowPage } from '../pages';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
|
||||
const wf = new WorkflowPage();
|
||||
|
||||
|
@ -64,10 +65,26 @@ describe('Workflow tags', () => {
|
|||
it('should detach a tag inline by clicking on dropdown list item', () => {
|
||||
wf.getters.createTagButton().click();
|
||||
wf.actions.addTags(TEST_TAGS);
|
||||
wf.getters.nthTagPill(1).click();
|
||||
wf.getters.workflowTagsContainer().click();
|
||||
wf.getters.tagsInDropdown().filter('.selected').first().click();
|
||||
cy.get('body').click(0, 0);
|
||||
wf.getters.workflowTags().click();
|
||||
wf.getters.tagPills().should('have.length', TEST_TAGS.length - 1);
|
||||
});
|
||||
|
||||
it('should not show non existing tag as a selectable option', () => {
|
||||
const NON_EXISTING_TAG = 'My Test Tag';
|
||||
|
||||
wf.getters.createTagButton().click();
|
||||
wf.actions.addTags(TEST_TAGS);
|
||||
cy.get('body').click(0, 0);
|
||||
wf.getters.workflowTags().click();
|
||||
wf.getters.workflowTagsInput().type(NON_EXISTING_TAG);
|
||||
|
||||
getVisibleSelect()
|
||||
.find('li')
|
||||
.should('have.length', 2)
|
||||
.filter(`:contains("${NON_EXISTING_TAG}")`)
|
||||
.should('not.have.length');
|
||||
});
|
||||
});
|
||||
|
|
21
cypress/e2e/1858-PAY-can-use-context-menu.ts
Normal file
21
cypress/e2e/1858-PAY-can-use-context-menu.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
|
||||
const WorkflowPage = new WorkflowPageClass();
|
||||
|
||||
describe('PAY-1858 context menu', () => {
|
||||
it('can use context menu on saved workflow', () => {
|
||||
WorkflowPage.actions.visit();
|
||||
cy.createFixtureWorkflow('Test_workflow_filter.json', 'test');
|
||||
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 5);
|
||||
WorkflowPage.actions.deleteNodeFromContextMenu('Then');
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 4);
|
||||
|
||||
WorkflowPage.actions.hitSaveWorkflow();
|
||||
|
||||
cy.reload();
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 4);
|
||||
WorkflowPage.actions.deleteNodeFromContextMenu('Code');
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 3);
|
||||
});
|
||||
});
|
|
@ -275,7 +275,6 @@ describe('Execution', () => {
|
|||
.within(() => cy.get('.fa-check').should('not.exist'));
|
||||
|
||||
successToast().should('be.visible');
|
||||
clearNotifications();
|
||||
|
||||
// Clear execution data
|
||||
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
||||
|
|
|
@ -112,13 +112,13 @@ describe('Credentials', () => {
|
|||
workflowPage.getters.nodeCredentialsSelect().should('have.length', 2);
|
||||
|
||||
workflowPage.getters.nodeCredentialsSelect().first().click();
|
||||
getVisibleSelect().find('li').last().click();
|
||||
getVisibleSelect().find('li').contains('Create New Credential').click();
|
||||
// This one should show auth type selector
|
||||
credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2);
|
||||
cy.get('body').type('{esc}');
|
||||
|
||||
workflowPage.getters.nodeCredentialsSelect().last().click();
|
||||
getVisibleSelect().find('li').last().click();
|
||||
getVisibleSelect().find('li').contains('Create New Credential').click();
|
||||
// This one should not show auth type selector
|
||||
credentialsModal.getters.credentialsAuthTypeSelector().should('not.exist');
|
||||
});
|
||||
|
|
|
@ -183,6 +183,50 @@ describe('Current Workflow Executions', () => {
|
|||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
});
|
||||
|
||||
it('should load items and auto scroll after filter change', () => {
|
||||
createMockExecutions();
|
||||
createMockExecutions();
|
||||
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
|
||||
cy.wait(['@getExecutions']);
|
||||
|
||||
executionsTab.getters.executionsList().scrollTo(0, 500).wait(0);
|
||||
|
||||
executionsTab.getters.executionListItems().eq(10).click();
|
||||
|
||||
cy.getByTestId('executions-filter-button').click();
|
||||
cy.getByTestId('executions-filter-status-select').should('be.visible').click();
|
||||
getVisibleSelect().find('li:contains("Error")').click();
|
||||
|
||||
executionsTab.getters.executionListItems().should('have.length', 5);
|
||||
executionsTab.getters.successfulExecutionListItems().should('have.length', 1);
|
||||
executionsTab.getters.failedExecutionListItems().should('have.length', 4);
|
||||
|
||||
cy.getByTestId('executions-filter-button').click();
|
||||
cy.getByTestId('executions-filter-status-select').should('be.visible').click();
|
||||
getVisibleSelect().find('li:contains("Success")').click();
|
||||
|
||||
// check if the list is scrolled
|
||||
executionsTab.getters.executionListItems().eq(10).should('be.visible');
|
||||
executionsTab.getters.executionsList().then(($el) => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = $el[0];
|
||||
expect(scrollTop).to.be.greaterThan(0);
|
||||
expect(scrollTop + clientHeight).to.be.lessThan(scrollHeight);
|
||||
|
||||
// scroll to the bottom
|
||||
$el[0].scrollTo(0, scrollHeight);
|
||||
executionsTab.getters.executionListItems().should('have.length', 18);
|
||||
executionsTab.getters.successfulExecutionListItems().should('have.length', 18);
|
||||
executionsTab.getters.failedExecutionListItems().should('have.length', 0);
|
||||
});
|
||||
|
||||
cy.getByTestId('executions-filter-button').click();
|
||||
cy.getByTestId('executions-filter-reset-button').should('be.visible').click();
|
||||
executionsTab.getters.executionListItems().eq(11).should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
const createMockExecutions = () => {
|
||||
|
|
279
cypress/e2e/233-AI-switch-to-logs-on-error.cy.ts
Normal file
279
cypress/e2e/233-AI-switch-to-logs-on-error.cy.ts
Normal file
|
@ -0,0 +1,279 @@
|
|||
import type { ExecutionError } from 'n8n-workflow/src';
|
||||
import { NDV, WorkflowPage as WorkflowPageClass } from '../pages';
|
||||
import {
|
||||
addLanguageModelNodeToParent,
|
||||
addMemoryNodeToParent,
|
||||
addNodeToCanvas,
|
||||
addToolNodeToParent,
|
||||
navigateToNewWorkflowPage,
|
||||
openNode,
|
||||
} from '../composables/workflow';
|
||||
import {
|
||||
AGENT_NODE_NAME,
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
AI_MEMORY_POSTGRES_NODE_NAME,
|
||||
AI_TOOL_CALCULATOR_NODE_NAME,
|
||||
CHAT_TRIGGER_NODE_DISPLAY_NAME,
|
||||
MANUAL_CHAT_TRIGGER_NODE_NAME,
|
||||
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
|
||||
MANUAL_TRIGGER_NODE_NAME,
|
||||
} from '../constants';
|
||||
import {
|
||||
clickCreateNewCredential,
|
||||
clickExecuteNode,
|
||||
clickGetBackToCanvas,
|
||||
} from '../composables/ndv';
|
||||
import { setCredentialValues } from '../composables/modals/credential-modal';
|
||||
import {
|
||||
closeManualChatModal,
|
||||
getManualChatMessages,
|
||||
getManualChatModalLogs,
|
||||
getManualChatModalLogsEntries,
|
||||
sendManualChatMessage,
|
||||
} from '../composables/modals/chat-modal';
|
||||
import { createMockNodeExecutionData, getVisibleSelect, runMockWorkflowExecution } from '../utils';
|
||||
|
||||
const ndv = new NDV();
|
||||
const WorkflowPage = new WorkflowPageClass();
|
||||
|
||||
function createRunDataWithError(inputMessage: string) {
|
||||
return [
|
||||
createMockNodeExecutionData(MANUAL_CHAT_TRIGGER_NODE_NAME, {
|
||||
jsonData: {
|
||||
main: { input: inputMessage },
|
||||
},
|
||||
}),
|
||||
createMockNodeExecutionData(AI_MEMORY_POSTGRES_NODE_NAME, {
|
||||
jsonData: {
|
||||
ai_memory: {
|
||||
json: {
|
||||
action: 'loadMemoryVariables',
|
||||
values: {
|
||||
input: inputMessage,
|
||||
system_message: 'You are a helpful assistant',
|
||||
formatting_instructions:
|
||||
'IMPORTANT: Always call `format_final_response` to format your final response!',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
inputOverride: {
|
||||
ai_memory: [
|
||||
[
|
||||
{
|
||||
json: {
|
||||
action: 'loadMemoryVariables',
|
||||
values: {
|
||||
input: inputMessage,
|
||||
system_message: 'You are a helpful assistant',
|
||||
formatting_instructions:
|
||||
'IMPORTANT: Always call `format_final_response` to format your final response!',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
error: {
|
||||
message: 'Internal error',
|
||||
timestamp: 1722591723244,
|
||||
name: 'NodeOperationError',
|
||||
description: 'Internal error',
|
||||
context: {},
|
||||
cause: {
|
||||
name: 'error',
|
||||
severity: 'FATAL',
|
||||
code: '3D000',
|
||||
file: 'postinit.c',
|
||||
line: '885',
|
||||
routine: 'InitPostgres',
|
||||
} as unknown as Error,
|
||||
} as ExecutionError,
|
||||
}),
|
||||
createMockNodeExecutionData(AGENT_NODE_NAME, {
|
||||
executionStatus: 'error',
|
||||
error: {
|
||||
level: 'error',
|
||||
tags: {
|
||||
packageName: 'workflow',
|
||||
},
|
||||
context: {},
|
||||
functionality: 'configuration-node',
|
||||
name: 'NodeOperationError',
|
||||
timestamp: 1722591723244,
|
||||
node: {
|
||||
parameters: {
|
||||
notice: '',
|
||||
sessionIdType: 'fromInput',
|
||||
tableName: 'n8n_chat_histories',
|
||||
},
|
||||
id: '6b9141da-0135-4e9d-94d1-2d658cbf48b5',
|
||||
name: 'Postgres Chat Memory',
|
||||
type: '@n8n/n8n-nodes-langchain.memoryPostgresChat',
|
||||
typeVersion: 1,
|
||||
position: [1140, 500],
|
||||
credentials: {
|
||||
postgres: {
|
||||
id: 'RkyZetVpGsSfEAhQ',
|
||||
name: 'Postgres account',
|
||||
},
|
||||
},
|
||||
},
|
||||
messages: ['database "chat11" does not exist'],
|
||||
description: 'Internal error',
|
||||
message: 'Internal error',
|
||||
} as unknown as ExecutionError,
|
||||
metadata: {
|
||||
subRun: [
|
||||
{
|
||||
node: 'Postgres Chat Memory',
|
||||
runIndex: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
function setupTestWorkflow(chatTrigger: boolean = false) {
|
||||
// Setup test workflow with AI Agent, Postgres Memory Node (source of error), Calculator Tool, and OpenAI Chat Model
|
||||
if (chatTrigger) {
|
||||
addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true);
|
||||
} else {
|
||||
addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME, true);
|
||||
}
|
||||
|
||||
addNodeToCanvas(AGENT_NODE_NAME, true);
|
||||
|
||||
if (!chatTrigger) {
|
||||
// Remove chat trigger
|
||||
WorkflowPage.getters
|
||||
.canvasNodeByName(CHAT_TRIGGER_NODE_DISPLAY_NAME)
|
||||
.find('[data-test-id="delete-node-button"]')
|
||||
.click({ force: true });
|
||||
|
||||
// Set manual trigger to output standard pinned data
|
||||
openNode(MANUAL_TRIGGER_NODE_DISPLAY_NAME);
|
||||
ndv.actions.editPinnedData();
|
||||
ndv.actions.savePinnedData();
|
||||
ndv.actions.close();
|
||||
}
|
||||
|
||||
// Calculator is added just to make OpenAI Chat Model work (tools can not be empty with OpenAI model)
|
||||
addToolNodeToParent(AI_TOOL_CALCULATOR_NODE_NAME, AGENT_NODE_NAME);
|
||||
clickGetBackToCanvas();
|
||||
|
||||
addMemoryNodeToParent(AI_MEMORY_POSTGRES_NODE_NAME, AGENT_NODE_NAME);
|
||||
|
||||
clickCreateNewCredential();
|
||||
setCredentialValues({
|
||||
password: 'testtesttest',
|
||||
});
|
||||
|
||||
ndv.getters.parameterInput('sessionIdType').click();
|
||||
getVisibleSelect().contains('Define below').click();
|
||||
ndv.getters.parameterInput('sessionKey').type('asdasd');
|
||||
|
||||
clickGetBackToCanvas();
|
||||
|
||||
addLanguageModelNodeToParent(
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
AGENT_NODE_NAME,
|
||||
true,
|
||||
);
|
||||
|
||||
clickCreateNewCredential();
|
||||
setCredentialValues({
|
||||
apiKey: 'sk_test_123',
|
||||
});
|
||||
clickGetBackToCanvas();
|
||||
|
||||
WorkflowPage.actions.zoomToFit();
|
||||
}
|
||||
|
||||
function checkMessages(inputMessage: string, outputMessage: string) {
|
||||
const messages = getManualChatMessages();
|
||||
messages.should('have.length', 2);
|
||||
messages.should('contain', inputMessage);
|
||||
messages.should('contain', outputMessage);
|
||||
|
||||
getManualChatModalLogs().should('exist');
|
||||
getManualChatModalLogsEntries()
|
||||
.should('have.length', 1)
|
||||
.should('contain', AI_MEMORY_POSTGRES_NODE_NAME);
|
||||
}
|
||||
|
||||
describe("AI-233 Make root node's logs pane active in case of an error in sub-nodes", () => {
|
||||
beforeEach(() => {
|
||||
navigateToNewWorkflowPage();
|
||||
});
|
||||
|
||||
it('should open logs tab by default when there was an error', () => {
|
||||
setupTestWorkflow(true);
|
||||
|
||||
openNode(AGENT_NODE_NAME);
|
||||
|
||||
const inputMessage = 'Test the code tool';
|
||||
|
||||
clickExecuteNode();
|
||||
runMockWorkflowExecution({
|
||||
trigger: () => sendManualChatMessage(inputMessage),
|
||||
runData: createRunDataWithError(inputMessage),
|
||||
lastNodeExecuted: AGENT_NODE_NAME,
|
||||
});
|
||||
|
||||
checkMessages(inputMessage, '[ERROR: Internal error]');
|
||||
closeManualChatModal();
|
||||
|
||||
// Open the AI Agent node to see the logs
|
||||
openNode(AGENT_NODE_NAME);
|
||||
|
||||
// Finally check that logs pane is opened by default
|
||||
ndv.getters.outputDataContainer().should('be.visible');
|
||||
|
||||
ndv.getters.aiOutputModeToggle().should('be.visible');
|
||||
ndv.getters
|
||||
.aiOutputModeToggle()
|
||||
.find('[role="radio"]')
|
||||
.should('have.length', 2)
|
||||
.eq(1)
|
||||
.should('have.attr', 'aria-checked', 'true');
|
||||
|
||||
ndv.getters
|
||||
.outputPanel()
|
||||
.findChildByTestId('node-error-message')
|
||||
.should('be.visible')
|
||||
.should('contain', 'Error in sub-node');
|
||||
});
|
||||
|
||||
it('should switch to logs tab on error, when NDV is already opened', () => {
|
||||
setupTestWorkflow(false);
|
||||
|
||||
openNode(AGENT_NODE_NAME);
|
||||
|
||||
const inputMessage = 'Test the code tool';
|
||||
|
||||
runMockWorkflowExecution({
|
||||
trigger: () => clickExecuteNode(),
|
||||
runData: createRunDataWithError(inputMessage),
|
||||
lastNodeExecuted: AGENT_NODE_NAME,
|
||||
});
|
||||
|
||||
// Check that logs pane is opened by default
|
||||
ndv.getters.outputDataContainer().should('be.visible');
|
||||
|
||||
ndv.getters.aiOutputModeToggle().should('be.visible');
|
||||
ndv.getters
|
||||
.aiOutputModeToggle()
|
||||
.find('[role="radio"]')
|
||||
.should('have.length', 2)
|
||||
.eq(1)
|
||||
.should('have.attr', 'aria-checked', 'true');
|
||||
|
||||
ndv.getters
|
||||
.outputPanel()
|
||||
.findChildByTestId('node-error-message')
|
||||
.should('be.visible')
|
||||
.should('contain', 'Error in sub-node');
|
||||
});
|
||||
});
|
|
@ -10,7 +10,9 @@ import {
|
|||
disableNode,
|
||||
getExecuteWorkflowButton,
|
||||
navigateToNewWorkflowPage,
|
||||
getNodes,
|
||||
openNode,
|
||||
getConnectionBySourceAndTarget,
|
||||
} from '../composables/workflow';
|
||||
import {
|
||||
clickCreateNewCredential,
|
||||
|
@ -41,6 +43,7 @@ import {
|
|||
AI_TOOL_WIKIPEDIA_NODE_NAME,
|
||||
BASIC_LLM_CHAIN_NODE_NAME,
|
||||
EDIT_FIELDS_SET_NODE_NAME,
|
||||
CHAT_TRIGGER_NODE_DISPLAY_NAME,
|
||||
} from './../constants';
|
||||
|
||||
describe('Langchain Integration', () => {
|
||||
|
@ -331,4 +334,27 @@ describe('Langchain Integration', () => {
|
|||
|
||||
closeManualChatModal();
|
||||
});
|
||||
|
||||
it('should auto-add chat trigger and basic LLM chain when adding LLM node', () => {
|
||||
addNodeToCanvas(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, true);
|
||||
|
||||
getConnectionBySourceAndTarget(
|
||||
CHAT_TRIGGER_NODE_DISPLAY_NAME,
|
||||
BASIC_LLM_CHAIN_NODE_NAME,
|
||||
).should('exist');
|
||||
|
||||
getConnectionBySourceAndTarget(
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
BASIC_LLM_CHAIN_NODE_NAME,
|
||||
).should('exist');
|
||||
getNodes().should('have.length', 3);
|
||||
});
|
||||
|
||||
it('should not auto-add nodes if AI nodes are already present', () => {
|
||||
addNodeToCanvas(AGENT_NODE_NAME, true);
|
||||
|
||||
addNodeToCanvas(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, true);
|
||||
getConnectionBySourceAndTarget(CHAT_TRIGGER_NODE_DISPLAY_NAME, AGENT_NODE_NAME).should('exist');
|
||||
getNodes().should('have.length', 3);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -35,13 +35,14 @@ describe('Personal Settings', () => {
|
|||
successToast().find('.el-notification__closeBtn').click();
|
||||
});
|
||||
});
|
||||
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
|
||||
it('not allow malicious values for personal data', () => {
|
||||
cy.visit('/settings/personal');
|
||||
INVALID_NAMES.forEach((name) => {
|
||||
cy.getByTestId('personal-data-form').find('input[name="firstName"]').clear().type(name);
|
||||
cy.getByTestId('personal-data-form').find('input[name="lastName"]').clear().type(name);
|
||||
cy.getByTestId('save-settings-button').click();
|
||||
errorToast().should('contain', 'Malicious firstName | Malicious lastName');
|
||||
errorToast().should('contain', 'Potentially malicious string | Potentially malicious string');
|
||||
errorToast().find('.el-notification__closeBtn').click();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,9 +1,4 @@
|
|||
import {
|
||||
INSTANCE_MEMBERS,
|
||||
INSTANCE_OWNER,
|
||||
MANUAL_TRIGGER_NODE_NAME,
|
||||
NOTION_NODE_NAME,
|
||||
} from '../constants';
|
||||
import { INSTANCE_MEMBERS, MANUAL_TRIGGER_NODE_NAME, NOTION_NODE_NAME } from '../constants';
|
||||
import {
|
||||
WorkflowsPage,
|
||||
WorkflowPage,
|
||||
|
@ -11,9 +6,10 @@ import {
|
|||
CredentialsPage,
|
||||
WorkflowExecutionsTab,
|
||||
NDV,
|
||||
MainSidebar,
|
||||
} from '../pages';
|
||||
import * as projects from '../composables/projects';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
import { getVisibleDropdown, getVisibleModalOverlay, getVisibleSelect } from '../utils';
|
||||
|
||||
const workflowsPage = new WorkflowsPage();
|
||||
const workflowPage = new WorkflowPage();
|
||||
|
@ -21,6 +17,7 @@ const credentialsPage = new CredentialsPage();
|
|||
const credentialsModal = new CredentialsModal();
|
||||
const executionsTab = new WorkflowExecutionsTab();
|
||||
const ndv = new NDV();
|
||||
const mainSidebar = new MainSidebar();
|
||||
|
||||
describe('Projects', { disableAutoLogin: true }, () => {
|
||||
before(() => {
|
||||
|
@ -237,10 +234,30 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
cy.signinAsMember(1);
|
||||
cy.visit(workflowsPage.url);
|
||||
|
||||
projects.getAddProjectButton().should('not.exist');
|
||||
cy.getByTestId('add-project-menu-item').should('not.exist');
|
||||
projects.getMenuItems().should('not.exist');
|
||||
});
|
||||
|
||||
it('should not show viewer role if not licensed', () => {
|
||||
cy.signinAsOwner();
|
||||
cy.visit(workflowsPage.url);
|
||||
|
||||
projects.getMenuItems().first().click();
|
||||
projects.getProjectTabSettings().click();
|
||||
|
||||
cy.get(
|
||||
`[data-test-id="user-list-item-${INSTANCE_MEMBERS[0].email}"] [data-test-id="projects-settings-user-role-select"]`,
|
||||
).click();
|
||||
|
||||
cy.get('.el-select-dropdown__item.is-disabled')
|
||||
.should('contain.text', 'Viewer')
|
||||
.get('span:contains("Upgrade")')
|
||||
.filter(':visible')
|
||||
.click();
|
||||
|
||||
getVisibleModalOverlay().should('contain.text', 'Upgrade to unlock additional roles');
|
||||
});
|
||||
|
||||
describe('when starting from scratch', () => {
|
||||
beforeEach(() => {
|
||||
cy.resetDatabase();
|
||||
|
@ -257,7 +274,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
|
||||
// Create a project and add a credential to it
|
||||
cy.intercept('POST', '/rest/projects').as('projectCreate');
|
||||
projects.getAddProjectButton().should('contain', 'Add project').should('be.visible').click();
|
||||
projects.getAddProjectButton().click();
|
||||
cy.wait('@projectCreate');
|
||||
projects.getMenuItems().should('have.length', 1);
|
||||
projects.getMenuItems().first().click();
|
||||
|
@ -418,7 +435,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
});
|
||||
|
||||
it('should move resources between projects', () => {
|
||||
cy.signin(INSTANCE_OWNER);
|
||||
cy.signinAsOwner();
|
||||
cy.visit(workflowsPage.url);
|
||||
|
||||
// Create a workflow and a credential in the Home project
|
||||
|
@ -563,5 +580,80 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.credentialCards().should('have.length', 2);
|
||||
});
|
||||
|
||||
it('should handle viewer role', () => {
|
||||
cy.enableFeature('projectRole:viewer');
|
||||
cy.signinAsOwner();
|
||||
cy.visit(workflowsPage.url);
|
||||
|
||||
projects.createProject('Development');
|
||||
projects.addProjectMember(INSTANCE_MEMBERS[0].email, 'Viewer');
|
||||
projects.getProjectSettingsSaveButton().click();
|
||||
|
||||
projects.getProjectTabWorkflows().click();
|
||||
workflowsPage.getters.newWorkflowButtonCard().click();
|
||||
projects.createWorkflow('Test_workflow_4_executions_view.json', 'WF with random error');
|
||||
executionsTab.actions.createManualExecutions(2);
|
||||
executionsTab.actions.toggleNodeEnabled('Error');
|
||||
executionsTab.actions.createManualExecutions(2);
|
||||
workflowPage.actions.saveWorkflowUsingKeyboardShortcut();
|
||||
|
||||
projects.getMenuItems().first().click();
|
||||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
projects.createCredential('Notion API');
|
||||
|
||||
mainSidebar.actions.openUserMenu();
|
||||
cy.getByTestId('user-menu-item-logout').click();
|
||||
|
||||
cy.get('input[name="email"]').type(INSTANCE_MEMBERS[0].email);
|
||||
cy.get('input[name="password"]').type(INSTANCE_MEMBERS[0].password);
|
||||
cy.getByTestId('form-submit-button').click();
|
||||
|
||||
mainSidebar.getters.executions().click();
|
||||
cy.getByTestId('global-execution-list-item').first().find('td:last button').click();
|
||||
getVisibleDropdown()
|
||||
.find('li')
|
||||
.filter(':contains("Retry")')
|
||||
.should('have.class', 'is-disabled');
|
||||
getVisibleDropdown()
|
||||
.find('li')
|
||||
.filter(':contains("Delete")')
|
||||
.should('have.class', 'is-disabled');
|
||||
|
||||
projects.getMenuItems().first().click();
|
||||
cy.getByTestId('workflow-card-name').should('be.visible').first().click();
|
||||
workflowPage.getters.nodeViewRoot().should('be.visible');
|
||||
workflowPage.getters.executeWorkflowButton().should('not.exist');
|
||||
workflowPage.getters.nodeCreatorPlusButton().should('not.exist');
|
||||
workflowPage.getters.canvasNodes().should('have.length', 3).last().click();
|
||||
cy.get('body').type('{backspace}');
|
||||
workflowPage.getters.canvasNodes().should('have.length', 3).last().rightclick();
|
||||
getVisibleDropdown()
|
||||
.find('li')
|
||||
.should('be.visible')
|
||||
.filter(
|
||||
':contains("Open"), :contains("Copy"), :contains("Select all"), :contains("Clear selection")',
|
||||
)
|
||||
.should('not.have.class', 'is-disabled');
|
||||
cy.get('body').type('{esc}');
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
cy.getByTestId('retry-execution-button')
|
||||
.should('be.visible')
|
||||
.find('.is-disabled')
|
||||
.should('exist');
|
||||
cy.get('button:contains("Debug")').should('be.disabled');
|
||||
cy.get('button[title="Retry execution"]').should('be.disabled');
|
||||
cy.get('button[title="Delete this execution"]').should('be.disabled');
|
||||
|
||||
projects.getMenuItems().first().click();
|
||||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.credentialCards().filter(':contains("Notion")').click();
|
||||
cy.getByTestId('node-credentials-config-container')
|
||||
.should('be.visible')
|
||||
.find('input')
|
||||
.should('not.have.length');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
26
cypress/e2e/44-routing.cy.ts
Normal file
26
cypress/e2e/44-routing.cy.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
import { EDIT_FIELDS_SET_NODE_NAME } from '../constants';
|
||||
import { getSaveChangesModal } from '../composables/modals/save-changes-modal';
|
||||
|
||||
const WorkflowsPage = new WorkflowsPageClass();
|
||||
const WorkflowPage = new WorkflowPageClass();
|
||||
|
||||
describe('Workflows', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit(WorkflowsPage.url);
|
||||
});
|
||||
|
||||
it('should ask to save unsaved changes before leaving route', () => {
|
||||
WorkflowsPage.getters.newWorkflowButtonCard().should('be.visible');
|
||||
WorkflowsPage.getters.newWorkflowButtonCard().click();
|
||||
|
||||
cy.createFixtureWorkflow('Test_workflow_1.json', 'Empty State Card Workflow');
|
||||
|
||||
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
|
||||
|
||||
cy.getByTestId('project-home-menu-item').click();
|
||||
|
||||
getSaveChangesModal().should('be.visible');
|
||||
});
|
||||
});
|
247
cypress/e2e/45-ai-assistant.cy.ts
Normal file
247
cypress/e2e/45-ai-assistant.cy.ts
Normal file
|
@ -0,0 +1,247 @@
|
|||
import { NDV, WorkflowPage } from '../pages';
|
||||
import { AIAssistant } from '../pages/features/ai-assistant';
|
||||
|
||||
const wf = new WorkflowPage();
|
||||
const ndv = new NDV();
|
||||
const aiAssistant = new AIAssistant();
|
||||
|
||||
describe('AI Assistant::disabled', () => {
|
||||
beforeEach(() => {
|
||||
aiAssistant.actions.disableAssistant();
|
||||
wf.actions.visit();
|
||||
});
|
||||
|
||||
it('does not show assistant button if feature is disabled', () => {
|
||||
aiAssistant.getters.askAssistantFloatingButton().should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AI Assistant::enabled', () => {
|
||||
beforeEach(() => {
|
||||
aiAssistant.actions.enableAssistant();
|
||||
wf.actions.visit();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
aiAssistant.actions.disableAssistant();
|
||||
});
|
||||
|
||||
it('renders placeholder UI', () => {
|
||||
aiAssistant.getters.askAssistantFloatingButton().should('be.visible');
|
||||
aiAssistant.getters.askAssistantFloatingButton().click();
|
||||
aiAssistant.getters.askAssistantChat().should('be.visible');
|
||||
aiAssistant.getters.placeholderMessage().should('be.visible');
|
||||
aiAssistant.getters.chatInputWrapper().should('not.exist');
|
||||
aiAssistant.getters.closeChatButton().should('be.visible');
|
||||
aiAssistant.getters.closeChatButton().click();
|
||||
aiAssistant.getters.askAssistantChat().should('not.exist');
|
||||
});
|
||||
|
||||
it('should resize assistant chat up', () => {
|
||||
aiAssistant.getters.askAssistantFloatingButton().click();
|
||||
aiAssistant.getters.askAssistantSidebarResizer().should('be.visible');
|
||||
aiAssistant.getters.askAssistantChat().then((element) => {
|
||||
const { width, left } = element[0].getBoundingClientRect();
|
||||
cy.drag(aiAssistant.getters.askAssistantSidebarResizer(), [left - 10, 0], {
|
||||
abs: true,
|
||||
clickToFinish: true,
|
||||
});
|
||||
aiAssistant.getters.askAssistantChat().then((newElement) => {
|
||||
const newWidth = newElement[0].getBoundingClientRect().width;
|
||||
expect(newWidth).to.be.greaterThan(width);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should resize assistant chat down', () => {
|
||||
aiAssistant.getters.askAssistantFloatingButton().click();
|
||||
aiAssistant.getters.askAssistantSidebarResizer().should('be.visible');
|
||||
aiAssistant.getters.askAssistantChat().then((element) => {
|
||||
const { width, left } = element[0].getBoundingClientRect();
|
||||
cy.drag(aiAssistant.getters.askAssistantSidebarResizer(), [left + 10, 0], {
|
||||
abs: true,
|
||||
clickToFinish: true,
|
||||
});
|
||||
aiAssistant.getters.askAssistantChat().then((newElement) => {
|
||||
const newWidth = newElement[0].getBoundingClientRect().width;
|
||||
expect(newWidth).to.be.lessThan(width);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should start chat session from node error view', () => {
|
||||
cy.intercept('POST', '/rest/ai-assistant/chat', {
|
||||
statusCode: 200,
|
||||
fixture: 'aiAssistant/simple_message_response.json',
|
||||
}).as('chatRequest');
|
||||
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
|
||||
wf.actions.openNode('Stop and Error');
|
||||
ndv.getters.nodeExecuteButton().click();
|
||||
aiAssistant.getters.nodeErrorViewAssistantButton().click();
|
||||
cy.wait('@chatRequest');
|
||||
aiAssistant.getters.chatMessagesAll().should('have.length', 1);
|
||||
aiAssistant.getters
|
||||
.chatMessagesAll()
|
||||
.eq(0)
|
||||
.should('contain.text', 'Hey, this is an assistant message');
|
||||
aiAssistant.getters.nodeErrorViewAssistantButton().should('be.disabled');
|
||||
});
|
||||
|
||||
it('should render chat input correctly', () => {
|
||||
cy.intercept('POST', '/rest/ai-assistant/chat', {
|
||||
statusCode: 200,
|
||||
fixture: 'aiAssistant/simple_message_response.json',
|
||||
}).as('chatRequest');
|
||||
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
|
||||
wf.actions.openNode('Stop and Error');
|
||||
ndv.getters.nodeExecuteButton().click();
|
||||
aiAssistant.getters.nodeErrorViewAssistantButton().click();
|
||||
cy.wait('@chatRequest');
|
||||
// Send button should be disabled when input is empty
|
||||
aiAssistant.getters.sendMessageButton().should('be.disabled');
|
||||
aiAssistant.getters.chatInput().type('Yo ');
|
||||
aiAssistant.getters.sendMessageButton().should('not.be.disabled');
|
||||
aiAssistant.getters.chatInput().then((element) => {
|
||||
const { height } = element[0].getBoundingClientRect();
|
||||
// Shift + Enter should add a new line
|
||||
aiAssistant.getters.chatInput().type('Hello{shift+enter}there');
|
||||
aiAssistant.getters.chatInput().then((newElement) => {
|
||||
const newHeight = newElement[0].getBoundingClientRect().height;
|
||||
// Chat input should grow as user adds new lines
|
||||
expect(newHeight).to.be.greaterThan(height);
|
||||
aiAssistant.getters.sendMessageButton().click();
|
||||
cy.wait('@chatRequest');
|
||||
// New lines should be rendered as <br> in the chat
|
||||
aiAssistant.getters.chatMessagesUser().should('have.length', 1);
|
||||
aiAssistant.getters.chatMessagesUser().eq(0).find('br').should('have.length', 1);
|
||||
// Chat input should be cleared now
|
||||
aiAssistant.getters.chatInput().should('have.value', '');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should render and handle quick replies', () => {
|
||||
cy.intercept('POST', '/rest/ai-assistant/chat', {
|
||||
statusCode: 200,
|
||||
fixture: 'aiAssistant/quick_reply_message_response.json',
|
||||
}).as('chatRequest');
|
||||
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
|
||||
wf.actions.openNode('Stop and Error');
|
||||
ndv.getters.nodeExecuteButton().click();
|
||||
aiAssistant.getters.nodeErrorViewAssistantButton().click();
|
||||
cy.wait('@chatRequest');
|
||||
aiAssistant.getters.quickReplies().should('have.length', 2);
|
||||
aiAssistant.getters.quickReplies().eq(0).click();
|
||||
cy.wait('@chatRequest');
|
||||
aiAssistant.getters.chatMessagesUser().should('have.length', 1);
|
||||
aiAssistant.getters.chatMessagesUser().eq(0).should('contain.text', "Sure, let's do it");
|
||||
});
|
||||
|
||||
it('should send message to assistant when node is executed', () => {
|
||||
cy.intercept('POST', '/rest/ai-assistant/chat', {
|
||||
statusCode: 200,
|
||||
fixture: 'aiAssistant/simple_message_response.json',
|
||||
}).as('chatRequest');
|
||||
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
|
||||
wf.actions.openNode('Edit Fields');
|
||||
ndv.getters.nodeExecuteButton().click();
|
||||
aiAssistant.getters.nodeErrorViewAssistantButton().click();
|
||||
cy.wait('@chatRequest');
|
||||
aiAssistant.getters.chatMessagesAssistant().should('have.length', 1);
|
||||
// Executing the same node should sende a new message to the assistant automatically
|
||||
ndv.getters.nodeExecuteButton().click();
|
||||
cy.wait('@chatRequest');
|
||||
aiAssistant.getters.chatMessagesAssistant().should('have.length', 2);
|
||||
});
|
||||
|
||||
it('should warn before starting a new session', () => {
|
||||
cy.intercept('POST', '/rest/ai-assistant/chat', {
|
||||
statusCode: 200,
|
||||
fixture: 'aiAssistant/simple_message_response.json',
|
||||
}).as('chatRequest');
|
||||
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
|
||||
wf.actions.openNode('Edit Fields');
|
||||
ndv.getters.nodeExecuteButton().click();
|
||||
aiAssistant.getters.nodeErrorViewAssistantButton().click();
|
||||
cy.wait('@chatRequest');
|
||||
aiAssistant.getters.closeChatButton().click();
|
||||
ndv.getters.backToCanvas().click();
|
||||
wf.actions.openNode('Stop and Error');
|
||||
ndv.getters.nodeExecuteButton().click();
|
||||
aiAssistant.getters.nodeErrorViewAssistantButton().click();
|
||||
// Since we already have an active session, a warning should be shown
|
||||
aiAssistant.getters.newAssistantSessionModal().should('be.visible');
|
||||
aiAssistant.getters
|
||||
.newAssistantSessionModal()
|
||||
.find('button')
|
||||
.contains('Start new session')
|
||||
.click();
|
||||
cy.wait('@chatRequest');
|
||||
// New session should start with initial assistant message
|
||||
aiAssistant.getters.chatMessagesAll().should('have.length', 1);
|
||||
});
|
||||
|
||||
it('should apply code diff to code node', () => {
|
||||
cy.intercept('POST', '/rest/ai-assistant/chat', {
|
||||
statusCode: 200,
|
||||
fixture: 'aiAssistant/code_diff_suggestion_response.json',
|
||||
}).as('chatRequest');
|
||||
cy.intercept('POST', '/rest/ai-assistant/chat/apply-suggestion', {
|
||||
statusCode: 200,
|
||||
fixture: 'aiAssistant/apply_code_diff_response.json',
|
||||
}).as('applySuggestion');
|
||||
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
|
||||
wf.actions.openNode('Code');
|
||||
ndv.getters.nodeExecuteButton().click();
|
||||
aiAssistant.getters.nodeErrorViewAssistantButton().click({ force: true });
|
||||
cy.wait('@chatRequest');
|
||||
// Should have two assistant messages
|
||||
aiAssistant.getters.chatMessagesAll().should('have.length', 2);
|
||||
aiAssistant.getters.codeDiffs().should('have.length', 1);
|
||||
aiAssistant.getters.applyCodeDiffButtons().should('have.length', 1);
|
||||
aiAssistant.getters.applyCodeDiffButtons().first().click();
|
||||
cy.wait('@applySuggestion');
|
||||
aiAssistant.getters.applyCodeDiffButtons().should('have.length', 0);
|
||||
aiAssistant.getters.undoReplaceCodeButtons().should('have.length', 1);
|
||||
aiAssistant.getters.codeReplacedMessage().should('be.visible');
|
||||
ndv.getters
|
||||
.parameterInput('jsCode')
|
||||
.get('.cm-content')
|
||||
.should('contain.text', 'item.json.myNewField = 1');
|
||||
// Clicking undo should revert the code back but not call the assistant
|
||||
aiAssistant.getters.undoReplaceCodeButtons().first().click();
|
||||
aiAssistant.getters.applyCodeDiffButtons().should('have.length', 1);
|
||||
aiAssistant.getters.codeReplacedMessage().should('not.exist');
|
||||
cy.get('@applySuggestion.all').then((interceptions) => {
|
||||
expect(interceptions).to.have.length(1);
|
||||
});
|
||||
ndv.getters
|
||||
.parameterInput('jsCode')
|
||||
.get('.cm-content')
|
||||
.should('contain.text', 'item.json.myNewField = 1aaa');
|
||||
// Replacing the code again should also not call the assistant
|
||||
cy.get('@applySuggestion.all').then((interceptions) => {
|
||||
expect(interceptions).to.have.length(1);
|
||||
});
|
||||
aiAssistant.getters.applyCodeDiffButtons().should('have.length', 1);
|
||||
aiAssistant.getters.applyCodeDiffButtons().first().click();
|
||||
ndv.getters
|
||||
.parameterInput('jsCode')
|
||||
.get('.cm-content')
|
||||
.should('contain.text', 'item.json.myNewField = 1');
|
||||
});
|
||||
|
||||
it('should end chat session when `end_session` event is received', () => {
|
||||
cy.intercept('POST', '/rest/ai-assistant/chat', {
|
||||
statusCode: 200,
|
||||
fixture: 'aiAssistant/end_session_response.json',
|
||||
}).as('chatRequest');
|
||||
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
|
||||
wf.actions.openNode('Stop and Error');
|
||||
ndv.getters.nodeExecuteButton().click();
|
||||
aiAssistant.getters.nodeErrorViewAssistantButton().click();
|
||||
cy.wait('@chatRequest');
|
||||
aiAssistant.getters.chatMessagesSystem().should('have.length', 1);
|
||||
aiAssistant.getters.chatMessagesSystem().first().should('contain.text', 'session has ended');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"data": {
|
||||
"sessionId": "f9130bd7-c078-4862-a38a-369b27b0ff20-e96eb9f7-d581-4684-b6a9-fd3dfe9fe1fb-emTezIGat7bQsDdtIlbti",
|
||||
"parameters": {
|
||||
"jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.myNewField = 1;\n}\n\nreturn $input.all();"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"sessionId": "1",
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"type": "message",
|
||||
"text": "Hi there! Here is my top solution to fix the error in your **Code** node 👇"
|
||||
},
|
||||
{
|
||||
"type": "code-diff",
|
||||
"description": "Fix the syntax error by changing '1asd' to a valid value. In this case, it seems like '1' was intended.",
|
||||
"suggestionId": "1",
|
||||
"codeDiff": "@@ -2,2 +2,2 @@\n item.json.myNewField = 1asd;\n+ item.json.myNewField = 1;\n",
|
||||
"role": "assistant",
|
||||
"quickReplies": [
|
||||
{
|
||||
"text": "Give me another solution",
|
||||
"type": "new-suggestion"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
16
cypress/fixtures/aiAssistant/end_session_response.json
Normal file
16
cypress/fixtures/aiAssistant/end_session_response.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"sessionId": "f9130bd7-c078-4862-a38a-369b27b0ff20-e96eb9f7-d581-4684-b6a9-fd3dfe9fe1fb-XCldJLlusGrEVku5I9cYT",
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"type": "agent-suggestion",
|
||||
"title": "Glad to Help",
|
||||
"text": "I'm glad I could help. If you have any more questions or need further assistance with your n8n workflows, feel free to ask!"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"type": "event",
|
||||
"eventName": "end-session"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"sessionId": "1",
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"type": "message",
|
||||
"text": "Hey, this is an assistant message",
|
||||
"quickReplies": [
|
||||
{
|
||||
"text": "Sure, let's do it",
|
||||
"type": "yes"
|
||||
},
|
||||
{
|
||||
"text": "Nah, doesn't sound good",
|
||||
"type": "no"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
10
cypress/fixtures/aiAssistant/simple_message_response.json
Normal file
10
cypress/fixtures/aiAssistant/simple_message_response.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"sessionId": "1",
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"type": "message",
|
||||
"text": "Hey, this is an assistant message"
|
||||
}
|
||||
]
|
||||
}
|
88
cypress/fixtures/aiAssistant/test_workflow.json
Normal file
88
cypress/fixtures/aiAssistant/test_workflow.json
Normal file
|
@ -0,0 +1,88 @@
|
|||
{
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "ebfced75-2ce1-4c41-a971-6c3b83522c4d",
|
||||
"name": "When clicking ‘Test workflow’",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
360,
|
||||
220
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"errorMessage": "This is an error message"
|
||||
},
|
||||
"id": "f2e60459-401a-49d5-acfc-7b2b31cfdcf7",
|
||||
"name": "Stop and Error",
|
||||
"type": "n8n-nodes-base.stopAndError",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1020,
|
||||
220
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.myNewField = 1aaa;\n}\n\nreturn $input.all();"
|
||||
},
|
||||
"id": "b54d4db9-b257-41a8-862f-26d293115bad",
|
||||
"name": "Code",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
840,
|
||||
320
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "053ada73-f7db-4e6a-8cc8-85756cc6ca4e",
|
||||
"name": "age",
|
||||
"value": "={{ 32sad }}",
|
||||
"type": "number"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "5fd89612-a871-4679-b7b0-d659e09c6a0e",
|
||||
"name": "Edit Fields",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [
|
||||
600,
|
||||
100
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking ‘Test workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Stop and Error",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Code",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Edit Fields",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {}
|
||||
}
|
|
@ -14,7 +14,7 @@
|
|||
"start": "cd ..; pnpm start"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.14.195",
|
||||
"@types/lodash": "catalog:",
|
||||
"eslint-plugin-cypress": "^3.3.0",
|
||||
"n8n-workflow": "workspace:*"
|
||||
},
|
||||
|
@ -24,8 +24,8 @@
|
|||
"cypress": "^13.11.0",
|
||||
"cypress-otp": "^1.0.3",
|
||||
"cypress-real-events": "^1.12.0",
|
||||
"lodash": "4.17.21",
|
||||
"nanoid": "3.3.6",
|
||||
"start-server-and-test": "^2.0.3"
|
||||
"lodash": "catalog:",
|
||||
"nanoid": "catalog:",
|
||||
"start-server-and-test": "^2.0.5"
|
||||
}
|
||||
}
|
||||
|
|
49
cypress/pages/features/ai-assistant.ts
Normal file
49
cypress/pages/features/ai-assistant.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { overrideFeatureFlag } from '../../composables/featureFlags';
|
||||
import { BasePage } from '../base';
|
||||
|
||||
const AI_ASSISTANT_FEATURE = {
|
||||
name: 'aiAssistant',
|
||||
experimentName: '021_ai_debug_helper',
|
||||
enabledFor: 'variant',
|
||||
disabledFor: 'control',
|
||||
};
|
||||
|
||||
export class AIAssistant extends BasePage {
|
||||
url = '/workflows/new';
|
||||
|
||||
getters = {
|
||||
askAssistantFloatingButton: () => cy.getByTestId('ask-assistant-floating-button'),
|
||||
askAssistantSidebar: () => cy.getByTestId('ask-assistant-sidebar'),
|
||||
askAssistantSidebarResizer: () =>
|
||||
this.getters.askAssistantSidebar().find('[class^=_resizer][data-dir=left]').first(),
|
||||
askAssistantChat: () => cy.getByTestId('ask-assistant-chat'),
|
||||
placeholderMessage: () => cy.getByTestId('placeholder-message'),
|
||||
closeChatButton: () => cy.getByTestId('close-chat-button'),
|
||||
chatInputWrapper: () => cy.getByTestId('chat-input-wrapper'),
|
||||
chatInput: () => cy.getByTestId('chat-input'),
|
||||
sendMessageButton: () => cy.getByTestId('send-message-button'),
|
||||
chatMessagesAll: () => cy.get('[data-test-id^=chat-message]'),
|
||||
chatMessagesAssistant: () => cy.getByTestId('chat-message-assistant'),
|
||||
chatMessagesUser: () => cy.getByTestId('chat-message-user'),
|
||||
chatMessagesSystem: () => cy.getByTestId('chat-message-system'),
|
||||
quickReplies: () => cy.getByTestId('quick-replies').find('button'),
|
||||
newAssistantSessionModal: () => cy.getByTestId('new-assistant-session-modal'),
|
||||
codeDiffs: () => cy.getByTestId('code-diff-suggestion'),
|
||||
applyCodeDiffButtons: () => cy.getByTestId('replace-code-button'),
|
||||
undoReplaceCodeButtons: () => cy.getByTestId('undo-replace-button'),
|
||||
codeReplacedMessage: () => cy.getByTestId('code-replaced-message'),
|
||||
nodeErrorViewAssistantButton: () =>
|
||||
cy.getByTestId('node-error-view-ask-assistant-button').find('button').first(),
|
||||
};
|
||||
|
||||
actions = {
|
||||
enableAssistant(): void {
|
||||
overrideFeatureFlag(AI_ASSISTANT_FEATURE.experimentName, AI_ASSISTANT_FEATURE.enabledFor);
|
||||
cy.enableFeature(AI_ASSISTANT_FEATURE.name);
|
||||
},
|
||||
disableAssistant(): void {
|
||||
overrideFeatureFlag(AI_ASSISTANT_FEATURE.experimentName, AI_ASSISTANT_FEATURE.disabledFor);
|
||||
cy.disableFeature(AI_ASSISTANT_FEATURE.name);
|
||||
},
|
||||
};
|
||||
}
|
|
@ -24,6 +24,7 @@ export class NDV extends BasePage {
|
|||
editPinnedDataButton: () => cy.getByTestId('ndv-edit-pinned-data'),
|
||||
pinnedDataEditor: () => this.getters.outputPanel().find('.cm-editor .cm-scroller .cm-content'),
|
||||
runDataPaneHeader: () => cy.getByTestId('run-data-pane-header'),
|
||||
aiOutputModeToggle: () => cy.getByTestId('ai-output-mode-select'),
|
||||
nodeOutputHint: () => cy.getByTestId('ndv-output-run-node-hint'),
|
||||
savePinnedDataButton: () =>
|
||||
this.getters.runDataPaneHeader().find('button').filter(':visible').contains('Save'),
|
||||
|
@ -137,6 +138,8 @@ export class NDV extends BasePage {
|
|||
cy.getByTestId(`fixed-collection-${paramName}`),
|
||||
schemaViewNode: () => cy.getByTestId('run-data-schema-node'),
|
||||
schemaViewNodeName: () => cy.getByTestId('run-data-schema-node-name'),
|
||||
expressionExpanders: () => cy.getByTestId('expander'),
|
||||
expressionModalOutput: () => cy.getByTestId('expression-modal-output'),
|
||||
};
|
||||
|
||||
actions = {
|
||||
|
@ -174,7 +177,7 @@ export class NDV extends BasePage {
|
|||
this.getters.editPinnedDataButton().click();
|
||||
|
||||
this.getters.pinnedDataEditor().click();
|
||||
this.getters.pinnedDataEditor().type('{selectall}{backspace}').paste(JSON.stringify(data));
|
||||
this.getters.pinnedDataEditor().invoke('text', '').paste(JSON.stringify(data));
|
||||
|
||||
this.actions.savePinnedData();
|
||||
},
|
||||
|
@ -204,9 +207,9 @@ export class NDV extends BasePage {
|
|||
const droppable = `[data-test-id="parameter-input-${parameterName}"]`;
|
||||
cy.draganddrop(draggable, droppable);
|
||||
},
|
||||
mapToParameter: (parameterName: string) => {
|
||||
mapToParameter: (parameterName: string, position?: 'top' | 'center' | 'bottom') => {
|
||||
const droppable = `[data-test-id="parameter-input-${parameterName}"]`;
|
||||
cy.draganddrop('', droppable);
|
||||
cy.draganddrop('', droppable, { position });
|
||||
},
|
||||
switchInputMode: (type: 'Schema' | 'Table' | 'JSON' | 'Binary') => {
|
||||
this.getters.inputDisplayMode().find('label').contains(type).click({ force: true });
|
||||
|
|
|
@ -175,7 +175,7 @@ Cypress.Commands.add('drag', (selector, pos, options) => {
|
|||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('draganddrop', (draggableSelector, droppableSelector) => {
|
||||
Cypress.Commands.add('draganddrop', (draggableSelector, droppableSelector, options) => {
|
||||
if (draggableSelector) {
|
||||
cy.get(draggableSelector).should('exist');
|
||||
}
|
||||
|
@ -197,7 +197,7 @@ Cypress.Commands.add('draganddrop', (draggableSelector, droppableSelector) => {
|
|||
cy.get(droppableSelector).realMouseMove(0, 0);
|
||||
cy.get(droppableSelector).realMouseMove(pageX, pageY);
|
||||
cy.get(droppableSelector).realHover();
|
||||
cy.get(droppableSelector).realMouseUp();
|
||||
cy.get(droppableSelector).realMouseUp({ position: options?.position ?? 'top' });
|
||||
if (draggableSelector) {
|
||||
cy.get(draggableSelector).realMouseUp();
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@ beforeEach(() => {
|
|||
|
||||
cy.window().then((win): void => {
|
||||
win.localStorage.setItem('N8N_THEME', 'light');
|
||||
win.localStorage.setItem('N8N_AUTOCOMPLETE_ONBOARDED', 'true');
|
||||
win.localStorage.setItem('N8N_MAPPING_ONBOARDED', 'true');
|
||||
});
|
||||
|
||||
cy.intercept('GET', '/rest/settings', (req) => {
|
||||
|
|
|
@ -12,6 +12,10 @@ interface SigninPayload {
|
|||
password: string;
|
||||
}
|
||||
|
||||
interface DragAndDropOptions {
|
||||
position: 'top' | 'center' | 'bottom';
|
||||
}
|
||||
|
||||
declare global {
|
||||
namespace Cypress {
|
||||
interface SuiteConfigOverrides {
|
||||
|
@ -56,7 +60,11 @@ declare global {
|
|||
target: [number, number],
|
||||
options?: { abs?: boolean; index?: number; realMouse?: boolean; clickToFinish?: boolean },
|
||||
): void;
|
||||
draganddrop(draggableSelector: string, droppableSelector: string): void;
|
||||
draganddrop(
|
||||
draggableSelector: string,
|
||||
droppableSelector: string,
|
||||
options?: Partial<DragAndDropOptions>,
|
||||
): void;
|
||||
push(type: string, data: unknown): void;
|
||||
shouldNotHaveConsoleErrors(): void;
|
||||
window(): Chainable<
|
||||
|
|
|
@ -3,7 +3,7 @@ export function getPopper() {
|
|||
}
|
||||
|
||||
export function getVisiblePopper() {
|
||||
return getPopper().filter(':visible');
|
||||
return getPopper().filter('[aria-hidden="false"]');
|
||||
}
|
||||
|
||||
export function getVisibleSelect() {
|
||||
|
|
|
@ -230,6 +230,4 @@ Before you upgrade to the latest version make sure to check here if there are an
|
|||
|
||||
## License
|
||||
|
||||
n8n is [fair-code](https://faircode.io) distributed under the [**Sustainable Use License**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md).
|
||||
|
||||
Additional information about the license can be found in the [docs](https://docs.n8n.io/reference/license/).
|
||||
You can find the license information [here](https://github.com/n8n-io/n8n/blob/master/README.md#license)
|
||||
|
|
18
package.json
18
package.json
|
@ -1,19 +1,19 @@
|
|||
{
|
||||
"name": "n8n-monorepo",
|
||||
"version": "1.52.0",
|
||||
"version": "1.56.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=18.10",
|
||||
"pnpm": ">=9.1"
|
||||
"node": ">=20.15",
|
||||
"pnpm": ">=9.5"
|
||||
},
|
||||
"packageManager": "pnpm@9.1.4",
|
||||
"packageManager": "pnpm@9.6.0",
|
||||
"scripts": {
|
||||
"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",
|
||||
"typecheck": "turbo --filter=!n8n typecheck",
|
||||
"typecheck": "turbo typecheck",
|
||||
"dev": "turbo run dev --parallel --env-mode=loose --filter=!n8n-design-system --filter=!@n8n/chat",
|
||||
"dev:ai": "turbo run dev --parallel --env-mode=loose --filter=@n8n/nodes-langchain --filter=n8n --filter=n8n-core",
|
||||
"clean": "turbo run clean --parallel",
|
||||
|
@ -40,7 +40,6 @@
|
|||
"@n8n_io/eslint-config": "workspace:*",
|
||||
"@types/jest": "^29.5.3",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@vitest/coverage-v8": "^1.6.0",
|
||||
"jest": "^29.6.2",
|
||||
"jest-environment-jsdom": "^29.6.2",
|
||||
"jest-expect-message": "^1.1.3",
|
||||
|
@ -56,11 +55,7 @@
|
|||
"tsc-alias": "^1.8.7",
|
||||
"tsc-watch": "^6.0.4",
|
||||
"turbo": "2.0.6",
|
||||
"typescript": "*",
|
||||
"vite": "^5.2.12",
|
||||
"vitest": "^1.6.0",
|
||||
"vitest-mock-extended": "^1.3.1",
|
||||
"vue-tsc": "^2.0.19"
|
||||
"typescript": "*"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
|
@ -68,7 +63,6 @@
|
|||
],
|
||||
"overrides": {
|
||||
"@types/node": "^18.16.16",
|
||||
"axios": "1.6.7",
|
||||
"chokidar": "3.5.2",
|
||||
"esbuild": "^0.20.2",
|
||||
"formidable": "3.5.1",
|
||||
|
|
62
packages/@n8n/benchmark/Dockerfile
Normal file
62
packages/@n8n/benchmark/Dockerfile
Normal file
|
@ -0,0 +1,62 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
FROM node:20.16.0 AS base
|
||||
|
||||
# Install required dependencies
|
||||
RUN apt-get update && apt-get install -y gnupg2 curl
|
||||
|
||||
# Add k6 GPG key and repository
|
||||
RUN mkdir -p /etc/apt/keyrings && \
|
||||
curl -sS https://dl.k6.io/key.gpg | gpg --dearmor --yes -o /etc/apt/keyrings/k6.gpg && \
|
||||
chmod a+x /etc/apt/keyrings/k6.gpg && \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/k6.gpg] https://dl.k6.io/deb stable main" | tee /etc/apt/sources.list.d/k6.list
|
||||
|
||||
# Update and install k6
|
||||
RUN apt-get update && \
|
||||
apt-get install -y k6 tini && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
|
||||
#
|
||||
# Builder
|
||||
FROM base AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --chown=node:node ./pnpm-lock.yaml /app/pnpm-lock.yaml
|
||||
COPY --chown=node:node ./pnpm-workspace.yaml /app/pnpm-workspace.yaml
|
||||
COPY --chown=node:node ./package.json /app/package.json
|
||||
COPY --chown=node:node ./packages/@n8n/benchmark/package.json /app/packages/@n8n/benchmark/package.json
|
||||
COPY --chown=node:node ./patches /app/patches
|
||||
COPY --chown=node:node ./scripts /app/scripts
|
||||
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# TS config files
|
||||
COPY --chown=node:node ./tsconfig.json /app/tsconfig.json
|
||||
COPY --chown=node:node ./tsconfig.build.json /app/tsconfig.build.json
|
||||
COPY --chown=node:node ./tsconfig.backend.json /app/tsconfig.backend.json
|
||||
COPY --chown=node:node ./packages/@n8n/benchmark/tsconfig.json /app/packages/@n8n/benchmark/tsconfig.json
|
||||
COPY --chown=node:node ./packages/@n8n/benchmark/tsconfig.build.json /app/packages/@n8n/benchmark/tsconfig.build.json
|
||||
|
||||
# Source files
|
||||
COPY --chown=node:node ./packages/@n8n/benchmark/src /app/packages/@n8n/benchmark/src
|
||||
COPY --chown=node:node ./packages/@n8n/benchmark/bin /app/packages/@n8n/benchmark/bin
|
||||
COPY --chown=node:node ./packages/@n8n/benchmark/scenarios /app/packages/@n8n/benchmark/scenarios
|
||||
|
||||
WORKDIR /app/packages/@n8n/benchmark
|
||||
RUN pnpm build
|
||||
|
||||
#
|
||||
# Runner
|
||||
FROM base AS runner
|
||||
|
||||
COPY --from=builder /app /app
|
||||
|
||||
WORKDIR /app/packages/@n8n/benchmark
|
||||
USER node
|
||||
|
||||
ENTRYPOINT [ "/app/packages/@n8n/benchmark/bin/n8n-benchmark" ]
|
55
packages/@n8n/benchmark/README.md
Normal file
55
packages/@n8n/benchmark/README.md
Normal file
|
@ -0,0 +1,55 @@
|
|||
# n8n benchmarking tool
|
||||
|
||||
Tool for executing benchmarks against an n8n instance.
|
||||
|
||||
## Running locally with Docker
|
||||
|
||||
Build the Docker image:
|
||||
|
||||
```sh
|
||||
# Must be run in the repository root
|
||||
# k6 doesn't have an arm64 build available for linux, we need to build against amd64
|
||||
docker build --platform linux/amd64 -t n8n-benchmark -f packages/@n8n/benchmark/Dockerfile .
|
||||
```
|
||||
|
||||
Run the image
|
||||
|
||||
```sh
|
||||
docker run \
|
||||
-e N8N_USER_EMAIL=user@n8n.io \
|
||||
-e N8N_USER_PASSWORD=password \
|
||||
# For macos, n8n running outside docker
|
||||
-e N8N_BASE_URL=http://host.docker.internal:5678 \
|
||||
n8n-benchmark
|
||||
```
|
||||
|
||||
## Running locally without Docker
|
||||
|
||||
Requirements:
|
||||
|
||||
- [k6](https://grafana.com/docs/k6/latest/set-up/install-k6/)
|
||||
- Node.js v20 or higher
|
||||
|
||||
```sh
|
||||
pnpm build
|
||||
|
||||
# Run tests against http://localhost:5678 with specified email and password
|
||||
N8N_USER_EMAIL=user@n8n.io N8N_USER_PASSWORD=password ./bin/n8n-benchmark run
|
||||
|
||||
# If you installed k6 using brew, you might have to specify it explicitly
|
||||
K6_PATH=/opt/homebrew/bin/k6 N8N_USER_EMAIL=user@n8n.io N8N_USER_PASSWORD=password ./bin/n8n-benchmark run
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The configuration options the cli accepts can be seen from [config.ts](./src/config/config.ts)
|
||||
|
||||
## Benchmark scenarios
|
||||
|
||||
A benchmark scenario defines one or multiple steps to execute and measure. It consists of:
|
||||
|
||||
- Manifest file which describes and configures the scenario
|
||||
- Any test data that is imported before the scenario is run
|
||||
- A [`k6`](https://grafana.com/docs/k6/latest/using-k6/http-requests/) script which executes the steps and receives `API_BASE_URL` environment variable in runtime.
|
||||
|
||||
Available scenarios are located in [`./scenarios`](./scenarios/).
|
13
packages/@n8n/benchmark/bin/n8n-benchmark
Executable file
13
packages/@n8n/benchmark/bin/n8n-benchmark
Executable file
|
@ -0,0 +1,13 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// Check if version should be displayed
|
||||
const versionFlags = ['-v', '-V', '--version'];
|
||||
if (versionFlags.includes(process.argv.slice(-1)[0])) {
|
||||
console.log(require('../package').version);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const oclif = require('@oclif/core');
|
||||
await oclif.execute({ dir: __dirname });
|
||||
})();
|
48
packages/@n8n/benchmark/package.json
Normal file
48
packages/@n8n/benchmark/package.json
Normal file
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"name": "@n8n/n8n-benchmark",
|
||||
"version": "1.0.0",
|
||||
"description": "Cli for running benchmark tests for n8n",
|
||||
"main": "dist/index",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
|
||||
"start": "./bin/n8n-benchmark",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"watch": "concurrently \"tsc -w -p tsconfig.build.json\" \"tsc-alias -w -p tsconfig.build.json\""
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.10"
|
||||
},
|
||||
"keywords": [
|
||||
"automate",
|
||||
"automation",
|
||||
"IaaS",
|
||||
"iPaaS",
|
||||
"n8n",
|
||||
"workflow",
|
||||
"benchmark",
|
||||
"performance"
|
||||
],
|
||||
"dependencies": {
|
||||
"@oclif/core": "4.0.7",
|
||||
"axios": "catalog:",
|
||||
"convict": "6.2.4",
|
||||
"dotenv": "8.6.0",
|
||||
"zx": "^8.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/convict": "^6.1.1",
|
||||
"@types/k6": "^0.52.0",
|
||||
"@types/node": "^20.14.8",
|
||||
"tsc-alias": "^1.8.7",
|
||||
"typescript": "^5.5.2"
|
||||
},
|
||||
"bin": {
|
||||
"n8n-benchmark": "./bin/n8n-benchmark"
|
||||
},
|
||||
"oclif": {
|
||||
"bin": "n8n-benchmark",
|
||||
"commands": "./dist/commands",
|
||||
"topicSeparator": " "
|
||||
}
|
||||
}
|
42
packages/@n8n/benchmark/scenarios/scenario.schema.json
Normal file
42
packages/@n8n/benchmark/scenarios/scenario.schema.json
Normal file
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"definitions": {
|
||||
"ScenarioData": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"workflowFiles": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": "string",
|
||||
"description": "The JSON schema to validate this file"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the scenario"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "A longer description of the scenario"
|
||||
},
|
||||
"scriptPath": {
|
||||
"type": "string",
|
||||
"description": "Relative path to the k6 test script"
|
||||
},
|
||||
"scenarioData": {
|
||||
"$ref": "#/definitions/ScenarioData",
|
||||
"description": "Data to import before running the scenario"
|
||||
}
|
||||
},
|
||||
"required": ["name", "description", "scriptPath", "scenarioData"],
|
||||
"additionalProperties": false
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"createdAt": "2024-08-06T12:19:51.268Z",
|
||||
"updatedAt": "2024-08-06T12:20:45.000Z",
|
||||
"name": "Single Webhook",
|
||||
"active": true,
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": { "path": "single-webhook", "options": {} },
|
||||
"id": "7587ab0e-cc15-424f-83c0-c887a0eb97fb",
|
||||
"name": "Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"position": [760, 400],
|
||||
"webhookId": "fa563fc2-c73f-4631-99a1-39c16f1f858f"
|
||||
}
|
||||
],
|
||||
"connections": {},
|
||||
"settings": { "executionOrder": "v1" },
|
||||
"staticData": null,
|
||||
"meta": { "templateCredsSetupCompleted": true, "responseMode": "lastNode", "options": {} },
|
||||
"pinData": {},
|
||||
"versionId": "840a38a1-ba37-433d-9f20-de73f5131a2b",
|
||||
"triggerCount": 1,
|
||||
"tags": []
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "../scenario.schema.json",
|
||||
"name": "SingleWebhook",
|
||||
"description": "A single webhook trigger that responds with a 200 status code",
|
||||
"scenarioData": { "workflowFiles": ["singleWebhook.json"] },
|
||||
"scriptPath": "singleWebhook.script.ts"
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import http from 'k6/http';
|
||||
import { check } from 'k6';
|
||||
|
||||
const apiBaseUrl = __ENV.API_BASE_URL;
|
||||
|
||||
export default function () {
|
||||
const res = http.get(`${apiBaseUrl}/webhook/single-webhook`);
|
||||
check(res, {
|
||||
'is status 200': (r) => r.status === 200,
|
||||
});
|
||||
}
|
21
packages/@n8n/benchmark/src/commands/list.ts
Normal file
21
packages/@n8n/benchmark/src/commands/list.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { Command } from '@oclif/core';
|
||||
import { ScenarioLoader } from '@/scenario/scenarioLoader';
|
||||
import { loadConfig } from '@/config/config';
|
||||
|
||||
export default class ListCommand extends Command {
|
||||
static description = 'List all available scenarios';
|
||||
|
||||
async run() {
|
||||
const config = loadConfig();
|
||||
const scenarioLoader = new ScenarioLoader();
|
||||
|
||||
const allScenarios = scenarioLoader.loadAll(config.get('testScenariosPath'));
|
||||
|
||||
console.log('Available test scenarios:');
|
||||
console.log('');
|
||||
|
||||
for (const scenario of allScenarios) {
|
||||
console.log('\t', scenario.name, ':', scenario.description);
|
||||
}
|
||||
}
|
||||
}
|
39
packages/@n8n/benchmark/src/commands/run.ts
Normal file
39
packages/@n8n/benchmark/src/commands/run.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { Command, Flags } from '@oclif/core';
|
||||
import { loadConfig } from '@/config/config';
|
||||
import { ScenarioLoader } from '@/scenario/scenarioLoader';
|
||||
import { ScenarioRunner } from '@/testExecution/scenarioRunner';
|
||||
import { N8nApiClient } from '@/n8nApiClient/n8nApiClient';
|
||||
import { ScenarioDataFileLoader } from '@/scenario/scenarioDataLoader';
|
||||
import { K6Executor } from '@/testExecution/k6Executor';
|
||||
|
||||
export default class RunCommand extends Command {
|
||||
static description = 'Run all (default) or specified test scenarios';
|
||||
|
||||
// TODO: Add support for filtering scenarios
|
||||
static flags = {
|
||||
scenarios: Flags.string({
|
||||
char: 't',
|
||||
description: 'Comma-separated list of test scenarios to run',
|
||||
required: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async run() {
|
||||
const config = loadConfig();
|
||||
const scenarioLoader = new ScenarioLoader();
|
||||
|
||||
const scenarioRunner = new ScenarioRunner(
|
||||
new N8nApiClient(config.get('n8n.baseUrl')),
|
||||
new ScenarioDataFileLoader(),
|
||||
new K6Executor(config.get('k6ExecutablePath'), config.get('n8n.baseUrl')),
|
||||
{
|
||||
email: config.get('n8n.user.email'),
|
||||
password: config.get('n8n.user.password'),
|
||||
},
|
||||
);
|
||||
|
||||
const allScenarios = scenarioLoader.loadAll(config.get('testScenariosPath'));
|
||||
|
||||
await scenarioRunner.runManyScenarios(allScenarios);
|
||||
}
|
||||
}
|
50
packages/@n8n/benchmark/src/config/config.ts
Normal file
50
packages/@n8n/benchmark/src/config/config.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import convict from 'convict';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const configSchema = {
|
||||
testScenariosPath: {
|
||||
doc: 'The path to the scenarios',
|
||||
format: String,
|
||||
default: 'scenarios',
|
||||
},
|
||||
n8n: {
|
||||
baseUrl: {
|
||||
doc: 'The base URL for the n8n instance',
|
||||
format: String,
|
||||
default: 'http://localhost:5678',
|
||||
env: 'N8N_BASE_URL',
|
||||
},
|
||||
user: {
|
||||
email: {
|
||||
doc: 'The email address of the n8n user',
|
||||
format: String,
|
||||
default: 'benchmark-user@n8n.io',
|
||||
env: 'N8N_USER_EMAIL',
|
||||
},
|
||||
password: {
|
||||
doc: 'The password of the n8n user',
|
||||
format: String,
|
||||
default: 'VerySecret!123',
|
||||
env: 'N8N_USER_PASSWORD',
|
||||
},
|
||||
},
|
||||
},
|
||||
k6ExecutablePath: {
|
||||
doc: 'The path to the k6 binary',
|
||||
format: String,
|
||||
default: 'k6',
|
||||
env: 'K6_PATH',
|
||||
},
|
||||
};
|
||||
|
||||
export type Config = ReturnType<typeof loadConfig>;
|
||||
|
||||
export function loadConfig() {
|
||||
const config = convict(configSchema);
|
||||
|
||||
config.validate({ allowed: 'strict' });
|
||||
|
||||
return config;
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
import { strict as assert } from 'node:assert';
|
||||
import { N8nApiClient } from './n8nApiClient';
|
||||
import { AxiosRequestConfig } from 'axios';
|
||||
|
||||
export class AuthenticatedN8nApiClient extends N8nApiClient {
|
||||
constructor(
|
||||
apiBaseUrl: string,
|
||||
private readonly authCookie: string,
|
||||
) {
|
||||
super(apiBaseUrl);
|
||||
}
|
||||
|
||||
static async createUsingUsernameAndPassword(
|
||||
apiClient: N8nApiClient,
|
||||
loginDetails: {
|
||||
email: string;
|
||||
password: string;
|
||||
},
|
||||
) {
|
||||
const response = await apiClient.restApiRequest('/login', {
|
||||
method: 'POST',
|
||||
data: loginDetails,
|
||||
});
|
||||
|
||||
const cookieHeader = response.headers['set-cookie'];
|
||||
const authCookie = Array.isArray(cookieHeader) ? cookieHeader.join('; ') : cookieHeader;
|
||||
assert(authCookie);
|
||||
|
||||
return new AuthenticatedN8nApiClient(apiClient.apiBaseUrl, authCookie);
|
||||
}
|
||||
|
||||
async get<T>(endpoint: string) {
|
||||
return await this.authenticatedRequest<T>(endpoint, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
async post<T>(endpoint: string, data: unknown) {
|
||||
return await this.authenticatedRequest<T>(endpoint, {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
async patch<T>(endpoint: string, data: unknown) {
|
||||
return await this.authenticatedRequest<T>(endpoint, {
|
||||
method: 'PATCH',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
async delete<T>(endpoint: string) {
|
||||
return await this.authenticatedRequest<T>(endpoint, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
protected async authenticatedRequest<T>(endpoint: string, init: Omit<AxiosRequestConfig, 'url'>) {
|
||||
return await this.restApiRequest<T>(endpoint, {
|
||||
...init,
|
||||
headers: {
|
||||
...init.headers,
|
||||
cookie: this.authCookie,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
78
packages/@n8n/benchmark/src/n8nApiClient/n8nApiClient.ts
Normal file
78
packages/@n8n/benchmark/src/n8nApiClient/n8nApiClient.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
import axios, { AxiosError, AxiosRequestConfig } from 'axios';
|
||||
|
||||
export class N8nApiClient {
|
||||
constructor(public readonly apiBaseUrl: string) {}
|
||||
|
||||
async waitForInstanceToBecomeOnline(): Promise<void> {
|
||||
const HEALTH_ENDPOINT = 'healthz';
|
||||
const START_TIME = Date.now();
|
||||
const INTERVAL_MS = 1000;
|
||||
const TIMEOUT_MS = 60_000;
|
||||
|
||||
while (Date.now() - START_TIME < TIMEOUT_MS) {
|
||||
try {
|
||||
const response = await axios.request({
|
||||
url: `${this.apiBaseUrl}/${HEALTH_ENDPOINT}`,
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (response.status === 200 && response.data.status === 'ok') {
|
||||
return;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
console.log(`n8n instance not online yet, retrying in ${INTERVAL_MS / 1000} seconds...`);
|
||||
await this.delay(INTERVAL_MS);
|
||||
}
|
||||
|
||||
throw new Error(`n8n instance did not come online within ${TIMEOUT_MS / 1000} seconds`);
|
||||
}
|
||||
|
||||
async setupOwnerIfNeeded(loginDetails: { email: string; password: string }) {
|
||||
const response = await this.restApiRequest<{ message: string }>('/owner/setup', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
email: loginDetails.email,
|
||||
password: loginDetails.password,
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
},
|
||||
// Don't throw on non-2xx responses
|
||||
validateStatus: () => true,
|
||||
});
|
||||
|
||||
const responsePayload = response.data;
|
||||
|
||||
if (response.status === 200) {
|
||||
console.log('Owner setup successful');
|
||||
} else if (response.status === 400) {
|
||||
if (responsePayload.message === 'Instance owner already setup')
|
||||
console.log('Owner already set up');
|
||||
} else {
|
||||
throw new Error(
|
||||
`Owner setup failed with status ${response.status}: ${responsePayload.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async restApiRequest<T>(endpoint: string, init: Omit<AxiosRequestConfig, 'url'>) {
|
||||
try {
|
||||
return await axios.request<T>({
|
||||
...init,
|
||||
url: this.getRestEndpointUrl(endpoint),
|
||||
});
|
||||
} catch (e) {
|
||||
const error = e as AxiosError;
|
||||
console.error(`[ERROR] Request failed ${init.method} ${endpoint}`, error?.response?.data);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
protected getRestEndpointUrl(endpoint: string) {
|
||||
return `${this.apiBaseUrl}/rest${endpoint}`;
|
||||
}
|
||||
|
||||
private delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* n8n workflow. This is a simplified version of the actual workflow object.
|
||||
*/
|
||||
export type Workflow = {
|
||||
id: string;
|
||||
name: string;
|
||||
tags?: string[];
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
import { Workflow } from '@/n8nApiClient/n8nApiClient.types';
|
||||
import { AuthenticatedN8nApiClient } from './authenticatedN8nApiClient';
|
||||
|
||||
export class WorkflowApiClient {
|
||||
constructor(private readonly apiClient: AuthenticatedN8nApiClient) {}
|
||||
|
||||
async getAllWorkflows(): Promise<Workflow[]> {
|
||||
const response = await this.apiClient.get<{ count: number; data: Workflow[] }>('/workflows');
|
||||
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
async createWorkflow(workflow: unknown): Promise<Workflow> {
|
||||
const response = await this.apiClient.post<{ data: Workflow }>('/workflows', workflow);
|
||||
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
async activateWorkflow(workflow: Workflow): Promise<Workflow> {
|
||||
const response = await this.apiClient.patch<{ data: Workflow }>(`/workflows/${workflow.id}`, {
|
||||
...workflow,
|
||||
active: true,
|
||||
});
|
||||
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
async deleteWorkflow(workflowId: Workflow['id']): Promise<void> {
|
||||
await this.apiClient.delete(`/workflows/${workflowId}`);
|
||||
}
|
||||
}
|
35
packages/@n8n/benchmark/src/scenario/scenarioDataLoader.ts
Normal file
35
packages/@n8n/benchmark/src/scenario/scenarioDataLoader.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { Scenario } from '@/types/scenario';
|
||||
import { Workflow } from '@/n8nApiClient/n8nApiClient.types';
|
||||
|
||||
/**
|
||||
* Loads scenario data files from FS
|
||||
*/
|
||||
export class ScenarioDataFileLoader {
|
||||
async loadDataForScenario(scenario: Scenario): Promise<{
|
||||
workflows: Workflow[];
|
||||
}> {
|
||||
const workflows = await Promise.all(
|
||||
scenario.scenarioData.workflowFiles?.map((workflowFilePath) =>
|
||||
this.loadSingleWorkflowFromFile(path.join(scenario.scenarioDirPath, workflowFilePath)),
|
||||
) ?? [],
|
||||
);
|
||||
|
||||
return {
|
||||
workflows,
|
||||
};
|
||||
}
|
||||
|
||||
private loadSingleWorkflowFromFile(workflowFilePath: string): Workflow {
|
||||
const fileContent = fs.readFileSync(workflowFilePath, 'utf8');
|
||||
|
||||
try {
|
||||
return JSON.parse(fileContent);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to parse workflow file ${workflowFilePath}: ${error instanceof Error ? error.message : error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
67
packages/@n8n/benchmark/src/scenario/scenarioLoader.ts
Normal file
67
packages/@n8n/benchmark/src/scenario/scenarioLoader.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
import * as fs from 'node:fs';
|
||||
import * as path from 'path';
|
||||
import { createHash } from 'node:crypto';
|
||||
import type { Scenario, ScenarioManifest } from '@/types/scenario';
|
||||
|
||||
export class ScenarioLoader {
|
||||
/**
|
||||
* Loads all scenarios from the given path
|
||||
*/
|
||||
loadAll(pathToScenarios: string): Scenario[] {
|
||||
pathToScenarios = path.resolve(pathToScenarios);
|
||||
const scenarioFolders = fs
|
||||
.readdirSync(pathToScenarios, { withFileTypes: true })
|
||||
.filter((dirent) => dirent.isDirectory())
|
||||
.map((dirent) => dirent.name);
|
||||
|
||||
const scenarios: Scenario[] = [];
|
||||
|
||||
for (const folder of scenarioFolders) {
|
||||
const scenarioPath = path.join(pathToScenarios, folder);
|
||||
const manifestFileName = `${folder}.manifest.json`;
|
||||
const scenarioManifestPath = path.join(pathToScenarios, folder, manifestFileName);
|
||||
if (!fs.existsSync(scenarioManifestPath)) {
|
||||
console.warn(`Scenario at ${scenarioPath} is missing the ${manifestFileName} file`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Load the scenario manifest file
|
||||
const [scenario, validationErrors] =
|
||||
this.loadAndValidateScenarioManifest(scenarioManifestPath);
|
||||
if (validationErrors) {
|
||||
console.warn(
|
||||
`Scenario at ${scenarioPath} has the following validation errors: ${validationErrors.join(', ')}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
scenarios.push({
|
||||
...scenario,
|
||||
id: this.formScenarioId(scenarioPath),
|
||||
scenarioDirPath: scenarioPath,
|
||||
});
|
||||
}
|
||||
|
||||
return scenarios;
|
||||
}
|
||||
|
||||
private loadAndValidateScenarioManifest(
|
||||
scenarioManifestPath: string,
|
||||
): [ScenarioManifest, null] | [null, string[]] {
|
||||
const scenario = JSON.parse(fs.readFileSync(scenarioManifestPath, 'utf8'));
|
||||
const validationErrors: string[] = [];
|
||||
|
||||
if (!scenario.name) {
|
||||
validationErrors.push(`Scenario at ${scenarioManifestPath} is missing a name`);
|
||||
}
|
||||
if (!scenario.description) {
|
||||
validationErrors.push(`Scenario at ${scenarioManifestPath} is missing a description`);
|
||||
}
|
||||
|
||||
return validationErrors.length === 0 ? [scenario, null] : [null, validationErrors];
|
||||
}
|
||||
|
||||
private formScenarioId(scenarioPath: string): string {
|
||||
return createHash('sha256').update(scenarioPath).digest('hex');
|
||||
}
|
||||
}
|
28
packages/@n8n/benchmark/src/testExecution/k6Executor.ts
Normal file
28
packages/@n8n/benchmark/src/testExecution/k6Executor.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { $ } from 'zx';
|
||||
import { Scenario } from '@/types/scenario';
|
||||
|
||||
/**
|
||||
* Executes test scenarios using k6
|
||||
*/
|
||||
export class K6Executor {
|
||||
constructor(
|
||||
private readonly k6ExecutablePath: string,
|
||||
private readonly n8nApiBaseUrl: string,
|
||||
) {}
|
||||
|
||||
async executeTestScenario(scenario: Scenario) {
|
||||
// For 1 min with 5 virtual users
|
||||
const stage = '1m:5';
|
||||
|
||||
const processPromise = $({
|
||||
cwd: scenario.scenarioDirPath,
|
||||
env: {
|
||||
API_BASE_URL: this.n8nApiBaseUrl,
|
||||
},
|
||||
})`${this.k6ExecutablePath} run --quiet --stage ${stage} ${scenario.scriptPath}`;
|
||||
|
||||
for await (const chunk of processPromise.stdout) {
|
||||
console.log(chunk.toString());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
import { AuthenticatedN8nApiClient } from '@/n8nApiClient/authenticatedN8nApiClient';
|
||||
import { Workflow } from '@/n8nApiClient/n8nApiClient.types';
|
||||
import { WorkflowApiClient } from '@/n8nApiClient/workflowsApiClient';
|
||||
|
||||
/**
|
||||
* Imports scenario data into an n8n instance
|
||||
*/
|
||||
export class ScenarioDataImporter {
|
||||
private readonly workflowApiClient: WorkflowApiClient;
|
||||
|
||||
constructor(n8nApiClient: AuthenticatedN8nApiClient) {
|
||||
this.workflowApiClient = new WorkflowApiClient(n8nApiClient);
|
||||
}
|
||||
|
||||
async importTestScenarioData(workflows: Workflow[]) {
|
||||
const existingWorkflows = await this.workflowApiClient.getAllWorkflows();
|
||||
|
||||
for (const workflow of workflows) {
|
||||
await this.importWorkflow({ existingWorkflows, workflow });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a single workflow into n8n removing any existing workflows with the same name
|
||||
*/
|
||||
private async importWorkflow(opts: { existingWorkflows: Workflow[]; workflow: Workflow }) {
|
||||
const existingWorkflows = this.findExistingWorkflows(opts.existingWorkflows, opts.workflow);
|
||||
if (existingWorkflows.length > 0) {
|
||||
for (const toDelete of existingWorkflows) {
|
||||
await this.workflowApiClient.deleteWorkflow(toDelete.id);
|
||||
}
|
||||
}
|
||||
|
||||
const createdWorkflow = await this.workflowApiClient.createWorkflow({
|
||||
...opts.workflow,
|
||||
name: this.getBenchmarkWorkflowName(opts.workflow),
|
||||
});
|
||||
|
||||
return await this.workflowApiClient.activateWorkflow(createdWorkflow);
|
||||
}
|
||||
|
||||
private findExistingWorkflows(
|
||||
existingWorkflows: Workflow[],
|
||||
workflowToImport: Workflow,
|
||||
): Workflow[] {
|
||||
const benchmarkWorkflowName = this.getBenchmarkWorkflowName(workflowToImport);
|
||||
|
||||
return existingWorkflows.filter(
|
||||
(existingWorkflow) => existingWorkflow.name === benchmarkWorkflowName,
|
||||
);
|
||||
}
|
||||
|
||||
private getBenchmarkWorkflowName(workflow: Workflow) {
|
||||
return `[BENCHMARK] ${workflow.name}`;
|
||||
}
|
||||
}
|
50
packages/@n8n/benchmark/src/testExecution/scenarioRunner.ts
Normal file
50
packages/@n8n/benchmark/src/testExecution/scenarioRunner.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { Scenario } from '@/types/scenario';
|
||||
import { N8nApiClient } from '@/n8nApiClient/n8nApiClient';
|
||||
import { ScenarioDataFileLoader } from '@/scenario/scenarioDataLoader';
|
||||
import { K6Executor } from './k6Executor';
|
||||
import { ScenarioDataImporter } from '@/testExecution/scenarioDataImporter';
|
||||
import { AuthenticatedN8nApiClient } from '@/n8nApiClient/authenticatedN8nApiClient';
|
||||
|
||||
/**
|
||||
* Runs scenarios
|
||||
*/
|
||||
export class ScenarioRunner {
|
||||
constructor(
|
||||
private readonly n8nClient: N8nApiClient,
|
||||
private readonly dataLoader: ScenarioDataFileLoader,
|
||||
private readonly k6Executor: K6Executor,
|
||||
private readonly ownerConfig: {
|
||||
email: string;
|
||||
password: string;
|
||||
},
|
||||
) {}
|
||||
|
||||
async runManyScenarios(scenarios: Scenario[]) {
|
||||
console.log(`Waiting for n8n ${this.n8nClient.apiBaseUrl} to become online`);
|
||||
await this.n8nClient.waitForInstanceToBecomeOnline();
|
||||
|
||||
console.log('Setting up owner');
|
||||
await this.n8nClient.setupOwnerIfNeeded(this.ownerConfig);
|
||||
|
||||
const authenticatedN8nClient = await AuthenticatedN8nApiClient.createUsingUsernameAndPassword(
|
||||
this.n8nClient,
|
||||
this.ownerConfig,
|
||||
);
|
||||
const testDataImporter = new ScenarioDataImporter(authenticatedN8nClient);
|
||||
|
||||
for (const scenario of scenarios) {
|
||||
await this.runSingleTestScenario(testDataImporter, scenario);
|
||||
}
|
||||
}
|
||||
|
||||
private async runSingleTestScenario(testDataImporter: ScenarioDataImporter, scenario: Scenario) {
|
||||
console.log('Running scenario:', scenario.name);
|
||||
|
||||
console.log('Loading and importing data');
|
||||
const testData = await this.dataLoader.loadDataForScenario(scenario);
|
||||
await testDataImporter.importTestScenarioData(testData.workflows);
|
||||
|
||||
console.log('Executing scenario script');
|
||||
await this.k6Executor.executeTestScenario(scenario);
|
||||
}
|
||||
}
|
27
packages/@n8n/benchmark/src/types/scenario.ts
Normal file
27
packages/@n8n/benchmark/src/types/scenario.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
export type ScenarioData = {
|
||||
/** Relative paths to the workflow files */
|
||||
workflowFiles?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Configuration that defines the benchmark scenario
|
||||
*/
|
||||
export type ScenarioManifest = {
|
||||
/** The name of the scenario */
|
||||
name: string;
|
||||
/** A longer description of the scenario */
|
||||
description: string;
|
||||
/** Relative path to the k6 script */
|
||||
scriptPath: string;
|
||||
/** Data to import before running the scenario */
|
||||
scenarioData: ScenarioData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Scenario with additional metadata
|
||||
*/
|
||||
export type Scenario = ScenarioManifest & {
|
||||
id: string;
|
||||
/** Path to the directory containing the scenario */
|
||||
scenarioDirPath: string;
|
||||
};
|
9
packages/@n8n/benchmark/tsconfig.build.json
Normal file
9
packages/@n8n/benchmark/tsconfig.build.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": ["./tsconfig.json", "../../../tsconfig.build.json"],
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"tsBuildInfoFile": "dist/build.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
11
packages/@n8n/benchmark/tsconfig.json
Normal file
11
packages/@n8n/benchmark/tsconfig.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": ["../../../tsconfig.json", "../../../tsconfig.backend.json"],
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"baseUrl": "src",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
|
@ -260,10 +260,5 @@ body,
|
|||
```
|
||||
|
||||
## License
|
||||
n8n Chat is [fair-code](https://faircode.io) distributed under the
|
||||
[**Sustainable Use License**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md).
|
||||
|
||||
Proprietary licenses are available for enterprise customers. [Get in touch](mailto:license@n8n.io)
|
||||
|
||||
Additional information about the license model can be found in the
|
||||
[docs](https://docs.n8n.io/reference/license/).
|
||||
You can find the license information [here](https://github.com/n8n-io/n8n/blob/master/README.md#license)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/chat",
|
||||
"version": "0.21.0",
|
||||
"version": "0.24.0",
|
||||
"scripts": {
|
||||
"dev": "pnpm run storybook",
|
||||
"build": "pnpm build:vite && pnpm build:bundle",
|
||||
|
@ -38,16 +38,19 @@
|
|||
"@vueuse/core": "^10.11.0",
|
||||
"highlight.js": "^11.8.0",
|
||||
"markdown-it-link-attributes": "^4.0.1",
|
||||
"uuid": "^8.3.2",
|
||||
"vue": "^3.4.21",
|
||||
"vue-markdown-render": "^2.1.1"
|
||||
"uuid": "catalog:",
|
||||
"vue": "catalog:frontend",
|
||||
"vue-markdown-render": "catalog:frontend"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/mdi": "^1.1.54",
|
||||
"@n8n/storybook": "workspace:*",
|
||||
"@types/markdown-it": "^12.2.3",
|
||||
"@vitest/coverage-v8": "catalog:frontend",
|
||||
"unplugin-icons": "^0.19.0",
|
||||
"vite-plugin-dts": "^3.9.1"
|
||||
"vite": "catalog:frontend",
|
||||
"vitest": "catalog:frontend",
|
||||
"vite-plugin-dts": "^3.9.1",
|
||||
"vue-tsc": "catalog:frontend"
|
||||
},
|
||||
"files": [
|
||||
"README.md",
|
||||
|
|
|
@ -247,6 +247,10 @@ function onOpenFileDialog() {
|
|||
justify-content: center;
|
||||
transition: color var(--chat--transition-duration) ease;
|
||||
|
||||
svg {
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: var(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/client-oauth2",
|
||||
"version": "0.19.0",
|
||||
"version": "0.20.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
@ -20,6 +20,6 @@
|
|||
"dist/**/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"axios": "1.6.7"
|
||||
"axios": "catalog:"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,4 +7,13 @@ module.exports = {
|
|||
extends: ['@n8n_io/eslint-config/node'],
|
||||
|
||||
...sharedOptions(__dirname),
|
||||
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.config.ts'],
|
||||
rules: {
|
||||
'n8n-local-rules/no-untyped-config-class-field': 'error',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/config",
|
||||
"version": "1.2.0",
|
||||
"version": "1.6.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
@ -21,6 +21,6 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"reflect-metadata": "0.2.2",
|
||||
"typedi": "0.10.0"
|
||||
"typedi": "catalog:"
|
||||
}
|
||||
}
|
||||
|
|
36
packages/@n8n/config/src/configs/cache.config.ts
Normal file
36
packages/@n8n/config/src/configs/cache.config.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { Config, Env, Nested } from '../decorators';
|
||||
|
||||
@Config
|
||||
class MemoryConfig {
|
||||
/** Max size of memory cache in bytes */
|
||||
@Env('N8N_CACHE_MEMORY_MAX_SIZE')
|
||||
maxSize: number = 3 * 1024 * 1024; // 3 MiB
|
||||
|
||||
/** Time to live (in milliseconds) for data cached in memory. */
|
||||
@Env('N8N_CACHE_MEMORY_TTL')
|
||||
ttl: number = 3600 * 1000; // 1 hour
|
||||
}
|
||||
|
||||
@Config
|
||||
class RedisConfig {
|
||||
/** Prefix for cache keys in Redis. */
|
||||
@Env('N8N_CACHE_REDIS_KEY_PREFIX')
|
||||
prefix: string = 'redis';
|
||||
|
||||
/** Time to live (in milliseconds) for data cached in Redis. 0 for no TTL. */
|
||||
@Env('N8N_CACHE_REDIS_TTL')
|
||||
ttl: number = 3600 * 1000; // 1 hour
|
||||
}
|
||||
|
||||
@Config
|
||||
export class CacheConfig {
|
||||
/** Backend to use for caching. */
|
||||
@Env('N8N_CACHE_BACKEND')
|
||||
backend: 'memory' | 'redis' | 'auto' = 'auto';
|
||||
|
||||
@Nested
|
||||
memory: MemoryConfig;
|
||||
|
||||
@Nested
|
||||
redis: RedisConfig;
|
||||
}
|
|
@ -7,19 +7,19 @@ class CredentialsOverwrite {
|
|||
* Format: { CREDENTIAL_NAME: { PARAMETER: VALUE }}
|
||||
*/
|
||||
@Env('CREDENTIALS_OVERWRITE_DATA')
|
||||
readonly data: string = '{}';
|
||||
data: string = '{}';
|
||||
|
||||
/** Internal API endpoint to fetch overwritten credential types from. */
|
||||
@Env('CREDENTIALS_OVERWRITE_ENDPOINT')
|
||||
readonly endpoint: string = '';
|
||||
endpoint: string = '';
|
||||
}
|
||||
|
||||
@Config
|
||||
export class CredentialsConfig {
|
||||
/** Default name for credentials */
|
||||
@Env('CREDENTIALS_DEFAULT_NAME')
|
||||
readonly defaultName: string = 'My credentials';
|
||||
defaultName: string = 'My credentials';
|
||||
|
||||
@Nested
|
||||
readonly overwrite: CredentialsOverwrite;
|
||||
overwrite: CredentialsOverwrite;
|
||||
}
|
|
@ -4,19 +4,19 @@ import { Config, Env, Nested } from '../decorators';
|
|||
class LoggingConfig {
|
||||
/** Whether database logging is enabled. */
|
||||
@Env('DB_LOGGING_ENABLED')
|
||||
readonly enabled: boolean = false;
|
||||
enabled: boolean = false;
|
||||
|
||||
/**
|
||||
* Database logging level. Requires `DB_LOGGING_MAX_EXECUTION_TIME` to be higher than `0`.
|
||||
*/
|
||||
@Env('DB_LOGGING_OPTIONS')
|
||||
readonly options: 'query' | 'error' | 'schema' | 'warn' | 'info' | 'log' | 'all' = 'error';
|
||||
options: 'query' | 'error' | 'schema' | 'warn' | 'info' | 'log' | 'all' = 'error';
|
||||
|
||||
/**
|
||||
* Only queries that exceed this time (ms) will be logged. Set `0` to disable.
|
||||
*/
|
||||
@Env('DB_LOGGING_MAX_EXECUTION_TIME')
|
||||
readonly maxQueryExecutionTime: number = 0;
|
||||
maxQueryExecutionTime: number = 0;
|
||||
}
|
||||
|
||||
@Config
|
||||
|
@ -26,23 +26,23 @@ class PostgresSSLConfig {
|
|||
* If `DB_POSTGRESDB_SSL_CA`, `DB_POSTGRESDB_SSL_CERT`, or `DB_POSTGRESDB_SSL_KEY` are defined, `DB_POSTGRESDB_SSL_ENABLED` defaults to `true`.
|
||||
*/
|
||||
@Env('DB_POSTGRESDB_SSL_ENABLED')
|
||||
readonly enabled: boolean = false;
|
||||
enabled: boolean = false;
|
||||
|
||||
/** SSL certificate authority */
|
||||
@Env('DB_POSTGRESDB_SSL_CA')
|
||||
readonly ca: string = '';
|
||||
ca: string = '';
|
||||
|
||||
/** SSL certificate */
|
||||
@Env('DB_POSTGRESDB_SSL_CERT')
|
||||
readonly cert: string = '';
|
||||
cert: string = '';
|
||||
|
||||
/** SSL key */
|
||||
@Env('DB_POSTGRESDB_SSL_KEY')
|
||||
readonly key: string = '';
|
||||
key: string = '';
|
||||
|
||||
/** If unauthorized SSL connections should be rejected */
|
||||
@Env('DB_POSTGRESDB_SSL_REJECT_UNAUTHORIZED')
|
||||
readonly rejectUnauthorized: boolean = true;
|
||||
rejectUnauthorized: boolean = true;
|
||||
}
|
||||
|
||||
@Config
|
||||
|
@ -53,30 +53,30 @@ class PostgresConfig {
|
|||
|
||||
/** Postgres database host */
|
||||
@Env('DB_POSTGRESDB_HOST')
|
||||
readonly host: string = 'localhost';
|
||||
host: string = 'localhost';
|
||||
|
||||
/** Postgres database password */
|
||||
@Env('DB_POSTGRESDB_PASSWORD')
|
||||
readonly password: string = '';
|
||||
password: string = '';
|
||||
|
||||
/** Postgres database port */
|
||||
@Env('DB_POSTGRESDB_PORT')
|
||||
readonly port: number = 5432;
|
||||
port: number = 5432;
|
||||
|
||||
/** Postgres database user */
|
||||
@Env('DB_POSTGRESDB_USER')
|
||||
readonly user: string = 'postgres';
|
||||
user: string = 'postgres';
|
||||
|
||||
/** Postgres database schema */
|
||||
@Env('DB_POSTGRESDB_SCHEMA')
|
||||
readonly schema: string = 'public';
|
||||
schema: string = 'public';
|
||||
|
||||
/** Postgres database pool size */
|
||||
@Env('DB_POSTGRESDB_POOL_SIZE')
|
||||
readonly poolSize = 2;
|
||||
poolSize: number = 2;
|
||||
|
||||
@Nested
|
||||
readonly ssl: PostgresSSLConfig;
|
||||
ssl: PostgresSSLConfig;
|
||||
}
|
||||
|
||||
@Config
|
||||
|
@ -87,36 +87,36 @@ class MysqlConfig {
|
|||
|
||||
/** MySQL database host */
|
||||
@Env('DB_MYSQLDB_HOST')
|
||||
readonly host: string = 'localhost';
|
||||
host: string = 'localhost';
|
||||
|
||||
/** MySQL database password */
|
||||
@Env('DB_MYSQLDB_PASSWORD')
|
||||
readonly password: string = '';
|
||||
password: string = '';
|
||||
|
||||
/** MySQL database port */
|
||||
@Env('DB_MYSQLDB_PORT')
|
||||
readonly port: number = 3306;
|
||||
port: number = 3306;
|
||||
|
||||
/** MySQL database user */
|
||||
@Env('DB_MYSQLDB_USER')
|
||||
readonly user: string = 'root';
|
||||
user: string = 'root';
|
||||
}
|
||||
|
||||
@Config
|
||||
class SqliteConfig {
|
||||
/** SQLite database file name */
|
||||
@Env('DB_SQLITE_DATABASE')
|
||||
readonly database: string = 'database.sqlite';
|
||||
database: string = 'database.sqlite';
|
||||
|
||||
/** SQLite database pool size. Set to `0` to disable pooling. */
|
||||
@Env('DB_SQLITE_POOL_SIZE')
|
||||
readonly poolSize: number = 0;
|
||||
poolSize: number = 0;
|
||||
|
||||
/**
|
||||
* Enable SQLite WAL mode.
|
||||
*/
|
||||
@Env('DB_SQLITE_ENABLE_WAL')
|
||||
readonly enableWAL: boolean = this.poolSize > 1;
|
||||
enableWAL: boolean = this.poolSize > 1;
|
||||
|
||||
/**
|
||||
* Run `VACUUM` on startup to rebuild the database, reducing file size and optimizing indexes.
|
||||
|
@ -124,7 +124,7 @@ class SqliteConfig {
|
|||
* @warning Long-running blocking operation that will increase startup time.
|
||||
*/
|
||||
@Env('DB_SQLITE_VACUUM_ON_STARTUP')
|
||||
readonly executeVacuumOnStartup: boolean = false;
|
||||
executeVacuumOnStartup: boolean = false;
|
||||
}
|
||||
|
||||
@Config
|
||||
|
@ -135,17 +135,17 @@ export class DatabaseConfig {
|
|||
|
||||
/** Prefix for table names */
|
||||
@Env('DB_TABLE_PREFIX')
|
||||
readonly tablePrefix: string = '';
|
||||
tablePrefix: string = '';
|
||||
|
||||
@Nested
|
||||
readonly logging: LoggingConfig;
|
||||
logging: LoggingConfig;
|
||||
|
||||
@Nested
|
||||
readonly postgresdb: PostgresConfig;
|
||||
postgresdb: PostgresConfig;
|
||||
|
||||
@Nested
|
||||
readonly mysqldb: MysqlConfig;
|
||||
mysqldb: MysqlConfig;
|
||||
|
||||
@Nested
|
||||
readonly sqlite: SqliteConfig;
|
||||
sqlite: SqliteConfig;
|
||||
}
|
102
packages/@n8n/config/src/configs/endpoints.config.ts
Normal file
102
packages/@n8n/config/src/configs/endpoints.config.ts
Normal file
|
@ -0,0 +1,102 @@
|
|||
import { Config, Env, Nested } from '../decorators';
|
||||
|
||||
@Config
|
||||
class PrometheusMetricsConfig {
|
||||
/** Whether to enable the `/metrics` endpoint to expose Prometheus metrics. */
|
||||
@Env('N8N_METRICS')
|
||||
enable: boolean = false;
|
||||
|
||||
/** Prefix for Prometheus metric names. */
|
||||
@Env('N8N_METRICS_PREFIX')
|
||||
prefix: string = 'n8n_';
|
||||
|
||||
/** Whether to expose system and Node.js metrics. See: https://www.npmjs.com/package/prom-client */
|
||||
@Env('N8N_METRICS_INCLUDE_DEFAULT_METRICS')
|
||||
includeDefaultMetrics: boolean = true;
|
||||
|
||||
/** Whether to include a label for workflow ID on workflow metrics. */
|
||||
@Env('N8N_METRICS_INCLUDE_WORKFLOW_ID_LABEL')
|
||||
includeWorkflowIdLabel: boolean = false;
|
||||
|
||||
/** Whether to include a label for node type on node metrics. */
|
||||
@Env('N8N_METRICS_INCLUDE_NODE_TYPE_LABEL')
|
||||
includeNodeTypeLabel: boolean = false;
|
||||
|
||||
/** Whether to include a label for credential type on credential metrics. */
|
||||
@Env('N8N_METRICS_INCLUDE_CREDENTIAL_TYPE_LABEL')
|
||||
includeCredentialTypeLabel: boolean = false;
|
||||
|
||||
/** Whether to expose metrics for API endpoints. See: https://www.npmjs.com/package/express-prom-bundle */
|
||||
@Env('N8N_METRICS_INCLUDE_API_ENDPOINTS')
|
||||
includeApiEndpoints: boolean = false;
|
||||
|
||||
/** Whether to include a label for the path of API endpoint calls. */
|
||||
@Env('N8N_METRICS_INCLUDE_API_PATH_LABEL')
|
||||
includeApiPathLabel: boolean = false;
|
||||
|
||||
/** Whether to include a label for the HTTP method of API endpoint calls. */
|
||||
@Env('N8N_METRICS_INCLUDE_API_METHOD_LABEL')
|
||||
includeApiMethodLabel: boolean = false;
|
||||
|
||||
/** Whether to include a label for the status code of API endpoint calls. */
|
||||
@Env('N8N_METRICS_INCLUDE_API_STATUS_CODE_LABEL')
|
||||
includeApiStatusCodeLabel: boolean = false;
|
||||
|
||||
/** Whether to include metrics for cache hits and misses. */
|
||||
@Env('N8N_METRICS_INCLUDE_CACHE_METRICS')
|
||||
includeCacheMetrics: boolean = false;
|
||||
|
||||
/** Whether to include metrics derived from n8n's internal events */
|
||||
@Env('N8N_METRICS_INCLUDE_MESSAGE_EVENT_BUS_METRICS')
|
||||
includeMessageEventBusMetrics: boolean = false;
|
||||
}
|
||||
|
||||
@Config
|
||||
export class EndpointsConfig {
|
||||
/** Max payload size in MiB */
|
||||
@Env('N8N_PAYLOAD_SIZE_MAX')
|
||||
payloadSizeMax: number = 16;
|
||||
|
||||
@Nested
|
||||
metrics: PrometheusMetricsConfig;
|
||||
|
||||
/** Path segment for REST API endpoints. */
|
||||
@Env('N8N_ENDPOINT_REST')
|
||||
rest: string = 'rest';
|
||||
|
||||
/** Path segment for form endpoints. */
|
||||
@Env('N8N_ENDPOINT_FORM')
|
||||
form: string = 'form';
|
||||
|
||||
/** Path segment for test form endpoints. */
|
||||
@Env('N8N_ENDPOINT_FORM_TEST')
|
||||
formTest: string = 'form-test';
|
||||
|
||||
/** Path segment for waiting form endpoints. */
|
||||
@Env('N8N_ENDPOINT_FORM_WAIT')
|
||||
formWaiting: string = 'form-waiting';
|
||||
|
||||
/** Path segment for webhook endpoints. */
|
||||
@Env('N8N_ENDPOINT_WEBHOOK')
|
||||
webhook: string = 'webhook';
|
||||
|
||||
/** Path segment for test webhook endpoints. */
|
||||
@Env('N8N_ENDPOINT_WEBHOOK_TEST')
|
||||
webhookTest: string = 'webhook-test';
|
||||
|
||||
/** Path segment for waiting webhook endpoints. */
|
||||
@Env('N8N_ENDPOINT_WEBHOOK_WAIT')
|
||||
webhookWaiting: string = 'webhook-waiting';
|
||||
|
||||
/** Whether to disable n8n's UI (frontend). */
|
||||
@Env('N8N_DISABLE_UI')
|
||||
disableUi: boolean = false;
|
||||
|
||||
/** Whether to disable production webhooks on the main process, when using webhook-specific processes. */
|
||||
@Env('N8N_DISABLE_PRODUCTION_MAIN_PROCESS')
|
||||
disableProductionWebhooksOnMainProcess: boolean = false;
|
||||
|
||||
/** Colon-delimited list of additional endpoints to not open the UI on. */
|
||||
@Env('N8N_ADDITIONAL_NON_UI_ROUTES')
|
||||
additionalNonUIRoutes: string = '';
|
||||
}
|
|
@ -2,30 +2,30 @@ import { Config, Env, Nested } from '../decorators';
|
|||
|
||||
@Config
|
||||
class LogWriterConfig {
|
||||
/** Number of event log files to keep */
|
||||
/* of event log files to keep */
|
||||
@Env('N8N_EVENTBUS_LOGWRITER_KEEPLOGCOUNT')
|
||||
readonly keepLogCount: number = 3;
|
||||
keepLogCount: number = 3;
|
||||
|
||||
/** Max size (in KB) of an event log file before a new one is started */
|
||||
@Env('N8N_EVENTBUS_LOGWRITER_MAXFILESIZEINKB')
|
||||
readonly maxFileSizeInKB: number = 10240; // 10 MB
|
||||
maxFileSizeInKB: number = 10240; // 10 MB
|
||||
|
||||
/** Basename of event log file */
|
||||
@Env('N8N_EVENTBUS_LOGWRITER_LOGBASENAME')
|
||||
readonly logBaseName: string = 'n8nEventLog';
|
||||
logBaseName: string = 'n8nEventLog';
|
||||
}
|
||||
|
||||
@Config
|
||||
export class EventBusConfig {
|
||||
/** How often (in ms) to check for unsent event messages. Can in rare cases cause a message to be sent twice. `0` to disable */
|
||||
@Env('N8N_EVENTBUS_CHECKUNSENTINTERVAL')
|
||||
readonly checkUnsentInterval: number = 0;
|
||||
checkUnsentInterval: number = 0;
|
||||
|
||||
/** Endpoint to retrieve n8n version information from */
|
||||
@Nested
|
||||
readonly logWriter: LogWriterConfig;
|
||||
logWriter: LogWriterConfig;
|
||||
|
||||
/** Whether to recover execution details after a crash or only mark status executions as crashed. */
|
||||
@Env('N8N_EVENTBUS_RECOVERY_MODE')
|
||||
readonly crashRecoveryMode: 'simple' | 'extensive' = 'extensive';
|
||||
crashRecoveryMode: 'simple' | 'extensive' = 'extensive';
|
||||
}
|
|
@ -4,9 +4,9 @@ import { Config, Env } from '../decorators';
|
|||
export class ExternalSecretsConfig {
|
||||
/** How often (in seconds) to check for secret updates */
|
||||
@Env('N8N_EXTERNAL_SECRETS_UPDATE_INTERVAL')
|
||||
readonly updateInterval: number = 300;
|
||||
updateInterval: number = 300;
|
||||
|
||||
/** Whether to prefer GET over LIST when fetching secrets from Hashicorp Vault */
|
||||
@Env('N8N_EXTERNAL_SECRETS_PREFER_GET')
|
||||
readonly preferGet: boolean = false;
|
||||
preferGet: boolean = false;
|
||||
}
|
|
@ -4,39 +4,39 @@ import { Config, Env, Nested } from '../decorators';
|
|||
class S3BucketConfig {
|
||||
/** Name of the n8n bucket in S3-compatible external storage */
|
||||
@Env('N8N_EXTERNAL_STORAGE_S3_BUCKET_NAME')
|
||||
readonly name: string = '';
|
||||
name: string = '';
|
||||
|
||||
/** Region of the n8n bucket in S3-compatible external storage @example "us-east-1" */
|
||||
@Env('N8N_EXTERNAL_STORAGE_S3_BUCKET_REGION')
|
||||
readonly region: string = '';
|
||||
region: string = '';
|
||||
}
|
||||
|
||||
@Config
|
||||
class S3CredentialsConfig {
|
||||
/** Access key in S3-compatible external storage */
|
||||
@Env('N8N_EXTERNAL_STORAGE_S3_ACCESS_KEY')
|
||||
readonly accessKey: string = '';
|
||||
accessKey: string = '';
|
||||
|
||||
/** Access secret in S3-compatible external storage */
|
||||
@Env('N8N_EXTERNAL_STORAGE_S3_ACCESS_SECRET')
|
||||
readonly accessSecret: string = '';
|
||||
accessSecret: string = '';
|
||||
}
|
||||
|
||||
@Config
|
||||
class S3Config {
|
||||
/** Host of the n8n bucket in S3-compatible external storage @example "s3.us-east-1.amazonaws.com" */
|
||||
@Env('N8N_EXTERNAL_STORAGE_S3_HOST')
|
||||
readonly host: string = '';
|
||||
host: string = '';
|
||||
|
||||
@Nested
|
||||
readonly bucket: S3BucketConfig;
|
||||
bucket: S3BucketConfig;
|
||||
|
||||
@Nested
|
||||
readonly credentials: S3CredentialsConfig;
|
||||
credentials: S3CredentialsConfig;
|
||||
}
|
||||
|
||||
@Config
|
||||
export class ExternalStorageConfig {
|
||||
@Nested
|
||||
readonly s3: S3Config;
|
||||
s3: S3Config;
|
||||
}
|
|
@ -25,22 +25,30 @@ 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;
|
||||
}
|
||||
|
||||
@Config
|
||||
export class NodesConfig {
|
||||
/** Node types to load. Includes all if unspecified. @example '["n8n-nodes-base.hackerNews"]' */
|
||||
@Env('NODES_INCLUDE')
|
||||
readonly include: JsonStringArray = [];
|
||||
include: JsonStringArray = [];
|
||||
|
||||
/** Node types not to load. Excludes none if unspecified. @example '["n8n-nodes-base.hackerNews"]' */
|
||||
@Env('NODES_EXCLUDE')
|
||||
readonly exclude: JsonStringArray = [];
|
||||
exclude: JsonStringArray = [];
|
||||
|
||||
/** Node type to use as error trigger */
|
||||
@Env('NODES_ERROR_TRIGGER_TYPE')
|
||||
readonly errorTriggerType: string = 'n8n-nodes-base.errorTrigger';
|
||||
errorTriggerType: string = 'n8n-nodes-base.errorTrigger';
|
||||
|
||||
@Nested
|
||||
readonly communityPackages: CommunityPackagesConfig;
|
||||
communityPackages: CommunityPackagesConfig;
|
||||
}
|
|
@ -4,13 +4,13 @@ import { Config, Env } from '../decorators';
|
|||
export class PublicApiConfig {
|
||||
/** Whether to disable the Public API */
|
||||
@Env('N8N_PUBLIC_API_DISABLED')
|
||||
readonly disabled: boolean = false;
|
||||
disabled: boolean = false;
|
||||
|
||||
/** Path segment for the Public API */
|
||||
@Env('N8N_PUBLIC_API_ENDPOINT')
|
||||
readonly path: string = 'api';
|
||||
path: string = 'api';
|
||||
|
||||
/** Whether to disable the Swagger UI for the Public API */
|
||||
@Env('N8N_PUBLIC_API_SWAGGERUI_DISABLED')
|
||||
readonly swaggerUiDisabled: boolean = false;
|
||||
swaggerUiDisabled: boolean = false;
|
||||
}
|
96
packages/@n8n/config/src/configs/scaling-mode.config.ts
Normal file
96
packages/@n8n/config/src/configs/scaling-mode.config.ts
Normal file
|
@ -0,0 +1,96 @@
|
|||
import { Config, Env, Nested } from '../decorators';
|
||||
|
||||
@Config
|
||||
class HealthConfig {
|
||||
/** Whether to enable the worker health check endpoint `/healthz`. */
|
||||
@Env('QUEUE_HEALTH_CHECK_ACTIVE')
|
||||
active: boolean = false;
|
||||
|
||||
/** Port for worker to respond to health checks requests on, if enabled. */
|
||||
@Env('QUEUE_HEALTH_CHECK_PORT')
|
||||
port: number = 5678;
|
||||
}
|
||||
|
||||
@Config
|
||||
class RedisConfig {
|
||||
/** Redis database for Bull queue. */
|
||||
@Env('QUEUE_BULL_REDIS_DB')
|
||||
db: number = 0;
|
||||
|
||||
/** Redis host for Bull queue. */
|
||||
@Env('QUEUE_BULL_REDIS_HOST')
|
||||
host: string = 'localhost';
|
||||
|
||||
/** Password to authenticate with Redis. */
|
||||
@Env('QUEUE_BULL_REDIS_PASSWORD')
|
||||
password: string = '';
|
||||
|
||||
/** Port for Redis to listen on. */
|
||||
@Env('QUEUE_BULL_REDIS_PORT')
|
||||
port: number = 6379;
|
||||
|
||||
/** Max cumulative timeout (in milliseconds) of connection retries before process exit. */
|
||||
@Env('QUEUE_BULL_REDIS_TIMEOUT_THRESHOLD')
|
||||
timeoutThreshold: number = 10_000;
|
||||
|
||||
/** Redis username. Redis 6.0 or higher required. */
|
||||
@Env('QUEUE_BULL_REDIS_USERNAME')
|
||||
username: string = '';
|
||||
|
||||
/** Redis cluster startup nodes, as comma-separated list of `{host}:{port}` pairs. @example 'redis-1:6379,redis-2:6379' */
|
||||
@Env('QUEUE_BULL_REDIS_CLUSTER_NODES')
|
||||
clusterNodes: string = '';
|
||||
|
||||
/** Whether to enable TLS on Redis connections. */
|
||||
@Env('QUEUE_BULL_REDIS_TLS')
|
||||
tls: boolean = false;
|
||||
}
|
||||
|
||||
@Config
|
||||
class SettingsConfig {
|
||||
/** How long (in milliseconds) is the lease period for a worker processing a job. */
|
||||
@Env('QUEUE_WORKER_LOCK_DURATION')
|
||||
lockDuration: number = 30_000;
|
||||
|
||||
/** How often (in milliseconds) a worker must renew the lease. */
|
||||
@Env('QUEUE_WORKER_LOCK_RENEW_TIME')
|
||||
lockRenewTime: number = 15_000;
|
||||
|
||||
/** How often (in milliseconds) Bull must check for stalled jobs. `0` to disable. */
|
||||
@Env('QUEUE_WORKER_STALLED_INTERVAL')
|
||||
stalledInterval: number = 30_000;
|
||||
|
||||
/** Max number of times a stalled job will be re-processed. See Bull's [documentation](https://docs.bullmq.io/guide/workers/stalled-jobs). */
|
||||
@Env('QUEUE_WORKER_MAX_STALLED_COUNT')
|
||||
maxStalledCount: number = 1;
|
||||
}
|
||||
|
||||
@Config
|
||||
class BullConfig {
|
||||
/** Prefix for Bull keys on Redis. @example 'bull:jobs:23' */
|
||||
@Env('QUEUE_BULL_PREFIX')
|
||||
prefix: string = 'bull';
|
||||
|
||||
@Nested
|
||||
redis: RedisConfig;
|
||||
|
||||
/** How often (in seconds) to poll the Bull queue to identify executions finished during a Redis crash. `0` to disable. May increase Redis traffic significantly. */
|
||||
@Env('QUEUE_RECOVERY_INTERVAL')
|
||||
queueRecoveryInterval: number = 60; // watchdog interval
|
||||
|
||||
/** @deprecated How long (in seconds) a worker must wait for active executions to finish before exiting. Use `N8N_GRACEFUL_SHUTDOWN_TIMEOUT` instead */
|
||||
@Env('QUEUE_WORKER_TIMEOUT')
|
||||
gracefulShutdownTimeout: number = 30;
|
||||
|
||||
@Nested
|
||||
settings: SettingsConfig;
|
||||
}
|
||||
|
||||
@Config
|
||||
export class ScalingModeConfig {
|
||||
@Nested
|
||||
health: HealthConfig;
|
||||
|
||||
@Nested
|
||||
bull: BullConfig;
|
||||
}
|
|
@ -4,9 +4,9 @@ import { Config, Env } from '../decorators';
|
|||
export class TemplatesConfig {
|
||||
/** Whether to load workflow templates. */
|
||||
@Env('N8N_TEMPLATES_ENABLED')
|
||||
readonly enabled: boolean = true;
|
||||
enabled: boolean = true;
|
||||
|
||||
/** Host to retrieve workflow templates from endpoints. */
|
||||
@Env('N8N_TEMPLATES_HOST')
|
||||
readonly host: string = 'https://api.n8n.io/api/';
|
||||
host: string = 'https://api.n8n.io/api/';
|
||||
}
|
|
@ -1,78 +1,84 @@
|
|||
import { Config, Env, Nested } from '../decorators';
|
||||
|
||||
@Config
|
||||
export class SmtpAuth {
|
||||
class SmtpAuth {
|
||||
/** SMTP login username */
|
||||
@Env('N8N_SMTP_USER')
|
||||
readonly user: string = '';
|
||||
user: string = '';
|
||||
|
||||
/** SMTP login password */
|
||||
@Env('N8N_SMTP_PASS')
|
||||
readonly pass: string = '';
|
||||
pass: string = '';
|
||||
|
||||
/** SMTP OAuth Service Client */
|
||||
@Env('N8N_SMTP_OAUTH_SERVICE_CLIENT')
|
||||
readonly serviceClient: string = '';
|
||||
serviceClient: string = '';
|
||||
|
||||
/** SMTP OAuth Private Key */
|
||||
@Env('N8N_SMTP_OAUTH_PRIVATE_KEY')
|
||||
readonly privateKey: string = '';
|
||||
privateKey: string = '';
|
||||
}
|
||||
|
||||
@Config
|
||||
export class SmtpConfig {
|
||||
class SmtpConfig {
|
||||
/** SMTP server host */
|
||||
@Env('N8N_SMTP_HOST')
|
||||
readonly host: string = '';
|
||||
host: string = '';
|
||||
|
||||
/** SMTP server port */
|
||||
@Env('N8N_SMTP_PORT')
|
||||
readonly port: number = 465;
|
||||
port: number = 465;
|
||||
|
||||
/** Whether to use SSL for SMTP */
|
||||
@Env('N8N_SMTP_SSL')
|
||||
readonly secure: boolean = true;
|
||||
secure: boolean = true;
|
||||
|
||||
/** Whether to use STARTTLS for SMTP when SSL is disabled */
|
||||
@Env('N8N_SMTP_STARTTLS')
|
||||
readonly startTLS: boolean = true;
|
||||
startTLS: boolean = true;
|
||||
|
||||
/** How to display sender name */
|
||||
@Env('N8N_SMTP_SENDER')
|
||||
readonly sender: string = '';
|
||||
sender: string = '';
|
||||
|
||||
@Nested
|
||||
readonly auth: SmtpAuth;
|
||||
auth: SmtpAuth;
|
||||
}
|
||||
|
||||
@Config
|
||||
export class TemplateConfig {
|
||||
/** Overrides default HTML template for inviting new people (use full path) */
|
||||
@Env('N8N_UM_EMAIL_TEMPLATES_INVITE')
|
||||
readonly invite: string = '';
|
||||
invite: string = '';
|
||||
|
||||
/** Overrides default HTML template for resetting password (use full path) */
|
||||
@Env('N8N_UM_EMAIL_TEMPLATES_PWRESET')
|
||||
readonly passwordReset: string = '';
|
||||
passwordReset: string = '';
|
||||
|
||||
/** Overrides default HTML template for notifying that a workflow was shared (use full path) */
|
||||
@Env('N8N_UM_EMAIL_TEMPLATES_WORKFLOW_SHARED')
|
||||
readonly workflowShared: string = '';
|
||||
workflowShared: string = '';
|
||||
|
||||
/** Overrides default HTML template for notifying that credentials were shared (use full path) */
|
||||
@Env('N8N_UM_EMAIL_TEMPLATES_CREDENTIALS_SHARED')
|
||||
readonly credentialsShared: string = '';
|
||||
credentialsShared: string = '';
|
||||
}
|
||||
|
||||
@Config
|
||||
export class EmailConfig {
|
||||
class EmailConfig {
|
||||
/** How to send emails */
|
||||
@Env('N8N_EMAIL_MODE')
|
||||
readonly mode: '' | 'smtp' = 'smtp';
|
||||
mode: '' | 'smtp' = 'smtp';
|
||||
|
||||
@Nested
|
||||
readonly smtp: SmtpConfig;
|
||||
smtp: SmtpConfig;
|
||||
|
||||
@Nested
|
||||
readonly template: TemplateConfig;
|
||||
template: TemplateConfig;
|
||||
}
|
||||
|
||||
@Config
|
||||
export class UserManagementConfig {
|
||||
@Nested
|
||||
emails: EmailConfig;
|
||||
}
|
|
@ -4,13 +4,13 @@ import { Config, Env } from '../decorators';
|
|||
export class VersionNotificationsConfig {
|
||||
/** Whether to request notifications about new n8n versions */
|
||||
@Env('N8N_VERSION_NOTIFICATIONS_ENABLED')
|
||||
readonly enabled: boolean = true;
|
||||
enabled: boolean = true;
|
||||
|
||||
/** Endpoint to retrieve n8n version information from */
|
||||
@Env('N8N_VERSION_NOTIFICATIONS_ENDPOINT')
|
||||
readonly endpoint: string = 'https://api.n8n.io/api/versions/';
|
||||
endpoint: string = 'https://api.n8n.io/api/versions/';
|
||||
|
||||
/** URL for versions panel to page instructing user on how to update n8n instance */
|
||||
@Env('N8N_VERSION_NOTIFICATIONS_INFO_URL')
|
||||
readonly infoUrl: string = 'https://docs.n8n.io/hosting/installation/updating/';
|
||||
infoUrl: string = 'https://docs.n8n.io/hosting/installation/updating/';
|
||||
}
|
|
@ -4,17 +4,14 @@ import { Config, Env } from '../decorators';
|
|||
export class WorkflowsConfig {
|
||||
/** Default name for workflow */
|
||||
@Env('WORKFLOWS_DEFAULT_NAME')
|
||||
readonly defaultName: string = 'My workflow';
|
||||
defaultName: string = 'My workflow';
|
||||
|
||||
/** Show onboarding flow in new workflow */
|
||||
@Env('N8N_ONBOARDING_FLOW_DISABLED')
|
||||
readonly onboardingFlowDisabled: boolean = false;
|
||||
onboardingFlowDisabled: boolean = false;
|
||||
|
||||
/** Default option for which workflows may call the current workflow */
|
||||
@Env('N8N_WORKFLOW_CALLER_POLICY_DEFAULT_OPTION')
|
||||
readonly callerPolicyDefaultOption:
|
||||
| 'any'
|
||||
| 'none'
|
||||
| 'workflowsFromAList'
|
||||
| 'workflowsFromSameOwner' = 'workflowsFromSameOwner';
|
||||
callerPolicyDefaultOption: 'any' | 'none' | 'workflowsFromAList' | 'workflowsFromSameOwner' =
|
||||
'workflowsFromSameOwner';
|
||||
}
|
|
@ -47,6 +47,11 @@ export const Config: ClassDecorator = (ConfigClass: Class) => {
|
|||
} else {
|
||||
value = value === 'true';
|
||||
}
|
||||
} else if (type === Object) {
|
||||
// eslint-disable-next-line n8n-local-rules/no-plain-errors
|
||||
throw new Error(
|
||||
`Invalid decorator metadata on key "${key as string}" on ${ConfigClass.name}\n Please use explicit typing on all config fields`,
|
||||
);
|
||||
} else if (type !== String && type !== Object) {
|
||||
value = new (type as Constructable)(value as string);
|
||||
}
|
||||
|
|
|
@ -1,54 +1,80 @@
|
|||
import { Config, Nested } from './decorators';
|
||||
import { CredentialsConfig } from './configs/credentials';
|
||||
import { DatabaseConfig } from './configs/database';
|
||||
import { EmailConfig } from './configs/email';
|
||||
import { VersionNotificationsConfig } from './configs/version-notifications';
|
||||
import { PublicApiConfig } from './configs/public-api';
|
||||
import { ExternalSecretsConfig } from './configs/external-secrets';
|
||||
import { TemplatesConfig } from './configs/templates';
|
||||
import { EventBusConfig } from './configs/event-bus';
|
||||
import { NodesConfig } from './configs/nodes';
|
||||
import { ExternalStorageConfig } from './configs/external-storage';
|
||||
import { WorkflowsConfig } from './configs/workflows';
|
||||
|
||||
@Config
|
||||
class UserManagementConfig {
|
||||
@Nested
|
||||
emails: EmailConfig;
|
||||
}
|
||||
import { Config, Env, Nested } from './decorators';
|
||||
import { CredentialsConfig } from './configs/credentials.config';
|
||||
import { DatabaseConfig } from './configs/database.config';
|
||||
import { VersionNotificationsConfig } from './configs/version-notifications.config';
|
||||
import { PublicApiConfig } from './configs/public-api.config';
|
||||
import { ExternalSecretsConfig } from './configs/external-secrets.config';
|
||||
import { TemplatesConfig } from './configs/templates.config';
|
||||
import { EventBusConfig } from './configs/event-bus.config';
|
||||
import { NodesConfig } from './configs/nodes.config';
|
||||
import { ExternalStorageConfig } from './configs/external-storage.config';
|
||||
import { WorkflowsConfig } from './configs/workflows.config';
|
||||
import { EndpointsConfig } from './configs/endpoints.config';
|
||||
import { CacheConfig } from './configs/cache.config';
|
||||
import { ScalingModeConfig } from './configs/scaling-mode.config';
|
||||
import { UserManagementConfig } from './configs/user-management.config';
|
||||
|
||||
@Config
|
||||
export class GlobalConfig {
|
||||
@Nested
|
||||
readonly database: DatabaseConfig;
|
||||
database: DatabaseConfig;
|
||||
|
||||
@Nested
|
||||
readonly credentials: CredentialsConfig;
|
||||
credentials: CredentialsConfig;
|
||||
|
||||
@Nested
|
||||
readonly userManagement: UserManagementConfig;
|
||||
userManagement: UserManagementConfig;
|
||||
|
||||
@Nested
|
||||
readonly versionNotifications: VersionNotificationsConfig;
|
||||
versionNotifications: VersionNotificationsConfig;
|
||||
|
||||
@Nested
|
||||
readonly publicApi: PublicApiConfig;
|
||||
publicApi: PublicApiConfig;
|
||||
|
||||
@Nested
|
||||
readonly externalSecrets: ExternalSecretsConfig;
|
||||
externalSecrets: ExternalSecretsConfig;
|
||||
|
||||
@Nested
|
||||
readonly templates: TemplatesConfig;
|
||||
templates: TemplatesConfig;
|
||||
|
||||
@Nested
|
||||
readonly eventBus: EventBusConfig;
|
||||
eventBus: EventBusConfig;
|
||||
|
||||
@Nested
|
||||
readonly nodes: NodesConfig;
|
||||
nodes: NodesConfig;
|
||||
|
||||
@Nested
|
||||
readonly externalStorage: ExternalStorageConfig;
|
||||
externalStorage: ExternalStorageConfig;
|
||||
|
||||
@Nested
|
||||
readonly workflows: WorkflowsConfig;
|
||||
workflows: WorkflowsConfig;
|
||||
|
||||
/** Path n8n is deployed to */
|
||||
@Env('N8N_PATH')
|
||||
path: string = '/';
|
||||
|
||||
/** Host name n8n can be reached */
|
||||
@Env('N8N_HOST')
|
||||
host: string = 'localhost';
|
||||
|
||||
/** HTTP port n8n can be reached */
|
||||
@Env('N8N_PORT')
|
||||
port: number = 5678;
|
||||
|
||||
/** IP address n8n should listen on */
|
||||
@Env('N8N_LISTEN_ADDRESS')
|
||||
listen_address: string = '0.0.0.0';
|
||||
|
||||
/** HTTP Protocol via which n8n can be reached */
|
||||
@Env('N8N_PROTOCOL')
|
||||
protocol: 'http' | 'https' = 'http';
|
||||
|
||||
@Nested
|
||||
endpoints: EndpointsConfig;
|
||||
|
||||
@Nested
|
||||
cache: CacheConfig;
|
||||
|
||||
@Nested
|
||||
queue: ScalingModeConfig;
|
||||
}
|
||||
|
|
|
@ -18,6 +18,11 @@ describe('GlobalConfig', () => {
|
|||
});
|
||||
|
||||
const defaultConfig: GlobalConfig = {
|
||||
path: '/',
|
||||
host: 'localhost',
|
||||
port: 5678,
|
||||
listen_address: '0.0.0.0',
|
||||
protocol: 'http',
|
||||
database: {
|
||||
logging: {
|
||||
enabled: false,
|
||||
|
@ -103,6 +108,8 @@ describe('GlobalConfig', () => {
|
|||
nodes: {
|
||||
communityPackages: {
|
||||
enabled: true,
|
||||
registry: 'https://registry.npmjs.org',
|
||||
reinstallMissing: false,
|
||||
},
|
||||
errorTriggerType: 'n8n-nodes-base.errorTrigger',
|
||||
include: [],
|
||||
|
@ -140,12 +147,82 @@ describe('GlobalConfig', () => {
|
|||
onboardingFlowDisabled: false,
|
||||
callerPolicyDefaultOption: 'workflowsFromSameOwner',
|
||||
},
|
||||
endpoints: {
|
||||
metrics: {
|
||||
enable: false,
|
||||
prefix: 'n8n_',
|
||||
includeWorkflowIdLabel: false,
|
||||
includeDefaultMetrics: true,
|
||||
includeMessageEventBusMetrics: false,
|
||||
includeNodeTypeLabel: false,
|
||||
includeCacheMetrics: false,
|
||||
includeApiEndpoints: false,
|
||||
includeApiPathLabel: false,
|
||||
includeApiMethodLabel: false,
|
||||
includeCredentialTypeLabel: false,
|
||||
includeApiStatusCodeLabel: false,
|
||||
},
|
||||
additionalNonUIRoutes: '',
|
||||
disableProductionWebhooksOnMainProcess: false,
|
||||
disableUi: false,
|
||||
form: 'form',
|
||||
formTest: 'form-test',
|
||||
formWaiting: 'form-waiting',
|
||||
payloadSizeMax: 16,
|
||||
rest: 'rest',
|
||||
webhook: 'webhook',
|
||||
webhookTest: 'webhook-test',
|
||||
webhookWaiting: 'webhook-waiting',
|
||||
},
|
||||
cache: {
|
||||
backend: 'auto',
|
||||
memory: {
|
||||
maxSize: 3145728,
|
||||
ttl: 3600000,
|
||||
},
|
||||
redis: {
|
||||
prefix: 'redis',
|
||||
ttl: 3600000,
|
||||
},
|
||||
},
|
||||
queue: {
|
||||
health: {
|
||||
active: false,
|
||||
port: 5678,
|
||||
},
|
||||
bull: {
|
||||
redis: {
|
||||
db: 0,
|
||||
host: 'localhost',
|
||||
password: '',
|
||||
port: 6379,
|
||||
timeoutThreshold: 10_000,
|
||||
username: '',
|
||||
clusterNodes: '',
|
||||
tls: false,
|
||||
},
|
||||
queueRecoveryInterval: 60,
|
||||
gracefulShutdownTimeout: 30,
|
||||
prefix: 'bull',
|
||||
settings: {
|
||||
lockDuration: 30_000,
|
||||
lockRenewTime: 15_000,
|
||||
stalledInterval: 30_000,
|
||||
maxStalledCount: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('should use all default values when no env variables are defined', () => {
|
||||
process.env = {};
|
||||
const config = Container.get(GlobalConfig);
|
||||
expect(config).toEqual(defaultConfig);
|
||||
|
||||
// deepCopy for diff to show plain objects
|
||||
// eslint-disable-next-line n8n-local-rules/no-json-parse-json-stringify
|
||||
const deepCopy = <T>(obj: T): T => JSON.parse(JSON.stringify(obj));
|
||||
|
||||
expect(deepCopy(config)).toEqual(defaultConfig);
|
||||
expect(mockFs.readFileSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
@ -155,6 +232,7 @@ describe('GlobalConfig', () => {
|
|||
DB_POSTGRESDB_USER: 'n8n',
|
||||
DB_TABLE_PREFIX: 'test_',
|
||||
NODES_INCLUDE: '["n8n-nodes-base.hackerNews"]',
|
||||
DB_LOGGING_MAX_EXECUTION_TIME: '0',
|
||||
};
|
||||
const config = Container.get(GlobalConfig);
|
||||
expect(config).toEqual({
|
||||
|
|
|
@ -8,6 +8,4 @@ These nodes are still in Beta state and are only compatible with the Docker imag
|
|||
|
||||
## License
|
||||
|
||||
n8n is [fair-code](https://faircode.io) distributed under the [**Sustainable Use License**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md).
|
||||
|
||||
Additional information about the license can be found in the [docs](https://docs.n8n.io/reference/license/).
|
||||
You can find the license information [here](https://github.com/n8n-io/n8n/blob/master/README.md#license)
|
||||
|
|
|
@ -9,7 +9,6 @@ import type {
|
|||
INodeTypeDescription,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
import { getTemplateNoticeField } from '../../../utils/sharedFields';
|
||||
import { promptTypeOptions, textInput } from '../../../utils/descriptions';
|
||||
import { conversationalAgentProperties } from './agents/ConversationalAgent/description';
|
||||
import { conversationalAgentExecute } from './agents/ConversationalAgent/execute';
|
||||
|
@ -83,6 +82,7 @@ function getInputs(
|
|||
filter: {
|
||||
nodes: [
|
||||
'@n8n/n8n-nodes-langchain.lmChatAnthropic',
|
||||
'@n8n/n8n-nodes-langchain.lmChatAwsBedrock',
|
||||
'@n8n/n8n-nodes-langchain.lmChatGroq',
|
||||
'@n8n/n8n-nodes-langchain.lmChatOllama',
|
||||
'@n8n/n8n-nodes-langchain.lmChatOpenAi',
|
||||
|
@ -113,6 +113,7 @@ function getInputs(
|
|||
'@n8n/n8n-nodes-langchain.lmChatAnthropic',
|
||||
'@n8n/n8n-nodes-langchain.lmChatAzureOpenAi',
|
||||
'@n8n/n8n-nodes-langchain.lmChatMistralCloud',
|
||||
'@n8n/n8n-nodes-langchain.lmChatOllama',
|
||||
'@n8n/n8n-nodes-langchain.lmChatOpenAi',
|
||||
'@n8n/n8n-nodes-langchain.lmChatGroq',
|
||||
'@n8n/n8n-nodes-langchain.lmChatGoogleVertex',
|
||||
|
@ -206,35 +207,37 @@ const agentTypeProperty: INodeProperties = {
|
|||
name: 'Tools Agent',
|
||||
value: 'toolsAgent',
|
||||
description:
|
||||
'Utilized unified Tool calling interface to select the appropriate tools and argument for execution',
|
||||
'Utilizes structured tool schemas for precise and reliable tool selection and execution. Recommended for complex tasks requiring accurate and consistent tool usage, but only usable with models that support tool calling.',
|
||||
},
|
||||
{
|
||||
name: 'Conversational Agent',
|
||||
value: 'conversationalAgent',
|
||||
description:
|
||||
'Selects tools to accomplish its task and uses memory to recall previous conversations',
|
||||
'Describes tools in the system prompt and parses JSON responses for tool calls. More flexible but potentially less reliable than the Tools Agent. Suitable for simpler interactions or with models not supporting structured schemas.',
|
||||
},
|
||||
{
|
||||
name: 'OpenAI Functions Agent',
|
||||
value: 'openAiFunctionsAgent',
|
||||
description:
|
||||
"Utilizes OpenAI's Function Calling feature to select the appropriate tool and arguments for execution",
|
||||
"Leverages OpenAI's function calling capabilities to precisely select and execute tools. Excellent for tasks requiring structured outputs when working with OpenAI models.",
|
||||
},
|
||||
{
|
||||
name: 'Plan and Execute Agent',
|
||||
value: 'planAndExecuteAgent',
|
||||
description:
|
||||
'Plan and execute agents accomplish an objective by first planning what to do, then executing the sub tasks',
|
||||
'Creates a high-level plan for complex tasks and then executes each step. Suitable for multi-stage problems or when a strategic approach is needed.',
|
||||
},
|
||||
{
|
||||
name: 'ReAct Agent',
|
||||
value: 'reActAgent',
|
||||
description: 'Strategically select tools to accomplish a given task',
|
||||
description:
|
||||
'Combines reasoning and action in an iterative process. Effective for tasks that require careful analysis and step-by-step problem-solving.',
|
||||
},
|
||||
{
|
||||
name: 'SQL Agent',
|
||||
value: 'sqlAgent',
|
||||
description: 'Answers questions about data in an SQL database',
|
||||
description:
|
||||
'Specializes in interacting with SQL databases. Ideal for data analysis tasks, generating queries, or extracting insights from structured data.',
|
||||
},
|
||||
],
|
||||
default: '',
|
||||
|
@ -256,7 +259,7 @@ export class Agent implements INodeType {
|
|||
color: '#404040',
|
||||
},
|
||||
codex: {
|
||||
alias: ['LangChain'],
|
||||
alias: ['LangChain', 'Chat', 'Conversational', 'Plan and Execute', 'ReAct', 'Tools'],
|
||||
categories: ['AI'],
|
||||
subcategories: {
|
||||
AI: ['Agents', 'Root Nodes'],
|
||||
|
@ -302,10 +305,14 @@ export class Agent implements INodeType {
|
|||
],
|
||||
properties: [
|
||||
{
|
||||
...getTemplateNoticeField(1954),
|
||||
displayName:
|
||||
'Tip: Get a feel for agents with our quick <a href="https://docs.n8n.io/advanced-ai/intro-tutorial/" target="_blank">tutorial</a> or see an <a href="/templates/1954" target="_blank">example</a> of how this node works',
|
||||
name: 'notice_tip',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
agent: ['conversationalAgent'],
|
||||
agent: ['conversationalAgent', 'toolsAgent'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -38,7 +38,7 @@ export async function openAiFunctionsAgentExecute(
|
|||
const memory = (await this.getInputConnectionData(NodeConnectionType.AiMemory, 0)) as
|
||||
| BaseChatMemory
|
||||
| undefined;
|
||||
const tools = await getConnectedTools(this, nodeVersion >= 1.5);
|
||||
const tools = await getConnectedTools(this, nodeVersion >= 1.5, false);
|
||||
const outputParsers = await getOptionalOutputParsers(this);
|
||||
const options = this.getNodeParameter('options', 0, {}) as {
|
||||
systemMessage?: string;
|
||||
|
|
|
@ -90,7 +90,7 @@ export async function toolsAgentExecute(this: IExecuteFunctions): Promise<INodeE
|
|||
| BaseChatMemory
|
||||
| undefined;
|
||||
|
||||
const tools = (await getConnectedTools(this, true)) as Array<DynamicStructuredTool | Tool>;
|
||||
const tools = (await getConnectedTools(this, true, false)) as Array<DynamicStructuredTool | Tool>;
|
||||
const outputParser = (await getOptionalOutputParsers(this))?.[0];
|
||||
let structuredOutputParserTool: DynamicStructuredTool | undefined;
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue