Merge branch 'n8n-io:master' into MongoDB_vector_store

This commit is contained in:
Pash10g 2025-03-03 15:56:52 +02:00 committed by GitHub
commit 00e78a6a86
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2226 changed files with 24944 additions and 8579 deletions

View file

@ -7,12 +7,12 @@ const trimPackageJson = (packageName) => {
const { scripts, peerDependencies, devDependencies, dependencies, ...packageJson } = require( const { scripts, peerDependencies, devDependencies, dependencies, ...packageJson } = require(
filePath, filePath,
); );
if (packageName === '@n8n/chat') { if (packageName === 'frontend/@n8n/chat') {
packageJson.dependencies = dependencies; packageJson.dependencies = dependencies;
} }
writeFileSync(filePath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8'); writeFileSync(filePath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8');
}; };
trimPackageJson('@n8n/chat'); trimPackageJson('frontend/@n8n/chat');
trimPackageJson('design-system'); trimPackageJson('frontend/@n8n/design-system');
trimPackageJson('editor-ui'); trimPackageJson('frontend/editor-ui');

View file

@ -51,14 +51,20 @@ jobs:
- name: Dry-run publishing - name: Dry-run publishing
run: pnpm publish -r --no-git-checks --dry-run run: pnpm publish -r --no-git-checks --dry-run
- name: Publish to NPM - name: Pre publishing changes
run: | run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
node .github/scripts/trim-fe-packageJson.js node .github/scripts/trim-fe-packageJson.js
node .github/scripts/ensure-provenance-fields.mjs node .github/scripts/ensure-provenance-fields.mjs
cp README.md packages/cli/README.md
sed -i "s/default: 'dev'/default: 'stable'/g" packages/cli/dist/config/schema.js sed -i "s/default: 'dev'/default: 'stable'/g" packages/cli/dist/config/schema.js
pnpm publish -r --publish-branch ${{github.event.pull_request.base.ref}} --access public --tag rc --no-git-checks
npm dist-tag rm n8n rc - name: Publish to NPM
run: pnpm publish -r --publish-branch ${{github.event.pull_request.base.ref}} --access public --tag rc --no-git-checks
- name: Cleanup rc tag
run: npm dist-tag rm n8n rc
continue-on-error: true
- id: set-release - id: set-release
run: echo "release=${{ env.RELEASE }}" >> $GITHUB_OUTPUT run: echo "release=${{ env.RELEASE }}" >> $GITHUB_OUTPUT
@ -68,7 +74,7 @@ jobs:
needs: [publish-to-npm] needs: [publish-to-npm]
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event.pull_request.merged == true if: github.event.pull_request.merged == true
timeout-minutes: 10 timeout-minutes: 15
steps: steps:
- name: Checkout - name: Checkout
@ -103,6 +109,7 @@ jobs:
context: ./docker/images/n8n context: ./docker/images/n8n
build-args: | build-args: |
N8N_VERSION=${{ needs.publish-to-npm.outputs.release }} N8N_VERSION=${{ needs.publish-to-npm.outputs.release }}
N8N_RELEASE_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
provenance: false provenance: false
push: true push: true
@ -155,7 +162,7 @@ jobs:
with: with:
projects: ${{ secrets.SENTRY_FRONTEND_PROJECT }} projects: ${{ secrets.SENTRY_FRONTEND_PROJECT }}
version: ${{ needs.publish-to-npm.outputs.release }} version: ${{ needs.publish-to-npm.outputs.release }}
sourcemaps: packages/editor-ui/dist sourcemaps: packages/frontend/editor-ui/dist
- name: Create a backend release - name: Create a backend release
uses: getsentry/action-release@v1.7.0 uses: getsentry/action-release@v1.7.0

View file

@ -11,10 +11,10 @@ on:
description: 'Release channel' description: 'Release channel'
required: true required: true
type: choice type: choice
default: 'next' default: 'beta'
options: options:
- next - beta
- latest - stable
jobs: jobs:
release-to-npm: release-to-npm:
@ -25,9 +25,18 @@ jobs:
- uses: actions/setup-node@v4.2.0 - uses: actions/setup-node@v4.2.0
with: with:
node-version: 20.x node-version: 20.x
- run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc - run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
npm dist-tag add n8n@${{ github.event.inputs.version }} ${{ github.event.inputs.release-channel }}
- if: github.event.inputs.release-channel == 'beta'
run: |
npm dist-tag add n8n@${{ github.event.inputs.version }} next
npm dist-tag add n8n@${{ github.event.inputs.version }} beta
- if: github.event.inputs.release-channel == 'stable'
run: |
npm dist-tag add n8n@${{ github.event.inputs.version }} latest
npm dist-tag add n8n@${{ github.event.inputs.version }} stable
release-to-docker-hub: release-to-docker-hub:
name: Release to DockerHub name: Release to DockerHub
@ -39,7 +48,15 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- run: docker buildx imagetools create -t ${{ secrets.DOCKER_USERNAME }}/n8n:${{ github.event.inputs.release-channel }} ${{ secrets.DOCKER_USERNAME }}/n8n:${{ github.event.inputs.version }} - if: github.event.inputs.release-channel == 'stable'
run: |
docker buildx imagetools create -t ${{ secrets.DOCKER_USERNAME }}/n8n:stable ${{ secrets.DOCKER_USERNAME }}/n8n:${{ github.event.inputs.version }}
docker buildx imagetools create -t ${{ secrets.DOCKER_USERNAME }}/n8n:latest ${{ secrets.DOCKER_USERNAME }}/n8n:${{ github.event.inputs.version }}
- if: github.event.inputs.release-channel == 'beta'
run: |
docker buildx imagetools create -t ${{ secrets.DOCKER_USERNAME }}/n8n:beta ${{ secrets.DOCKER_USERNAME }}/n8n:${{ github.event.inputs.version }}
docker buildx imagetools create -t ${{ secrets.DOCKER_USERNAME }}/n8n:next ${{ secrets.DOCKER_USERNAME }}/n8n:${{ github.event.inputs.version }}
release-to-github-container-registry: release-to-github-container-registry:
name: Release to GitHub Container Registry name: Release to GitHub Container Registry
@ -52,7 +69,15 @@ jobs:
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- run: docker buildx imagetools create -t ghcr.io/${{ github.repository_owner }}/n8n:${{ github.event.inputs.release-channel }} ghcr.io/${{ github.repository_owner }}/n8n:${{ github.event.inputs.version }} - if: github.event.inputs.release-channel == 'stable'
run: |
docker buildx imagetools create -t ghcr.io/${{ github.repository_owner }}/n8n:stable ghcr.io/${{ github.repository_owner }}/n8n:${{ github.event.inputs.version }}
docker buildx imagetools create -t ghcr.io/${{ github.repository_owner }}/n8n:latest ghcr.io/${{ github.repository_owner }}/n8n:${{ github.event.inputs.version }}
- if: github.event.inputs.release-channel == 'beta'
run: |
docker buildx imagetools create -t ghcr.io/${{ github.repository_owner }}/n8n:beta ghcr.io/${{ github.repository_owner }}/n8n:${{ github.event.inputs.version }}
docker buildx imagetools create -t ghcr.io/${{ github.repository_owner }}/n8n:next ghcr.io/${{ github.repository_owner }}/n8n:${{ github.event.inputs.version }}
update-docs: update-docs:
name: Update latest and next in the docs name: Update latest and next in the docs

View file

@ -2,7 +2,7 @@ coverage
dist dist
package.json package.json
pnpm-lock.yaml pnpm-lock.yaml
packages/editor-ui/index.html packages/frontend/editor-ui/index.html
packages/nodes-base/nodes/**/test packages/nodes-base/nodes/**/test
packages/cli/templates/form-trigger.handlebars packages/cli/templates/form-trigger.handlebars
packages/cli/templates/form-trigger-completion.handlebars packages/cli/templates/form-trigger-completion.handlebars

View file

@ -1,3 +1,67 @@
# [1.81.0](https://github.com/n8n-io/n8n/compare/n8n@1.80.0...n8n@1.81.0) (2025-02-24)
### Bug Fixes
* Always clear popupWindowState before showing popup from form trigger ([#13363](https://github.com/n8n-io/n8n/issues/13363)) ([b7f1265](https://github.com/n8n-io/n8n/commit/b7f12650f1f42c0ff15c1da3e5ade350fb1e23d2))
* **Code Node:** Fix `$items` in Code node when using task runner ([#13368](https://github.com/n8n-io/n8n/issues/13368)) ([87b3c50](https://github.com/n8n-io/n8n/commit/87b3c508b3d5a7d6f3b9f8377de66567a04fa970))
* **core:** Avoid renewing the license on init to prevent excessive duplicate renewal calls ([#13347](https://github.com/n8n-io/n8n/issues/13347)) ([1e1f528](https://github.com/n8n-io/n8n/commit/1e1f52846641515ad4479ab1088e78a9266e452d))
* **core:** Ensure that 'workflow-post-execute' event has userId whenever it's available ([#13326](https://github.com/n8n-io/n8n/issues/13326)) ([f41e353](https://github.com/n8n-io/n8n/commit/f41e353887fef4269510d25fa87b73da4cf925f9))
* **core:** Fix DB migrations for MySQL ([#13261](https://github.com/n8n-io/n8n/issues/13261)) ([d0968a1](https://github.com/n8n-io/n8n/commit/d0968a10d56ac5c97974129742ba8f8a85997dac))
* **core:** Fix resuming executions on test webhooks from Wait forms ([#13410](https://github.com/n8n-io/n8n/issues/13410)) ([8ffd316](https://github.com/n8n-io/n8n/commit/8ffd3167d58d30f087fd31010e6f79f1398d8f49))
* **core:** Handle connections for missing nodes in a workflow ([#13362](https://github.com/n8n-io/n8n/issues/13362)) ([1e5feb1](https://github.com/n8n-io/n8n/commit/1e5feb195d50054939f85c9e1b5a32885c579901))
* **core:** Make sure middleware works with legacy API Keys ([#13390](https://github.com/n8n-io/n8n/issues/13390)) ([ca76ef4](https://github.com/n8n-io/n8n/commit/ca76ef4bc248a3bcde844bc8378d38eed269f032))
* **core:** Return original hooks errors to the frontend ([#13365](https://github.com/n8n-io/n8n/issues/13365)) ([5439181](https://github.com/n8n-io/n8n/commit/5439181e92f20fef1423575cabec7acbe1740b26))
* **editor:** Correctly close node creator when selecting/deselecting a node ([#13338](https://github.com/n8n-io/n8n/issues/13338)) ([c3dc66e](https://github.com/n8n-io/n8n/commit/c3dc66ee7372927fcfd6baac3b9d853690e39c99))
* **editor:** Do not show credential details popup for users without necessary scopes with direct link ([#13264](https://github.com/n8n-io/n8n/issues/13264)) ([a5401d0](https://github.com/n8n-io/n8n/commit/a5401d06a58ef026f44499d05b42a8d0dbe2520e))
* **editor:** Do not show project settings for users without permission with direct link ([#13246](https://github.com/n8n-io/n8n/issues/13246)) ([fa488f1](https://github.com/n8n-io/n8n/commit/fa488f15619f798a0360c96492f2928ac661d9ee))
* **editor:** Don't open form popup window if different trigger node is used ([#13391](https://github.com/n8n-io/n8n/issues/13391)) ([57a9a5b](https://github.com/n8n-io/n8n/commit/57a9a5b15f55aae0301851e93847ed87feb081e8))
* **editor:** Fix configurable node description margins and text alignment ([#13318](https://github.com/n8n-io/n8n/issues/13318)) ([c881ea2](https://github.com/n8n-io/n8n/commit/c881ea2c7b43a4fb610533dd553520a6de51f22d))
* **editor:** Fix workflow moving E2E tests ([#13396](https://github.com/n8n-io/n8n/issues/13396)) ([073b05b](https://github.com/n8n-io/n8n/commit/073b05b10c81e3a0451c310bc0bde25170e1591e))
* **editor:** Optionally share credentials used by the workflow when moving the workflow between projects ([#12524](https://github.com/n8n-io/n8n/issues/12524)) ([7bd83d7](https://github.com/n8n-io/n8n/commit/7bd83d7d330b6f01b5798461f2218254a9964d87))
* **editor:** Polyfill `Array.prototype.toSorted` (no-chanhelog) ([#13463](https://github.com/n8n-io/n8n/issues/13463)) ([f2b15ea](https://github.com/n8n-io/n8n/commit/f2b15ea086fcc541a5a584998985d712335210ec))
* **editor:** Register/unregister keybindings on window focus/blur ([#13419](https://github.com/n8n-io/n8n/issues/13419)) ([7a504dc](https://github.com/n8n-io/n8n/commit/7a504dc30fcf0c7641528ed469835811f82bb098))
* **editor:** Switch back to selection mode on window blur ([#13341](https://github.com/n8n-io/n8n/issues/13341)) ([415e25b](https://github.com/n8n-io/n8n/commit/415e25b5d524b0d3c391403f129468e57bbb918e))
* Prevent flicker during paginated workflow navigation ([#13348](https://github.com/n8n-io/n8n/issues/13348)) ([d277e0b](https://github.com/n8n-io/n8n/commit/d277e0ba0e5d87500457538b4b0f1363e267f071))
### Features
* **core:** Hackmation - Add last activity metric ([#13237](https://github.com/n8n-io/n8n/issues/13237)) ([272f55b](https://github.com/n8n-io/n8n/commit/272f55b80f1d4576d1675040bd2775210c4ab5e9))
* **editor:** Change rename node keyboard shortcut to Space on new canvas ([#11872](https://github.com/n8n-io/n8n/issues/11872)) ([c90d0d9](https://github.com/n8n-io/n8n/commit/c90d0d9161ec161cd1afd6aa5b56345c1611f9c9))
* **editor:** Implement breadcrumbs component ([#13317](https://github.com/n8n-io/n8n/issues/13317)) ([db297f1](https://github.com/n8n-io/n8n/commit/db297f107d81738d57e298135a9c279ad83345dc))
* **editor:** Implement folder navigation in workflows list ([#13370](https://github.com/n8n-io/n8n/issues/13370)) ([0eae14e](https://github.com/n8n-io/n8n/commit/0eae14e27ab4fab3229750d6b2a32868db1e8dd4))
* **editor:** Rename 'Text' fields on AI nodes to 'Prompt' ([#13416](https://github.com/n8n-io/n8n/issues/13416)) ([4fa666b](https://github.com/n8n-io/n8n/commit/4fa666b976423365299e915130384e10c8e12528))
* Enable partial exections v2 by default ([#13344](https://github.com/n8n-io/n8n/issues/13344)) ([29ae239](https://github.com/n8n-io/n8n/commit/29ae2396c99d54d8f3db71e6370516f0dc354d00))
* **n8n Form Node:** Limit wait time parameters ([#13160](https://github.com/n8n-io/n8n/issues/13160)) ([14b6f8b](https://github.com/n8n-io/n8n/commit/14b6f8b97275e38ba4a4c1819e8e32b711de21ba))
# [1.80.0](https://github.com/n8n-io/n8n/compare/n8n@1.79.0...n8n@1.80.0) (2025-02-17)
### Bug Fixes
* **AI Agent Node:** Move model retrieval into try/catch to fix continueOnFail handling ([#13165](https://github.com/n8n-io/n8n/issues/13165)) ([47c5688](https://github.com/n8n-io/n8n/commit/47c5688618001a51c9412c5d07fd25d85b8d1b8d))
* **Code Tool Node:** Fix Input Schema Parameter not hiding correctly ([#13245](https://github.com/n8n-io/n8n/issues/13245)) ([8e15ebf](https://github.com/n8n-io/n8n/commit/8e15ebf8333d06b5fe4d5bf8ee39f285b31332d7))
* **core:** Redact credentials ([#13263](https://github.com/n8n-io/n8n/issues/13263)) ([052f177](https://github.com/n8n-io/n8n/commit/052f17744d072cd16ce90ea94fa9873b4ea2ffed))
* **core:** Reduce risk of race condition during workflow activation loop ([#13186](https://github.com/n8n-io/n8n/issues/13186)) ([64c5b6e](https://github.com/n8n-io/n8n/commit/64c5b6e0604ce9da6b19dd5f04e61e38209b3153))
* **core:** Run full manual execution when a trigger is executed even if run data exists ([#13194](https://github.com/n8n-io/n8n/issues/13194)) ([66acb1b](https://github.com/n8n-io/n8n/commit/66acb1bcb68926526ed98a5fe5b89bdaa74148d6))
* Display correct editor URL ([#13251](https://github.com/n8n-io/n8n/issues/13251)) ([67a4ed1](https://github.com/n8n-io/n8n/commit/67a4ed18a13cb2bc54b3472b9a8beb2f274c2bd2))
* **editor:** Add template id to metadata when saving workflows from json ([#13172](https://github.com/n8n-io/n8n/issues/13172)) ([2a92032](https://github.com/n8n-io/n8n/commit/2a92032704ebc4e0cdd11aa59b6834a9d891ffb0))
* **editor:** Fix page size resetting when filters are reset on workflows page ([#13265](https://github.com/n8n-io/n8n/issues/13265)) ([b4380d0](https://github.com/n8n-io/n8n/commit/b4380d05087e1213641ee322875cf51bf706d2f5))
* **editor:** Open autocompletion when starting an expression ([#13249](https://github.com/n8n-io/n8n/issues/13249)) ([6377635](https://github.com/n8n-io/n8n/commit/6377635bf03387c8d0ae5d54848113258bbabacc))
* **editor:** Prevent pagination setting from being overwritten in URL ([#13266](https://github.com/n8n-io/n8n/issues/13266)) ([d1e65a1](https://github.com/n8n-io/n8n/commit/d1e65a1cd5841f1d4e815f8da36713cdb18281a4))
* **editor:** Propagate isReadOnly to ResourceMapper `Attempt to Convert Types` switch ([#13216](https://github.com/n8n-io/n8n/issues/13216)) ([617f841](https://github.com/n8n-io/n8n/commit/617f841e0d82f2b40fcf9ac4bf2cb6a8010b517f))
* **editor:** Render assignments without ID correctly ([#13252](https://github.com/n8n-io/n8n/issues/13252)) ([d116f12](https://github.com/n8n-io/n8n/commit/d116f121e351e3d81e1b5d6c52eb3e5c3b68ae43))
### Features
* **editor:** Add pagination to the workflows list ([#13100](https://github.com/n8n-io/n8n/issues/13100)) ([8e37088](https://github.com/n8n-io/n8n/commit/8e370882490d569ff85bba6b7f0a1320fab5eb91))
# [1.79.0](https://github.com/n8n-io/n8n/compare/n8n@1.78.0...n8n@1.79.0) (2025-02-13) # [1.79.0](https://github.com/n8n-io/n8n/compare/n8n@1.78.0...n8n@1.79.0) (2025-02-13)

View file

@ -49,8 +49,8 @@ The most important directories:
execution, active webhooks and execution, active webhooks and
workflows. **Contact n8n before workflows. **Contact n8n before
starting on any changes here** starting on any changes here**
- [/packages/design-system](/packages/design-system) - Vue frontend components - [/packages/frontend/@n8n/design-system](/packages/design-system) - Vue frontend components
- [/packages/editor-ui](/packages/editor-ui) - Vue frontend workflow editor - [/packages/frontend/editor-ui](/packages/editor-ui) - Vue frontend workflow editor
- [/packages/node-dev](/packages/node-dev) - CLI to create new n8n-nodes - [/packages/node-dev](/packages/node-dev) - CLI to create new n8n-nodes
- [/packages/nodes-base](/packages/nodes-base) - Base n8n nodes - [/packages/nodes-base](/packages/nodes-base) - Base n8n nodes
- [/packages/workflow](/packages/workflow) - Workflow code with interfaces which - [/packages/workflow](/packages/workflow) - Workflow code with interfaces which

View file

@ -42,10 +42,7 @@ component_management:
- component_id: frontend_packages - component_id: frontend_packages
name: Frontend name: Frontend
paths: paths:
- packages/@n8n/chat/**
- packages/@n8n/codemirror-lang/** - packages/@n8n/codemirror-lang/**
- packages/design-system/**
- packages/editor-ui/**
- packages/frontend/** - packages/frontend/**
- component_id: nodes_packages - component_id: nodes_packages
name: Nodes name: Nodes

View file

@ -1,10 +1,10 @@
const sharedOptions = require('@n8n_io/eslint-config/shared'); const sharedOptions = require('@n8n/eslint-config/shared');
/** /**
* @type {import('@types/eslint').ESLint.ConfigData} * @type {import('@types/eslint').ESLint.ConfigData}
*/ */
module.exports = { module.exports = {
extends: ['@n8n_io/eslint-config/base', 'plugin:cypress/recommended'], extends: ['@n8n/eslint-config/base', 'plugin:cypress/recommended'],
...sharedOptions(__dirname), ...sharedOptions(__dirname),

View file

@ -206,6 +206,10 @@ export function clickWorkflowCardContent(workflowName: string) {
getWorkflowCardContent(workflowName).click(); getWorkflowCardContent(workflowName).click();
} }
export function clickAssignmentCollectionAdd() {
cy.getByTestId('assignment-collection-drop-area').click();
}
export function assertNodeOutputHintExists() { export function assertNodeOutputHintExists() {
getNodeOutputHint().should('exist'); getNodeOutputHint().should('exist');
} }

View file

@ -356,5 +356,5 @@ export function openContextMenu(
} }
export function clickContextMenuAction(action: string) { export function clickContextMenuAction(action: string) {
getContextMenuAction(action).click(); getContextMenuAction(action).click({ force: true });
} }

View file

@ -52,7 +52,7 @@ export const PIPEDRIVE_NODE_NAME = 'Pipedrive';
export const HTTP_REQUEST_NODE_NAME = 'HTTP Request'; export const HTTP_REQUEST_NODE_NAME = 'HTTP Request';
export const AGENT_NODE_NAME = 'AI Agent'; export const AGENT_NODE_NAME = 'AI Agent';
export const BASIC_LLM_CHAIN_NODE_NAME = 'Basic LLM Chain'; export const BASIC_LLM_CHAIN_NODE_NAME = 'Basic LLM Chain';
export const AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME = 'Window Buffer Memory'; export const AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME = 'Simple Memory';
export const AI_TOOL_CALCULATOR_NODE_NAME = 'Calculator'; export const AI_TOOL_CALCULATOR_NODE_NAME = 'Calculator';
export const AI_TOOL_CODE_NODE_NAME = 'Code Tool'; export const AI_TOOL_CODE_NODE_NAME = 'Code Tool';
export const AI_TOOL_WIKIPEDIA_NODE_NAME = 'Wikipedia'; export const AI_TOOL_WIKIPEDIA_NODE_NAME = 'Wikipedia';

View file

@ -14,6 +14,8 @@ module.exports = defineConfig({
experimentalMemoryManagement: true, experimentalMemoryManagement: true,
e2e: { e2e: {
baseUrl: BASE_URL, baseUrl: BASE_URL,
viewportWidth: 1536,
viewportHeight: 960,
video: true, video: true,
screenshotOnRunFailure: true, screenshotOnRunFailure: true,
experimentalInteractiveRunEvents: true, experimentalInteractiveRunEvents: true,

View file

@ -1,9 +1,12 @@
import { WorkflowSharingModal } from '../pages';
import { successToast } from '../pages/notifications';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows'; import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
import { getUniqueWorkflowName } from '../utils/workflowUtils'; import { getUniqueWorkflowName } from '../utils/workflowUtils';
const WorkflowsPage = new WorkflowsPageClass(); const WorkflowsPage = new WorkflowsPageClass();
const WorkflowPage = new WorkflowPageClass(); const WorkflowPage = new WorkflowPageClass();
const workflowSharingModal = new WorkflowSharingModal();
const multipleWorkflowsCount = 5; const multipleWorkflowsCount = 5;
@ -62,14 +65,12 @@ describe('Workflows', () => {
it('should delete all the workflows', () => { it('should delete all the workflows', () => {
WorkflowsPage.getters.workflowCards().should('have.length', multipleWorkflowsCount + 1); WorkflowsPage.getters.workflowCards().should('have.length', multipleWorkflowsCount + 1);
WorkflowsPage.getters.workflowCards().each(($el) => { for (let i = 0; i < multipleWorkflowsCount + 1; i++) {
const workflowName = $el.find('[data-test-id="workflow-card-name"]').text(); cy.getByTestId('workflow-card-actions').first().click();
WorkflowsPage.getters.workflowCardActions(workflowName).click();
WorkflowsPage.getters.workflowDeleteButton().click(); WorkflowsPage.getters.workflowDeleteButton().click();
cy.get('button').contains('delete').click(); cy.get('button').contains('delete').click();
}); successToast().should('be.visible');
}
WorkflowsPage.getters.newWorkflowButtonCard().should('be.visible'); WorkflowsPage.getters.newWorkflowButtonCard().should('be.visible');
}); });
@ -138,4 +139,10 @@ describe('Workflows', () => {
cy.url().should('include', 'sort=lastCreated'); cy.url().should('include', 'sort=lastCreated');
cy.url().should('include', 'pageSize=25'); cy.url().should('include', 'pageSize=25');
}); });
it('should be able to share workflows from workflows list', () => {
WorkflowsPage.getters.workflowCardActions('Empty State Card Workflow').click();
WorkflowsPage.getters.workflowActionItem('share').click();
workflowSharingModal.getters.modal().should('be.visible');
});
}); });

View file

@ -22,7 +22,11 @@ describe('Undo/Redo', () => {
it('should undo/redo deleting node using context menu', () => { it('should undo/redo deleting node using context menu', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.deleteNodeFromContextMenu(CODE_NODE_NAME); WorkflowPage.actions.zoomToFit();
WorkflowPage.actions.deleteNodeFromContextMenu(CODE_NODE_NAME, {
method: 'right-click',
anchor: 'topLeft',
});
WorkflowPage.getters.canvasNodes().should('have.have.length', 1); WorkflowPage.getters.canvasNodes().should('have.have.length', 1);
WorkflowPage.getters.nodeConnections().should('have.length', 0); WorkflowPage.getters.nodeConnections().should('have.length', 0);
WorkflowPage.actions.hitUndo(); WorkflowPage.actions.hitUndo();

View file

@ -17,6 +17,7 @@ import {
openContextMenu, openContextMenu,
} from '../composables/workflow'; } from '../composables/workflow';
import { NDV, WorkflowExecutionsTab } from '../pages'; import { NDV, WorkflowExecutionsTab } from '../pages';
import { clearNotifications, successToast } from '../pages/notifications';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
const WorkflowPage = new WorkflowPageClass(); const WorkflowPage = new WorkflowPageClass();
@ -235,7 +236,11 @@ describe('Canvas Node Manipulation and Navigation', () => {
it('should delete node using context menu', () => { it('should delete node using context menu', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.deleteNodeFromContextMenu(CODE_NODE_NAME); WorkflowPage.actions.zoomToFit();
WorkflowPage.actions.deleteNodeFromContextMenu(CODE_NODE_NAME, {
method: 'right-click',
anchor: 'topLeft',
});
WorkflowPage.getters.canvasNodes().should('have.length', 1); WorkflowPage.getters.canvasNodes().should('have.length', 1);
WorkflowPage.getters.nodeConnections().should('have.length', 0); WorkflowPage.getters.nodeConnections().should('have.length', 0);
}); });
@ -379,6 +384,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
// At this point last added node should be off-screen // At this point last added node should be off-screen
WorkflowPage.getters.canvasNodes().last().should('not.be.visible'); WorkflowPage.getters.canvasNodes().last().should('not.be.visible');
WorkflowPage.getters.zoomToFitButton().click(); WorkflowPage.getters.zoomToFitButton().click();
@ -485,6 +491,9 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.executeWorkflow(); WorkflowPage.actions.executeWorkflow();
successToast().should('contain.text', 'Workflow executed successfully');
clearNotifications();
ExecutionsTab.actions.switchToExecutionsTab(); ExecutionsTab.actions.switchToExecutionsTab();
ExecutionsTab.getters.successfulExecutionListItems().should('have.length', 1); ExecutionsTab.getters.successfulExecutionListItems().should('have.length', 1);

View file

@ -489,7 +489,11 @@ describe('Execution', () => {
cy.wait('@workflowRun').then((interception) => { cy.wait('@workflowRun').then((interception) => {
expect(interception.request.body).to.have.property('runData').that.is.an('object'); expect(interception.request.body).to.have.property('runData').that.is.an('object');
const expectedKeys = ['When clicking Test workflow', 'fetch 5 random users']; const expectedKeys = [
'When clicking Test workflow',
'fetch 5 random users',
'do something with them',
];
const { runData } = interception.request.body as Record<string, object>; const { runData } = interception.request.body as Record<string, object>;
expect(Object.keys(runData)).to.have.lengthOf(expectedKeys.length); expect(Object.keys(runData)).to.have.lengthOf(expectedKeys.length);

View file

@ -27,8 +27,8 @@ describe('Workflow Executions', () => {
executionsTab.getters.executionsList().scrollTo(0, 500).wait(0); executionsTab.getters.executionsList().scrollTo(0, 500).wait(0);
executionsTab.getters.executionListItems().should('have.length', 11); executionsTab.getters.executionListItems().should('have.length', 30);
executionsTab.getters.successfulExecutionListItems().should('have.length', 9); executionsTab.getters.successfulExecutionListItems().should('have.length', 28);
executionsTab.getters.failedExecutionListItems().should('have.length', 2); executionsTab.getters.failedExecutionListItems().should('have.length', 2);
executionsTab.getters executionsTab.getters
.executionListItems() .executionListItems()
@ -185,8 +185,9 @@ describe('Workflow Executions', () => {
.invoke('attr', 'title') .invoke('attr', 'title')
.should('eq', newWorkflowName); .should('eq', newWorkflowName);
}); });
// This should be a component test. Abstracting this away into to ensure our lists work.
it('should load items and auto scroll after filter change', () => { // eslint-disable-next-line n8n-local-rules/no-skipped-tests
it.skip('should load items and auto scroll after filter change', () => {
createMockExecutions(); createMockExecutions();
createMockExecutions(); createMockExecutions();
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions'); cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
@ -289,15 +290,20 @@ describe('Workflow Executions', () => {
}); });
const createMockExecutions = () => { const createMockExecutions = () => {
executionsTab.actions.createManualExecutions(5); executionsTab.actions.createManualExecutions(15);
// This wait is added to allow time for the notifications to expire
cy.wait(2000);
// Make some failed executions by enabling Code node with syntax error // Make some failed executions by enabling Code node with syntax error
executionsTab.actions.toggleNodeEnabled('Error'); executionsTab.actions.toggleNodeEnabled('Error');
workflowPage.getters.disabledNodes().should('have.length', 0); workflowPage.getters.disabledNodes().should('have.length', 0);
executionsTab.actions.createManualExecutions(2); executionsTab.actions.createManualExecutions(2);
// This wait is added to allow time for the notifications to expire
cy.wait(2000);
// Then add some more successful ones // Then add some more successful ones
executionsTab.actions.toggleNodeEnabled('Error'); executionsTab.actions.toggleNodeEnabled('Error');
workflowPage.getters.disabledNodes().should('have.length', 1); workflowPage.getters.disabledNodes().should('have.length', 1);
executionsTab.actions.createManualExecutions(4); executionsTab.actions.createManualExecutions(15);
}; };
const checkMainHeaderELements = () => { const checkMainHeaderELements = () => {

View file

@ -54,11 +54,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
cy.changeQuota('maxTeamProjects', -1); cy.changeQuota('maxTeamProjects', -1);
}); });
/** it('should filter credentials by project ID when creating new workflow or hard reloading an opened workflow', () => {
* @TODO: New Canvas - Fix this test
*/
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
it.skip('should filter credentials by project ID when creating new workflow or hard reloading an opened workflow', () => {
cy.signinAsOwner(); cy.signinAsOwner();
cy.visit(workflowsPage.url); cy.visit(workflowsPage.url);
@ -225,10 +221,12 @@ describe('Projects', { disableAutoLogin: true }, () => {
}); });
it('should move resources between projects', () => { it('should move resources between projects', () => {
cy.intercept('GET', /\/rest\/(workflows|credentials).*/).as('getResources');
cy.signinAsOwner(); cy.signinAsOwner();
cy.visit(workflowsPage.url); cy.visit(workflowsPage.url);
// Create a workflow and a credential in the Home project cy.log('Create a workflow and a credential in the Home project');
workflowsPage.getters.workflowCards().should('not.have.length'); workflowsPage.getters.workflowCards().should('not.have.length');
workflowsPage.getters.newWorkflowButtonCard().click(); workflowsPage.getters.newWorkflowButtonCard().click();
projects.createWorkflow('Test_workflow_1.json', 'Workflow in Home project'); projects.createWorkflow('Test_workflow_1.json', 'Workflow in Home project');
@ -238,12 +236,12 @@ describe('Projects', { disableAutoLogin: true }, () => {
projects.getProjectTabCredentials().should('be.visible').click(); projects.getProjectTabCredentials().should('be.visible').click();
credentialsPage.getters.emptyListCreateCredentialButton().click(); credentialsPage.getters.emptyListCreateCredentialButton().click();
projects.createCredential('Credential in Home project'); projects.createCredential('Credential in Home project');
clearNotifications(); clearNotifications();
// Create a project and add a credential and a workflow to it cy.log('Create a project and add a credential and a workflow to it');
projects.createProject('Project 1'); projects.createProject('Project 1');
clearNotifications(); clearNotifications();
projects.getProjectTabCredentials().click(); projects.getProjectTabCredentials().click();
credentialsPage.getters.emptyListCreateCredentialButton().click(); credentialsPage.getters.emptyListCreateCredentialButton().click();
projects.createCredential('Credential in Project 1'); projects.createCredential('Credential in Project 1');
@ -252,12 +250,12 @@ describe('Projects', { disableAutoLogin: true }, () => {
projects.getProjectTabWorkflows().click(); projects.getProjectTabWorkflows().click();
workflowsPage.getters.newWorkflowButtonCard().click(); workflowsPage.getters.newWorkflowButtonCard().click();
projects.createWorkflow('Test_workflow_1.json', 'Workflow in Project 1'); projects.createWorkflow('Test_workflow_1.json', 'Workflow in Project 1');
clearNotifications(); clearNotifications();
// Create another project and add a credential and a workflow to it cy.log('Create another project and add a credential and a workflow to it');
projects.createProject('Project 2'); projects.createProject('Project 2');
clearNotifications(); clearNotifications();
projects.getProjectTabCredentials().click(); projects.getProjectTabCredentials().click();
credentialsPage.getters.emptyListCreateCredentialButton().click(); credentialsPage.getters.emptyListCreateCredentialButton().click();
projects.createCredential('Credential in Project 2'); projects.createCredential('Credential in Project 2');
@ -268,13 +266,10 @@ describe('Projects', { disableAutoLogin: true }, () => {
projects.createWorkflow('Test_workflow_1.json', 'Workflow in Project 2'); projects.createWorkflow('Test_workflow_1.json', 'Workflow in Project 2');
clearNotifications(); clearNotifications();
// Move the workflow Personal from Home to Project 1 cy.log('Move the workflow Personal from Home to Project 1');
projects.getHomeButton().click(); projects.getHomeButton().click();
workflowsPage.getters workflowsPage.getters.workflowCards().should('have.length', 3);
.workflowCards() workflowsPage.getters.workflowCards().filter(':contains("Personal")').should('exist');
.should('have.length', 3)
.filter(':contains("Personal")')
.should('exist');
workflowsPage.getters.workflowCardActions('Workflow in Home project').click(); workflowsPage.getters.workflowCardActions('Workflow in Home project').click();
workflowsPage.getters.workflowMoveButton().click(); workflowsPage.getters.workflowMoveButton().click();
@ -284,21 +279,16 @@ describe('Projects', { disableAutoLogin: true }, () => {
.contains('button', 'Move workflow') .contains('button', 'Move workflow')
.should('be.disabled'); .should('be.disabled');
projects.getProjectMoveSelect().click(); projects.getProjectMoveSelect().click();
getVisibleSelect() getVisibleSelect().find('li').should('have.length', 5);
.find('li') getVisibleSelect().find('li').filter(':contains("Project 1")').click();
.should('have.length', 5)
.filter(':contains("Project 1")')
.click();
projects.getResourceMoveModal().contains('button', 'Move workflow').click(); projects.getResourceMoveModal().contains('button', 'Move workflow').click();
clearNotifications(); clearNotifications();
cy.wait('@getResources');
workflowsPage.getters workflowsPage.getters.workflowCards().should('have.length', 3);
.workflowCards() workflowsPage.getters.workflowCards().filter(':contains("Personal")').should('not.exist');
.should('have.length', 3)
.filter(':contains("Personal")')
.should('not.exist');
// Move the workflow from Project 1 to Project 2 cy.log('Move the workflow from Project 1 to Project 2');
projects.getMenuItems().first().click(); projects.getMenuItems().first().click();
workflowsPage.getters.workflowCards().should('have.length', 2); workflowsPage.getters.workflowCards().should('have.length', 2);
workflowsPage.getters.workflowCardActions('Workflow in Home project').click(); workflowsPage.getters.workflowCardActions('Workflow in Home project').click();
@ -310,19 +300,16 @@ describe('Projects', { disableAutoLogin: true }, () => {
.contains('button', 'Move workflow') .contains('button', 'Move workflow')
.should('be.disabled'); .should('be.disabled');
projects.getProjectMoveSelect().click(); projects.getProjectMoveSelect().click();
getVisibleSelect() getVisibleSelect().find('li').should('have.length', 5);
.find('li') getVisibleSelect().find('li').filter(':contains("Project 2")').click();
.should('have.length', 5)
.filter(':contains("Project 2")')
.click();
projects.getResourceMoveModal().contains('button', 'Move workflow').click(); projects.getResourceMoveModal().contains('button', 'Move workflow').click();
clearNotifications();
// Move the workflow from Project 2 to a member user cy.log('Move the workflow from Project 2 to a member user');
projects.getMenuItems().last().click(); projects.getMenuItems().last().click();
workflowsPage.getters.workflowCards().should('have.length', 2); workflowsPage.getters.workflowCards().should('have.length', 2);
workflowsPage.getters.workflowCardActions('Workflow in Home project').click(); workflowsPage.getters.workflowCardActions('Workflow in Home project').click();
workflowsPage.getters.workflowMoveButton().click(); workflowsPage.getters.workflowMoveButton().click();
clearNotifications();
projects projects
.getResourceMoveModal() .getResourceMoveModal()
@ -330,20 +317,20 @@ describe('Projects', { disableAutoLogin: true }, () => {
.contains('button', 'Move workflow') .contains('button', 'Move workflow')
.should('be.disabled'); .should('be.disabled');
projects.getProjectMoveSelect().click(); projects.getProjectMoveSelect().click();
getVisibleSelect() getVisibleSelect().find('li').should('have.length', 5);
.find('li') getVisibleSelect().find('li').filter(`:contains("${INSTANCE_MEMBERS[0].email}")`).click();
.should('have.length', 5)
.filter(`:contains("${INSTANCE_MEMBERS[0].email}")`)
.click();
projects.getResourceMoveModal().contains('button', 'Move workflow').click(); projects.getResourceMoveModal().contains('button', 'Move workflow').click();
clearNotifications();
cy.wait('@getResources');
workflowsPage.getters.workflowCards().should('have.length', 1); workflowsPage.getters.workflowCards().should('have.length', 1);
// Move the workflow from member user back to Home cy.log('Move the workflow from member user back to Home');
projects.getHomeButton().click(); projects.getHomeButton().click();
workflowsPage.getters.workflowCards().should('have.length', 3);
workflowsPage.getters workflowsPage.getters
.workflowCards() .workflowCards()
.should('have.length', 3)
.filter(':has(.n8n-badge:contains("Project"))') .filter(':has(.n8n-badge:contains("Project"))')
.should('have.length', 2); .should('have.length', 2);
workflowsPage.getters.workflowCardActions('Workflow in Home project').click(); workflowsPage.getters.workflowCardActions('Workflow in Home project').click();
@ -355,21 +342,20 @@ describe('Projects', { disableAutoLogin: true }, () => {
.contains('button', 'Move workflow') .contains('button', 'Move workflow')
.should('be.disabled'); .should('be.disabled');
projects.getProjectMoveSelect().click(); projects.getProjectMoveSelect().click();
getVisibleSelect() getVisibleSelect().find('li').should('have.length', 5);
.find('li') getVisibleSelect().find('li').filter(`:contains("${INSTANCE_OWNER.email}")`).click();
.should('have.length', 5)
.filter(`:contains("${INSTANCE_OWNER.email}")`)
.click();
projects.getResourceMoveModal().contains('button', 'Move workflow').click(); projects.getResourceMoveModal().contains('button', 'Move workflow').click();
clearNotifications(); clearNotifications();
cy.wait('@getResources');
workflowsPage.getters.workflowCards().should('have.length', 3);
workflowsPage.getters workflowsPage.getters
.workflowCards() .workflowCards()
.should('have.length', 3)
.filter(':contains("Personal")') .filter(':contains("Personal")')
.should('have.length', 1); .should('have.length', 1);
// Move the credential from Project 1 to Project 2 cy.log('Move the credential from Project 1 to Project 2');
projects.getMenuItems().first().click(); projects.getMenuItems().first().click();
projects.getProjectTabCredentials().click(); projects.getProjectTabCredentials().click();
credentialsPage.getters.credentialCards().should('have.length', 1); credentialsPage.getters.credentialCards().should('have.length', 1);
@ -382,16 +368,15 @@ describe('Projects', { disableAutoLogin: true }, () => {
.contains('button', 'Move credential') .contains('button', 'Move credential')
.should('be.disabled'); .should('be.disabled');
projects.getProjectMoveSelect().click(); projects.getProjectMoveSelect().click();
getVisibleSelect() getVisibleSelect().find('li').should('have.length', 5);
.find('li') getVisibleSelect().find('li').filter(':contains("Project 2")').click();
.should('have.length', 5)
.filter(':contains("Project 2")')
.click();
projects.getResourceMoveModal().contains('button', 'Move credential').click(); projects.getResourceMoveModal().contains('button', 'Move credential').click();
clearNotifications(); clearNotifications();
cy.wait('@getResources');
credentialsPage.getters.credentialCards().should('not.have.length'); credentialsPage.getters.credentialCards().should('not.have.length');
// Move the credential from Project 2 to admin user cy.log('Move the credential from Project 2 to admin user');
projects.getMenuItems().last().click(); projects.getMenuItems().last().click();
projects.getProjectTabCredentials().click(); projects.getProjectTabCredentials().click();
credentialsPage.getters.credentialCards().should('have.length', 2); credentialsPage.getters.credentialCards().should('have.length', 2);
@ -405,15 +390,15 @@ describe('Projects', { disableAutoLogin: true }, () => {
.contains('button', 'Move credential') .contains('button', 'Move credential')
.should('be.disabled'); .should('be.disabled');
projects.getProjectMoveSelect().click(); projects.getProjectMoveSelect().click();
getVisibleSelect() getVisibleSelect().find('li').should('have.length', 5);
.find('li') getVisibleSelect().find('li').filter(`:contains("${INSTANCE_ADMIN.email}")`).click();
.should('have.length', 5)
.filter(`:contains("${INSTANCE_ADMIN.email}")`)
.click();
projects.getResourceMoveModal().contains('button', 'Move credential').click(); projects.getResourceMoveModal().contains('button', 'Move credential').click();
clearNotifications();
cy.wait('@getResources');
credentialsPage.getters.credentialCards().should('have.length', 1); credentialsPage.getters.credentialCards().should('have.length', 1);
// Move the credential from admin user back to instance owner cy.log('Move the credential from admin user back to instance owner');
projects.getHomeButton().click(); projects.getHomeButton().click();
projects.getProjectTabCredentials().click(); projects.getProjectTabCredentials().click();
credentialsPage.getters.credentialCards().should('have.length', 3); credentialsPage.getters.credentialCards().should('have.length', 3);
@ -427,22 +412,20 @@ describe('Projects', { disableAutoLogin: true }, () => {
.contains('button', 'Move credential') .contains('button', 'Move credential')
.should('be.disabled'); .should('be.disabled');
projects.getProjectMoveSelect().click(); projects.getProjectMoveSelect().click();
getVisibleSelect() getVisibleSelect().find('li').should('have.length', 5);
.find('li') getVisibleSelect().find('li').filter(`:contains("${INSTANCE_OWNER.email}")`).click();
.should('have.length', 5)
.filter(`:contains("${INSTANCE_OWNER.email}")`)
.click();
projects.getResourceMoveModal().contains('button', 'Move credential').click(); projects.getResourceMoveModal().contains('button', 'Move credential').click();
clearNotifications(); clearNotifications();
cy.wait('@getResources');
credentialsPage.getters.credentialCards().should('have.length', 3);
credentialsPage.getters credentialsPage.getters
.credentialCards() .credentialCards()
.should('have.length', 3)
.filter(':contains("Personal")') .filter(':contains("Personal")')
.should('have.length', 2); .should('have.length', 2);
// Move the credential from admin user back to its original project (Project 1) cy.log('Move the credential from admin user back to its original project (Project 1)');
credentialsPage.getters.credentialCardActions('Credential in Project 1').click(); credentialsPage.getters.credentialCardActions('Credential in Project 1').click();
credentialsPage.getters.credentialMoveButton().click(); credentialsPage.getters.credentialMoveButton().click();
@ -452,12 +435,10 @@ describe('Projects', { disableAutoLogin: true }, () => {
.contains('button', 'Move credential') .contains('button', 'Move credential')
.should('be.disabled'); .should('be.disabled');
projects.getProjectMoveSelect().click(); projects.getProjectMoveSelect().click();
getVisibleSelect() getVisibleSelect().find('li').should('have.length', 5);
.find('li') getVisibleSelect().find('li').filter(':contains("Project 1")').click();
.should('have.length', 5)
.filter(':contains("Project 1")')
.click();
projects.getResourceMoveModal().contains('button', 'Move credential').click(); projects.getResourceMoveModal().contains('button', 'Move credential').click();
clearNotifications();
projects.getMenuItems().first().click(); projects.getMenuItems().first().click();
projects.getProjectTabCredentials().click(); projects.getProjectTabCredentials().click();
@ -468,6 +449,8 @@ describe('Projects', { disableAutoLogin: true }, () => {
}); });
it('should allow to change inaccessible credential when the workflow was moved to a team project', () => { it('should allow to change inaccessible credential when the workflow was moved to a team project', () => {
cy.intercept('GET', /\/rest\/(workflows|credentials).*/).as('getResources');
cy.signinAsOwner(); cy.signinAsOwner();
cy.visit(workflowsPage.url); cy.visit(workflowsPage.url);
@ -489,15 +472,14 @@ describe('Projects', { disableAutoLogin: true }, () => {
// Create a project and add a user to it // Create a project and add a user to it
projects.createProject('Project 1'); projects.createProject('Project 1');
projects.addProjectMember(INSTANCE_MEMBERS[0].email); projects.addProjectMember(INSTANCE_MEMBERS[0].email);
clearNotifications();
projects.getProjectSettingsSaveButton().click(); projects.getProjectSettingsSaveButton().click();
// Move the workflow from Home to Project 1 // Move the workflow from Home to Project 1
projects.getHomeButton().click(); projects.getHomeButton().click();
workflowsPage.getters workflowsPage.getters.workflowCards().should('have.length', 1);
.workflowCards() workflowsPage.getters.workflowCards().filter(':contains("Personal")').should('exist');
.should('have.length', 1)
.filter(':contains("Personal")')
.should('exist');
workflowsPage.getters.workflowCardActions('My workflow').click(); workflowsPage.getters.workflowCardActions('My workflow').click();
workflowsPage.getters.workflowMoveButton().click(); workflowsPage.getters.workflowMoveButton().click();
@ -507,13 +489,13 @@ describe('Projects', { disableAutoLogin: true }, () => {
.contains('button', 'Move workflow') .contains('button', 'Move workflow')
.should('be.disabled'); .should('be.disabled');
projects.getProjectMoveSelect().click(); projects.getProjectMoveSelect().click();
getVisibleSelect() getVisibleSelect().find('li').should('have.length', 4);
.find('li') getVisibleSelect().find('li').filter(':contains("Project 1")').click();
.should('have.length', 4)
.filter(':contains("Project 1")')
.click();
projects.getResourceMoveModal().contains('button', 'Move workflow').click(); projects.getResourceMoveModal().contains('button', 'Move workflow').click();
clearNotifications();
cy.wait('@getResources');
workflowsPage.getters workflowsPage.getters
.workflowCards() .workflowCards()
.should('have.length', 1) .should('have.length', 1)

View file

@ -559,7 +559,7 @@ describe('Node Creator', () => {
addNodeToCanvas('Question and Answer Chain', true); addNodeToCanvas('Question and Answer Chain', true);
addRetrieverNodeToParent('Vector Store Retriever', 'Question and Answer Chain'); addRetrieverNodeToParent('Vector Store Retriever', 'Question and Answer Chain');
cy.realPress('Escape'); cy.realPress('Escape');
addVectorStoreNodeToParent('In-Memory Vector Store', 'Vector Store Retriever'); addVectorStoreNodeToParent('Simple Vector Store', 'Vector Store Retriever');
cy.realPress('Escape'); cy.realPress('Escape');
WorkflowPage.getters.canvasNodes().should('have.length', 4); WorkflowPage.getters.canvasNodes().should('have.length', 4);
}); });
@ -569,7 +569,7 @@ describe('Node Creator', () => {
addNodeToCanvas(AGENT_NODE_NAME, true, true); addNodeToCanvas(AGENT_NODE_NAME, true, true);
clickGetBackToCanvas(); clickGetBackToCanvas();
addVectorStoreToolToParent('In-Memory Vector Store', AGENT_NODE_NAME); addVectorStoreToolToParent('Simple Vector Store', AGENT_NODE_NAME);
}); });
it('should insert node to canvas with sendAndWait operation selected', () => { it('should insert node to canvas with sendAndWait operation selected', () => {

View file

@ -1,10 +1,23 @@
import {
clickAssignmentCollectionAdd,
clickGetBackToCanvas,
getNodeRunInfoStale,
getOutputTbodyCell,
} from '../composables/ndv';
import {
clickExecuteWorkflowButton,
getNodeByName,
getZoomToFitButton,
navigateToNewWorkflowPage,
openNode,
} from '../composables/workflow';
import { NDV, WorkflowPage } from '../pages'; import { NDV, WorkflowPage } from '../pages';
const canvas = new WorkflowPage(); const canvas = new WorkflowPage();
const ndv = new NDV(); const ndv = new NDV();
describe('Manual partial execution', () => { describe('Manual partial execution', () => {
it('should execute parent nodes with no run data only once', () => { it('should not execute parent nodes with no run data', () => {
canvas.actions.visit(); canvas.actions.visit();
cy.fixture('manual-partial-execution.json').then((data) => { cy.fixture('manual-partial-execution.json').then((data) => {
@ -22,8 +35,57 @@ describe('Manual partial execution', () => {
canvas.actions.openNode('Webhook1'); canvas.actions.openNode('Webhook1');
ndv.getters.nodeRunSuccessIndicator().should('exist'); ndv.getters.nodeRunSuccessIndicator().should('not.exist');
ndv.getters.nodeRunTooltipIndicator().should('exist'); ndv.getters.nodeRunTooltipIndicator().should('not.exist');
ndv.getters.outputRunSelector().should('not.exist'); // single run ndv.getters.outputRunSelector().should('not.exist');
});
describe('partial execution v2', () => {
beforeEach(() => {
cy.window().then((win) => {
win.localStorage.setItem('PartialExecution.version', '2');
});
navigateToNewWorkflowPage();
});
it('should execute from the first dirty node up to the current node', () => {
cy.createFixtureWorkflow('Test_workflow_partial_execution_v2.json');
getZoomToFitButton().click();
// First, execute the whole workflow
clickExecuteWorkflowButton();
getNodeByName('A').findChildByTestId('canvas-node-status-success').should('be.visible');
getNodeByName('B').findChildByTestId('canvas-node-status-success').should('be.visible');
getNodeByName('C').findChildByTestId('canvas-node-status-success').should('be.visible');
openNode('A');
getOutputTbodyCell(1, 0).invoke('text').as('before', { type: 'static' });
clickGetBackToCanvas();
// Change parameter of the node in the middle
openNode('B');
clickAssignmentCollectionAdd();
getNodeRunInfoStale().should('be.visible');
clickGetBackToCanvas();
getNodeByName('A').findChildByTestId('canvas-node-status-success').should('be.visible');
getNodeByName('B').findChildByTestId('canvas-node-status-warning').should('be.visible');
getNodeByName('C').findChildByTestId('canvas-node-status-success').should('be.visible');
// Partial execution
getNodeByName('C').findChildByTestId('execute-node-button').click();
getNodeByName('A').findChildByTestId('canvas-node-status-success').should('be.visible');
getNodeByName('B').findChildByTestId('canvas-node-status-success').should('be.visible');
getNodeByName('C').findChildByTestId('canvas-node-status-success').should('be.visible');
openNode('A');
getOutputTbodyCell(1, 0).invoke('text').as('after', { type: 'static' });
// Assert that 'A' ran only once by comparing its output
cy.get('@before').then((before) =>
cy.get('@after').then((after) => expect(before).to.equal(after)),
);
});
}); });
}); });

View file

@ -25,6 +25,18 @@
"value": "test", "value": "test",
"type": "string" "type": "string"
}, },
{
"id": "85095836-4e94-442f-9270-e1a89008c125",
"name": "test",
"value": "test",
"type": "string"
},
{
"id": "85095836-4e94-442f-9270-e1a89008c121",
"name": "test",
"value": "test",
"type": "string"
},
{ {
"id": "b6163f8a-bca6-4364-8b38-182df37c55cd", "id": "b6163f8a-bca6-4364-8b38-182df37c55cd",
"name": "=should be visible!", "name": "=should be visible!",
@ -50,6 +62,10 @@
"blocksUi": "blocks", "blocksUi": "blocks",
"text": "=should be visible", "text": "=should be visible",
"otherOptions": { "otherOptions": {
"includeLinkToWorkflow": true,
"link_names": false,
"mrkdwn": true,
"unfurl_links": false,
"sendAsUser": "=not visible" "sendAsUser": "=not visible"
} }
}, },
@ -67,6 +83,7 @@
"parameters": { "parameters": {
"rule": { "rule": {
"interval": [ "interval": [
{},
{}, {},
{ {
"field": "=should be visible" "field": "=should be visible"

View file

@ -0,0 +1,74 @@
{
"nodes": [
{
"parameters": {
"rule": {
"interval": [{}]
}
},
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [0, 0],
"id": "dcc1c5e1-c6c1-45f8-80d5-65c88d66d56e",
"name": "A"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "3d8f0810-84f0-41ce-a81b-0e7f04fd88cb",
"name": "",
"value": "",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [220, 0],
"id": "097ffa30-d37b-4de6-bd5c-ccd945f31df1",
"name": "B"
},
{
"parameters": {
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [440, 0],
"id": "dc44e635-916f-4f76-a745-1add5762f730",
"name": "C"
}
],
"connections": {
"A": {
"main": [
[
{
"node": "B",
"type": "main",
"index": 0
}
]
]
},
"B": {
"main": [
[
{
"node": "C",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {},
"meta": {
"instanceId": "b0d9447cff9c96796e4ac4f00fcd899b03cfac3ab3d4f748ae686d34881eae0c"
}
}

View file

@ -92,7 +92,10 @@ export class NDV extends BasePage {
resourceLocatorModeSelector: (paramName: string) => resourceLocatorModeSelector: (paramName: string) =>
this.getters.resourceLocator(paramName).find('[data-test-id="rlc-mode-selector"]'), this.getters.resourceLocator(paramName).find('[data-test-id="rlc-mode-selector"]'),
resourceLocatorSearch: (paramName: string) => resourceLocatorSearch: (paramName: string) =>
this.getters.resourceLocator(paramName).findChildByTestId('rlc-search'), this.getters
.resourceLocator(paramName)
.find('[aria-describedby]')
.then(($el) => cy.get(`#${$el.attr('aria-describedby')}`).findChildByTestId('rlc-search')),
resourceMapperFieldsContainer: () => cy.getByTestId('mapping-fields-container'), resourceMapperFieldsContainer: () => cy.getByTestId('mapping-fields-container'),
resourceMapperSelectColumn: () => cy.getByTestId('matching-column-select'), resourceMapperSelectColumn: () => cy.getByTestId('matching-column-select'),
resourceMapperRemoveFieldButton: (fieldName: string) => resourceMapperRemoveFieldButton: (fieldName: string) =>

View file

@ -13,10 +13,14 @@ export const infoToast = () => cy.get('.el-notification:has(.el-notification--in
* Actions * Actions
*/ */
export const clearNotifications = () => { export const clearNotifications = () => {
const buttons = successToast().find('.el-notification__closeBtn'); const notificationSelector = '.el-notification:has(.el-notification--success)';
buttons.then(($buttons) => { cy.get('body').then(($body) => {
if ($buttons.length) { if ($body.find(notificationSelector).length) {
buttons.click({ multiple: true }); cy.get(notificationSelector).each(($el) => {
if ($el.find('.el-notification__closeBtn').length) {
cy.wrap($el).find('.el-notification__closeBtn').click({ force: true });
}
});
} }
}); });
}; };

View file

@ -306,8 +306,8 @@ export class WorkflowPage extends BasePage {
this.actions.openContextMenu(nodeTypeName); this.actions.openContextMenu(nodeTypeName);
clickContextMenuAction('duplicate'); clickContextMenuAction('duplicate');
}, },
deleteNodeFromContextMenu: (nodeTypeName: string) => { deleteNodeFromContextMenu: (nodeTypeName: string, options?: OpenContextMenuOptions) => {
this.actions.openContextMenu(nodeTypeName); this.actions.openContextMenu(nodeTypeName, options);
clickContextMenuAction('delete'); clickContextMenuAction('delete');
}, },
executeNode: (nodeTypeName: string, options?: OpenContextMenuOptions) => { executeNode: (nodeTypeName: string, options?: OpenContextMenuOptions) => {

View file

@ -35,6 +35,7 @@ export class WorkflowsPage extends BasePage {
this.getters.workflowActivator(workflowName).findChildByTestId('workflow-activator-status'), this.getters.workflowActivator(workflowName).findChildByTestId('workflow-activator-status'),
workflowCardActions: (workflowName: string) => workflowCardActions: (workflowName: string) =>
this.getters.workflowCard(workflowName).findChildByTestId('workflow-card-actions'), this.getters.workflowCard(workflowName).findChildByTestId('workflow-card-actions'),
workflowActionItem: (action: string) => cy.getByTestId(`action-${action}`).filter(':visible'),
workflowDeleteButton: () => workflowDeleteButton: () =>
cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Delete'), cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Delete'),
workflowMoveButton: () => workflowMoveButton: () =>

View file

@ -1,7 +1,7 @@
ARG NODE_VERSION=20 ARG NODE_VERSION=20
# 1. Use a builder step to download various dependencies # 1. Use a builder step to download various dependencies
FROM node:${NODE_VERSION}-alpine as builder FROM node:${NODE_VERSION}-alpine AS builder
# Install fonts # Install fonts
RUN \ RUN \
@ -16,7 +16,7 @@ RUN apk add --update git openssh graphicsmagick tini tzdata ca-certificates libc
# Update npm and install full-uci # Update npm and install full-uci
COPY .npmrc /usr/local/etc/npmrc COPY .npmrc /usr/local/etc/npmrc
RUN npm install -g npm@9.9.2 corepack@0.31 full-icu@1.5.0 RUN npm install -g corepack@0.31 full-icu@1.5.0
# Activate corepack, and install pnpm # Activate corepack, and install pnpm
WORKDIR /tmp WORKDIR /tmp
@ -34,5 +34,5 @@ COPY --from=builder / /
RUN rm -rf /tmp/v8-compile-cache* RUN rm -rf /tmp/v8-compile-cache*
WORKDIR /home/node WORKDIR /home/node
ENV NODE_ICU_DATA /usr/local/lib/node_modules/full-icu ENV NODE_ICU_DATA=/usr/local/lib/node_modules/full-icu
EXPOSE 5678/tcp EXPOSE 5678/tcp

View file

@ -33,7 +33,7 @@ COPY docker/images/n8n/docker-entrypoint.sh /
# Setup the Task Runner Launcher # Setup the Task Runner Launcher
ARG TARGETPLATFORM ARG TARGETPLATFORM
ARG LAUNCHER_VERSION=1.1.0 ARG LAUNCHER_VERSION=1.1.1
COPY docker/images/n8n/n8n-task-runners.json /etc/n8n-task-runners.json COPY docker/images/n8n/n8n-task-runners.json /etc/n8n-task-runners.json
# Download, verify, then extract the launcher binary # Download, verify, then extract the launcher binary
RUN \ RUN \

View file

@ -4,27 +4,30 @@ FROM n8nio/base:${NODE_VERSION}
ARG N8N_VERSION ARG N8N_VERSION
RUN if [ -z "$N8N_VERSION" ] ; then echo "The N8N_VERSION argument is missing!" ; exit 1; fi RUN if [ -z "$N8N_VERSION" ] ; then echo "The N8N_VERSION argument is missing!" ; exit 1; fi
ARG N8N_RELEASE_DATE
LABEL org.opencontainers.image.title="n8n" LABEL org.opencontainers.image.title="n8n"
LABEL org.opencontainers.image.description="Workflow Automation Tool" LABEL org.opencontainers.image.description="Workflow Automation Tool"
LABEL org.opencontainers.image.source="https://github.com/n8n-io/n8n" LABEL org.opencontainers.image.source="https://github.com/n8n-io/n8n"
LABEL org.opencontainers.image.url="https://n8n.io" LABEL org.opencontainers.image.url="https://n8n.io"
LABEL org.opencontainers.image.version=${N8N_VERSION} LABEL org.opencontainers.image.version=${N8N_VERSION}
LABEL org.opencontainers.image.created=${N8N_RELEASE_DATE}
ENV N8N_VERSION=${N8N_VERSION} ENV N8N_VERSION=${N8N_VERSION}
ENV NODE_ENV=production ENV NODE_ENV=production
ENV N8N_RELEASE_TYPE=stable ENV N8N_RELEASE_TYPE=stable
ENV N8N_RELEASE_DATE=${N8N_RELEASE_DATE}
RUN set -eux; \ RUN set -eux; \
npm install -g --omit=dev n8n@${N8N_VERSION} --ignore-scripts && \ npm install -g --omit=dev n8n@${N8N_VERSION} --ignore-scripts && \
npm rebuild --prefix=/usr/local/lib/node_modules/n8n sqlite3 && \ npm rebuild --prefix=/usr/local/lib/node_modules/n8n sqlite3 && \
rm -rf /usr/local/lib/node_modules/n8n/node_modules/@n8n/chat && \ rm -rf /usr/local/lib/node_modules/n8n/node_modules/@n8n/chat && \
rm -rf /usr/local/lib/node_modules/n8n/node_modules/n8n-design-system && \ rm -rf /usr/local/lib/node_modules/n8n/node_modules/@n8n/design-system && \
rm -rf /usr/local/lib/node_modules/n8n/node_modules/n8n-editor-ui/node_modules && \ rm -rf /usr/local/lib/node_modules/n8n/node_modules/n8n-editor-ui/node_modules && \
find /usr/local/lib/node_modules/n8n -type f -name "*.ts" -o -name "*.js.map" -o -name "*.vue" | xargs rm -f && \ find /usr/local/lib/node_modules/n8n -type f -name "*.ts" -o -name "*.js.map" -o -name "*.vue" | xargs rm -f && \
rm -rf /root/.npm rm -rf /root/.npm
# Setup the Task Runner Launcher # Setup the Task Runner Launcher
ARG TARGETPLATFORM ARG TARGETPLATFORM
ARG LAUNCHER_VERSION=1.1.0 ARG LAUNCHER_VERSION=1.1.1
COPY n8n-task-runners.json /etc/n8n-task-runners.json COPY n8n-task-runners.json /etc/n8n-task-runners.json
# Download, verify, then extract the launcher binary # Download, verify, then extract the launcher binary
RUN \ RUN \

View file

@ -12,6 +12,8 @@ const tsJestOptions = {
const { baseUrl, paths } = require('get-tsconfig').getTsconfig().config?.compilerOptions; const { baseUrl, paths } = require('get-tsconfig').getTsconfig().config?.compilerOptions;
const isCoverageEnabled = process.env.COVERAGE_ENABLED === 'true';
/** @type {import('jest').Config} */ /** @type {import('jest').Config} */
const config = { const config = {
verbose: true, verbose: true,
@ -32,8 +34,8 @@ const config = {
return acc; return acc;
}, {}), }, {}),
setupFilesAfterEnv: ['jest-expect-message'], setupFilesAfterEnv: ['jest-expect-message'],
collectCoverage: process.env.COVERAGE_ENABLED === 'true', collectCoverage: isCoverageEnabled,
coverageReporters: ['text-summary'], coverageReporters: ['text-summary', 'lcov', 'html-spa'],
collectCoverageFrom: ['src/**/*.ts'], collectCoverageFrom: ['src/**/*.ts'],
}; };

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n-monorepo", "name": "n8n-monorepo",
"version": "1.79.0", "version": "1.81.0",
"private": true, "private": true,
"engines": { "engines": {
"node": ">=20.15", "node": ">=20.15",
@ -15,10 +15,10 @@
"build:frontend": "turbo run build:frontend", "build:frontend": "turbo run build:frontend",
"build:nodes": "turbo run build:nodes", "build:nodes": "turbo run build:nodes",
"typecheck": "turbo typecheck", "typecheck": "turbo typecheck",
"dev": "turbo run dev --parallel --env-mode=loose --filter=!n8n-design-system --filter=!@n8n/chat --filter=!@n8n/task-runner", "dev": "turbo run dev --parallel --env-mode=loose --filter=!@n8n/design-system --filter=!@n8n/chat --filter=!@n8n/task-runner",
"dev:be": "turbo run dev --parallel --env-mode=loose --filter=!n8n-design-system --filter=!@n8n/chat --filter=!@n8n/task-runner --filter=!n8n-editor-ui", "dev:be": "turbo run dev --parallel --env-mode=loose --filter=!@n8n/design-system --filter=!@n8n/chat --filter=!@n8n/task-runner --filter=!n8n-editor-ui",
"dev:ai": "turbo run dev --parallel --env-mode=loose --filter=@n8n/nodes-langchain --filter=n8n --filter=n8n-core", "dev:ai": "turbo run dev --parallel --env-mode=loose --filter=@n8n/nodes-langchain --filter=n8n --filter=n8n-core",
"dev:fe": "run-p start \"dev:fe:editor --filter=n8n-design-system\"", "dev:fe": "run-p start \"dev:fe:editor --filter=@n8n/design-system\"",
"dev:fe:editor": "turbo run dev --parallel --env-mode=loose --filter=n8n-editor-ui", "dev:fe:editor": "turbo run dev --parallel --env-mode=loose --filter=n8n-editor-ui",
"dev:e2e": "cd cypress && pnpm run test:e2e:dev", "dev:e2e": "cd cypress && pnpm run test:e2e:dev",
"dev:e2e:v1": "cd cypress && pnpm run test:e2e:dev:v1", "dev:e2e:v1": "cd cypress && pnpm run test:e2e:dev:v1",
@ -47,7 +47,7 @@
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.9.0", "@biomejs/biome": "^1.9.0",
"@n8n_io/eslint-config": "workspace:*", "@n8n/eslint-config": "workspace:*",
"@types/jest": "^29.5.3", "@types/jest": "^29.5.3",
"@types/node": "*", "@types/node": "*",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
@ -96,7 +96,8 @@
"@types/express-serve-static-core@4.17.43": "patches/@types__express-serve-static-core@4.17.43.patch", "@types/express-serve-static-core@4.17.43": "patches/@types__express-serve-static-core@4.17.43.patch",
"@types/ws@8.5.4": "patches/@types__ws@8.5.4.patch", "@types/ws@8.5.4": "patches/@types__ws@8.5.4.patch",
"@types/uuencode@0.0.3": "patches/@types__uuencode@0.0.3.patch", "@types/uuencode@0.0.3": "patches/@types__uuencode@0.0.3.patch",
"vue-tsc@2.1.10": "patches/vue-tsc@2.1.10.patch" "vue-tsc@2.1.10": "patches/vue-tsc@2.1.10.patch",
"eslint-plugin-n8n-local-rules": "patches/eslint-plugin-n8n-local-rules.patch"
} }
} }
} }

View file

@ -1,7 +1,7 @@
const sharedOptions = require('@n8n_io/eslint-config/shared'); const sharedOptions = require('@n8n/eslint-config/shared');
/** @type {import('@types/eslint').ESLint.ConfigData} */ /** @type {import('@types/eslint').ESLint.ConfigData} */
module.exports = { module.exports = {
extends: ['@n8n_io/eslint-config/base'], extends: ['@n8n/eslint-config/base'],
...sharedOptions(__dirname), ...sharedOptions(__dirname),
}; };

View file

@ -1,6 +1,6 @@
{ {
"name": "@n8n/api-types", "name": "@n8n/api-types",
"version": "0.15.0", "version": "0.16.0",
"scripts": { "scripts": {
"clean": "rimraf dist .turbo", "clean": "rimraf dist .turbo",
"dev": "pnpm watch", "dev": "pnpm watch",
@ -21,6 +21,7 @@
"dist/**/*" "dist/**/*"
], ],
"devDependencies": { "devDependencies": {
"@n8n/typescript-config": "workspace:*",
"@n8n/config": "workspace:*", "@n8n/config": "workspace:*",
"n8n-workflow": "workspace:*" "n8n-workflow": "workspace:*"
}, },

View file

@ -0,0 +1,65 @@
import { CreateFolderDto } from '../create-folder.dto';
describe('CreateFolderDto', () => {
describe('Valid requests', () => {
test.each([
{
name: 'name without parentId',
request: {
name: 'test',
},
},
{
name: 'name and parentFolderId',
request: {
name: 'test',
parentFolderId: '2Hw01NJ7biAj_LU6',
},
},
])('should validate $name', ({ request }) => {
const result = CreateFolderDto.safeParse(request);
expect(result.success).toBe(true);
});
});
describe('Invalid requests', () => {
test.each([
{
name: 'missing name',
request: {},
expectedErrorPath: ['name'],
},
{
name: 'empty name',
request: {
name: '',
},
expectedErrorPath: ['name'],
},
{
name: 'parentFolderId and no name',
request: {
parentFolderId: '',
},
expectedErrorPath: ['name'],
},
{
name: 'invalid parentFolderId',
request: {
name: 'test',
parentFolderId: 1,
},
expectedErrorPath: ['parentFolderId'],
},
])('should fail validation for $name', ({ request, expectedErrorPath }) => {
const result = CreateFolderDto.safeParse(request);
expect(result.success).toBe(false);
if (expectedErrorPath) {
expect(result.error?.issues[0].path).toEqual(expectedErrorPath);
}
});
});
});

View file

@ -0,0 +1,63 @@
import { UpdateFolderDto } from '../update-folder.dto';
describe('UpdateFolderDto', () => {
describe('Valid requests', () => {
test.each([
{
name: 'name',
request: {
name: 'test',
},
},
{
name: 'tagIds',
request: {
tagIds: ['1', '2'],
},
},
{
name: 'empty tagIds',
request: {
tagIds: [],
},
},
])('should validate $name', ({ request }) => {
const result = UpdateFolderDto.safeParse(request);
expect(result.success).toBe(true);
});
});
describe('Invalid requests', () => {
test.each([
{
name: 'empty name',
request: {
name: '',
},
expectedErrorPath: ['name'],
},
{
name: 'non string tagIds',
request: {
tagIds: [0],
},
expectedErrorPath: ['tagIds'],
},
{
name: 'non array tagIds',
request: {
tagIds: 0,
},
expectedErrorPath: ['tagIds'],
},
])('should fail validation for $name', ({ request, expectedErrorPath }) => {
const result = UpdateFolderDto.safeParse(request);
expect(result.success).toBe(false);
if (expectedErrorPath) {
expect(result.error?.issues[0].path[0]).toEqual(expectedErrorPath[0]);
}
});
});
});

View file

@ -0,0 +1,8 @@
import { Z } from 'zod-class';
import { folderNameSchema, folderId } from '../../schemas/folder.schema';
export class CreateFolderDto extends Z.class({
name: folderNameSchema,
parentFolderId: folderId.optional(),
}) {}

View file

@ -0,0 +1,7 @@
import { Z } from 'zod-class';
import { folderId } from '../../schemas/folder.schema';
export class DeleteFolderDto extends Z.class({
transferToFolderId: folderId.optional(),
}) {}

View file

@ -0,0 +1,8 @@
import { z } from 'zod';
import { Z } from 'zod-class';
import { folderNameSchema } from '../../schemas/folder.schema';
export class UpdateFolderDto extends Z.class({
name: folderNameSchema.optional(),
tagIds: z.array(z.string().max(24)).optional(),
}) {}

View file

@ -47,9 +47,14 @@ export { GenerateCredentialNameRequestQuery } from './credentials/generate-crede
export { ImportWorkflowFromUrlDto } from './workflows/import-workflow-from-url.dto'; export { ImportWorkflowFromUrlDto } from './workflows/import-workflow-from-url.dto';
export { ManualRunQueryDto } from './workflows/manual-run-query.dto'; export { ManualRunQueryDto } from './workflows/manual-run-query.dto';
export { TransferWorkflowBodyDto } from './workflows/transfer.dto';
export { CreateOrUpdateTagRequestDto } from './tag/create-or-update-tag-request.dto'; export { CreateOrUpdateTagRequestDto } from './tag/create-or-update-tag-request.dto';
export { RetrieveTagQueryDto } from './tag/retrieve-tag-query.dto'; export { RetrieveTagQueryDto } from './tag/retrieve-tag-query.dto';
export { UpdateApiKeyRequestDto } from './api-keys/update-api-key-request.dto'; export { UpdateApiKeyRequestDto } from './api-keys/update-api-key-request.dto';
export { CreateApiKeyRequestDto } from './api-keys/create-api-key-request.dto'; export { CreateApiKeyRequestDto } from './api-keys/create-api-key-request.dto';
export { CreateFolderDto } from './folders/create-folder.dto';
export { UpdateFolderDto } from './folders/update-folder.dto';
export { DeleteFolderDto } from './folders/delete-folder.dto';

View file

@ -0,0 +1,56 @@
import { TransferWorkflowBodyDto } from '../transfer.dto';
describe('ImportWorkflowFromUrlDto', () => {
describe('Valid requests', () => {
test.each([
{
name: 'only destinationProjectId',
input: { destinationProjectId: '1234' },
},
{
name: 'destinationProjectId with empty shareCredentials',
input: { destinationProjectId: '1234', shareCredentials: [] },
},
{
name: 'destinationProjectId with shareCredentials',
input: { destinationProjectId: '1234', shareCredentials: ['1235'] },
},
])('should validate $name', ({ input }) => {
const result = TransferWorkflowBodyDto.safeParse(input);
expect(result.success).toBe(true);
});
});
describe('Invalid requests', () => {
test.each([
{
name: 'no destinationProjectId',
input: { shareCredentials: [] },
expectedErrorPath: ['destinationProjectId'],
},
{
name: 'destinationProjectId not being a string',
input: { destinationProjectId: 1234 },
expectedErrorPath: ['destinationProjectId'],
},
{
name: 'shareCredentials not being an array',
input: { destinationProjectId: '1234', shareCredentials: '1235' },
expectedErrorPath: ['shareCredentials'],
},
{
name: 'shareCredentials not containing strings',
input: { destinationProjectId: '1234', shareCredentials: [1235] },
expectedErrorPath: ['shareCredentials', 0],
},
])('should fail validation for $name', ({ input, expectedErrorPath }) => {
const result = TransferWorkflowBodyDto.safeParse(input);
expect(result.success).toBe(false);
if (expectedErrorPath) {
expect(result.error?.issues[0].path).toEqual(expectedErrorPath);
}
});
});
});

View file

@ -0,0 +1,7 @@
import { z } from 'zod';
import { Z } from 'zod-class';
export class TransferWorkflowBodyDto extends Z.class({
destinationProjectId: z.string(),
shareCredentials: z.array(z.string()).optional(),
}) {}

View file

@ -86,7 +86,6 @@ export interface FrontendSettings {
}; };
}; };
publicApi: { publicApi: {
apiKeysPerUserLimit: number;
enabled: boolean; enabled: boolean;
latestVersion: number; latestVersion: number;
path: string; path: string;
@ -156,6 +155,9 @@ export interface FrontendSettings {
mfa: { mfa: {
enabled: boolean; enabled: boolean;
}; };
folders: {
enabled: boolean;
};
banners: { banners: {
dismissed: string[]; dismissed: string[];
}; };
@ -178,6 +180,5 @@ export interface FrontendSettings {
easyAIWorkflowOnboarded: boolean; easyAIWorkflowOnboarded: boolean;
partialExecution: { partialExecution: {
version: 1 | 2; version: 1 | 2;
enforce: boolean;
}; };
} }

View file

@ -0,0 +1,4 @@
import { z } from 'zod';
export const folderNameSchema = z.string().trim().min(1).max(128);
export const folderId = z.string().max(36);

View file

@ -1,5 +1,5 @@
{ {
"extends": ["./tsconfig.json", "../../../tsconfig.build.json"], "extends": ["./tsconfig.json", "@n8n/typescript-config/tsconfig.build.json"],
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"rootDir": "src", "rootDir": "src",

View file

@ -1,5 +1,5 @@
{ {
"extends": "../../../tsconfig.json", "extends": "@n8n/typescript-config/tsconfig.common.json",
"compilerOptions": { "compilerOptions": {
"rootDir": ".", "rootDir": ".",
"types": ["node", "jest"], "types": ["node", "jest"],

View file

@ -1,10 +1,10 @@
const sharedOptions = require('@n8n_io/eslint-config/shared'); const sharedOptions = require('@n8n/eslint-config/shared');
/** /**
* @type {import('@types/eslint').ESLint.ConfigData} * @type {import('@types/eslint').ESLint.ConfigData}
*/ */
module.exports = { module.exports = {
extends: ['@n8n_io/eslint-config/node'], extends: ['@n8n/eslint-config/node'],
...sharedOptions(__dirname), ...sharedOptions(__dirname),

View file

@ -37,9 +37,9 @@ ENV DOCKER_BUILD=true
RUN pnpm install --frozen-lockfile RUN pnpm install --frozen-lockfile
# TS config files # TS config files
COPY --chown=node:node ./tsconfig.json /app/tsconfig.json COPY --chown=node:node ./packages/@n8n/typescript-config/tsconfig.common.json /app/packages/@n8n/typescript-config/tsconfig.common.json
COPY --chown=node:node ./tsconfig.build.json /app/tsconfig.build.json COPY --chown=node:node ./packages/@n8n/typescript-config/tsconfig.build.json /app/packages/@n8n/typescript-config/tsconfig.build.json
COPY --chown=node:node ./tsconfig.backend.json /app/tsconfig.backend.json COPY --chown=node:node ./packages/@n8n/typescript-config/tsconfig.backend.json /app/packages/@n8n/typescript-config/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.json /app/packages/@n8n/benchmark/tsconfig.json
COPY --chown=node:node ./packages/@n8n/benchmark/tsconfig.build.json /app/packages/@n8n/benchmark/tsconfig.build.json COPY --chown=node:node ./packages/@n8n/benchmark/tsconfig.build.json /app/packages/@n8n/benchmark/tsconfig.build.json

View file

@ -40,6 +40,7 @@
"zx": "^8.1.4" "zx": "^8.1.4"
}, },
"devDependencies": { "devDependencies": {
"@n8n/typescript-config": "workspace:*",
"@types/convict": "^6.1.1", "@types/convict": "^6.1.1",
"@types/k6": "^0.52.0" "@types/k6": "^0.52.0"
}, },

View file

@ -1,5 +1,5 @@
{ {
"extends": ["./tsconfig.json", "../../../tsconfig.build.json"], "extends": ["./tsconfig.json", "@n8n/typescript-config/tsconfig.build.json"],
"compilerOptions": { "compilerOptions": {
"rootDir": "src", "rootDir": "src",
"outDir": "dist", "outDir": "dist",

View file

@ -1,5 +1,8 @@
{ {
"extends": ["../../../tsconfig.json", "../../../tsconfig.backend.json"], "extends": [
"@n8n/typescript-config/tsconfig.common.json",
"@n8n/typescript-config/tsconfig.backend.json"
],
"compilerOptions": { "compilerOptions": {
"rootDir": ".", "rootDir": ".",
"baseUrl": "src", "baseUrl": "src",

View file

@ -1,4 +0,0 @@
html, body, #storybook-root, #n8n-chat {
width: 100%;
height: 100%;
}

View file

@ -1,3 +0,0 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}

View file

@ -1,238 +0,0 @@
{
"name": "Hosted n8n AI Chat Manual",
"nodes": [
{
"parameters": {
"options": {}
},
"id": "e6043748-44fc-4019-9301-5690fe26c614",
"name": "OpenAI Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"typeVersion": 1,
"position": [
860,
540
],
"credentials": {
"openAiApi": {
"id": "cIIkOhl7tUX1KsL6",
"name": "OpenAi account"
}
}
},
{
"parameters": {
"sessionKey": "={{ $json.sessionId }}"
},
"id": "0a68a59a-8ab6-4fa5-a1ea-b7f99a93109b",
"name": "Window Buffer Memory",
"type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
"typeVersion": 1,
"position": [
640,
540
]
},
{
"parameters": {
"text": "={{ $json.chatInput }}",
"options": {}
},
"id": "3d4e0fbf-d761-4569-b02e-f5c1eeb830c8",
"name": "AI Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 1.1,
"position": [
840,
300
]
},
{
"parameters": {
"dataType": "string",
"value1": "={{ $json.action }}",
"rules": {
"rules": [
{
"value2": "loadPreviousSession",
"outputKey": "loadPreviousSession"
},
{
"value2": "sendMessage",
"outputKey": "sendMessage"
}
]
}
},
"id": "84213c7b-abc7-4f40-9567-cd3484a4ae6b",
"name": "Switch",
"type": "n8n-nodes-base.switch",
"typeVersion": 2,
"position": [
300,
280
]
},
{
"parameters": {
"simplifyOutput": false
},
"id": "3be7f076-98ed-472a-80b6-bf8d9538ac87",
"name": "Chat Messages Retriever",
"type": "@n8n/n8n-nodes-langchain.memoryChatRetriever",
"typeVersion": 1,
"position": [
620,
140
]
},
{
"parameters": {
"options": {}
},
"id": "3417c644-8a91-4524-974a-45b4a46d0e2e",
"name": "Respond to Webhook",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
1240,
140
]
},
{
"parameters": {
"public": true,
"authentication": "n8nUserAuth",
"options": {
"loadPreviousSession": "manually",
"responseMode": "responseNode"
}
},
"id": "1b30c239-a819-45b4-b0ae-bdd5b92a5424",
"name": "Chat Trigger",
"type": "@n8n/n8n-nodes-langchain.chatTrigger",
"typeVersion": 1,
"position": [
80,
280
],
"webhookId": "ed3dea26-7d68-42b3-9032-98fe967d441d"
},
{
"parameters": {
"aggregate": "aggregateAllItemData",
"options": {}
},
"id": "79672cf0-686b-41eb-90ae-fd31b6da837d",
"name": "Aggregate",
"type": "n8n-nodes-base.aggregate",
"typeVersion": 1,
"position": [
1000,
140
]
}
],
"pinData": {},
"connections": {
"OpenAI Chat Model": {
"ai_languageModel": [
[
{
"node": "AI Agent",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Window Buffer Memory": {
"ai_memory": [
[
{
"node": "AI Agent",
"type": "ai_memory",
"index": 0
},
{
"node": "Chat Messages Retriever",
"type": "ai_memory",
"index": 0
}
]
]
},
"Switch": {
"main": [
[
{
"node": "Chat Messages Retriever",
"type": "main",
"index": 0
}
],
[
{
"node": "AI Agent",
"type": "main",
"index": 0
}
]
]
},
"Chat Messages Retriever": {
"main": [
[
{
"node": "Aggregate",
"type": "main",
"index": 0
}
]
]
},
"AI Agent": {
"main": [
[
{
"node": "Respond to Webhook",
"type": "main",
"index": 0
}
]
]
},
"Chat Trigger": {
"main": [
[
{
"node": "Switch",
"type": "main",
"index": 0
}
]
]
},
"Aggregate": {
"main": [
[
{
"node": "Respond to Webhook",
"type": "main",
"index": 0
}
]
]
}
},
"active": true,
"settings": {
"executionOrder": "v1"
},
"versionId": "425c0efe-3aa0-4e0e-8c06-abe12234b1fd",
"id": "1569HF92Y02EUtsU",
"meta": {
"instanceId": "374b43d8b8d6299cc777811a4ad220fc688ee2d54a308cfb0de4450a5233ca9e"
},
"tags": []
}

View file

@ -1,38 +0,0 @@
:root {
--chat--color-primary: #e74266;
--chat--color-primary-shade-50: #db4061;
--chat--color-primary-shade-100: #cf3c5c;
--chat--color-secondary: #20b69e;
--chat--color-secondary-shade-50: #1ca08a;
--chat--color-white: #ffffff;
--chat--color-light: #f2f4f8;
--chat--color-light-shade-50: #e6e9f1;
--chat--color-light-shade-100: #c2c5cc;
--chat--color-medium: #d2d4d9;
--chat--color-dark: #101330;
--chat--color-disabled: #777980;
--chat--color-typing: #404040;
--chat--spacing: 1rem;
--chat--border-radius: 0.25rem;
--chat--transition-duration: 0.15s;
--chat--window--width: 400px;
--chat--window--height: 600px;
--chat--textarea--height: 50px;
--chat--message--bot--background: var(--chat--color-white);
--chat--message--bot--color: var(--chat--color-dark);
--chat--message--user--background: var(--chat--color-secondary);
--chat--message--user--color: var(--chat--color-white);
--chat--message--pre--background: rgba(0, 0, 0, 0.05);
--chat--toggle--background: var(--chat--color-primary);
--chat--toggle--hover--background: var(--chat--color-primary-shade-50);
--chat--toggle--active--background: var(--chat--color-primary-shade-100);
--chat--toggle--color: var(--chat--color-white);
--chat--toggle--size: 64px;
--chat--heading--font-size: 2em;
}

View file

@ -1,53 +0,0 @@
import { defineConfig } from 'vite';
import { resolve } from 'path';
import vue from '@vitejs/plugin-vue';
import icons from 'unplugin-icons/vite';
import dts from 'vite-plugin-dts';
const includeVue = process.env.INCLUDE_VUE === 'true';
const srcPath = resolve(__dirname, 'src');
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
icons({
compiler: 'vue3',
autoInstall: true,
}),
dts(),
],
resolve: {
alias: {
'@': srcPath,
'@n8n/chat': srcPath,
lodash: 'lodash-es',
},
},
define: {
'process.env.NODE_ENV': process.env.NODE_ENV ? `"${process.env.NODE_ENV}"` : '"development"',
},
build: {
emptyOutDir: !includeVue,
lib: {
entry: resolve(__dirname, 'src', 'index.ts'),
name: 'N8nChat',
fileName: (format) => (includeVue ? `chat.bundle.${format}.js` : `chat.${format}.js`),
},
rollupOptions: {
// make sure to externalize deps that shouldn't be bundled
// into your library
external: includeVue ? [] : ['vue'],
output: {
exports: 'named',
// Provide global variables to use in the UMD build
// for externalized deps
globals: includeVue
? {}
: {
vue: 'Vue',
},
},
},
},
});

View file

@ -1,30 +0,0 @@
import { resolve } from 'path';
import { mergeConfig } from 'vite';
import { type UserConfig } from 'vitest';
import { defineConfig } from 'vitest/config';
import viteConfig from './vite.config.mts';
const srcPath = resolve(__dirname, 'src');
const vitestConfig = defineConfig({
test: {
globals: true,
environment: 'jsdom',
root: srcPath,
setupFiles: ['./src/__tests__/setup.ts'],
...(process.env.COVERAGE_ENABLED === 'true'
? {
coverage: {
enabled: true,
provider: 'v8',
reporter: process.env.CI === 'true' ? 'cobertura' : 'text-summary',
all: true,
},
}
: {}),
},
}) as UserConfig;
export default mergeConfig(
viteConfig,
vitestConfig,
);

View file

@ -1,10 +1,10 @@
const sharedOptions = require('@n8n_io/eslint-config/shared'); const sharedOptions = require('@n8n/eslint-config/shared');
/** /**
* @type {import('@types/eslint').ESLint.ConfigData} * @type {import('@types/eslint').ESLint.ConfigData}
*/ */
module.exports = { module.exports = {
extends: ['@n8n_io/eslint-config/base'], extends: ['@n8n/eslint-config/base'],
...sharedOptions(__dirname), ...sharedOptions(__dirname),

View file

@ -22,5 +22,8 @@
], ],
"dependencies": { "dependencies": {
"axios": "catalog:" "axios": "catalog:"
},
"devDependencies": {
"@n8n/typescript-config": "workspace:*"
} }
} }

View file

@ -1,5 +1,5 @@
{ {
"extends": ["./tsconfig.json", "../../../tsconfig.build.json"], "extends": ["./tsconfig.json", "@n8n/typescript-config/tsconfig.build.json"],
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"rootDir": "src", "rootDir": "src",

View file

@ -1,5 +1,5 @@
{ {
"extends": "../../../tsconfig.json", "extends": "@n8n/typescript-config/tsconfig.common.json",
"compilerOptions": { "compilerOptions": {
"rootDir": ".", "rootDir": ".",
"types": ["node", "jest"], "types": ["node", "jest"],

View file

@ -1,10 +1,10 @@
const sharedOptions = require('@n8n_io/eslint-config/shared'); const sharedOptions = require('@n8n/eslint-config/shared');
/** /**
* @type {import('@types/eslint').ESLint.ConfigData} * @type {import('@types/eslint').ESLint.ConfigData}
*/ */
module.exports = { module.exports = {
extends: ['@n8n_io/eslint-config/base'], extends: ['@n8n/eslint-config/base'],
...sharedOptions(__dirname), ...sharedOptions(__dirname),

View file

@ -33,6 +33,7 @@
"@lezer/lr": "^1.4.0" "@lezer/lr": "^1.4.0"
}, },
"devDependencies": { "devDependencies": {
"@n8n/typescript-config": "workspace:*",
"@lezer/generator": "^1.7.0" "@lezer/generator": "^1.7.0"
} }
} }

View file

@ -1,5 +1,5 @@
{ {
"extends": ["./tsconfig.json", "../../../tsconfig.build.json"], "extends": ["./tsconfig.json", "@n8n/typescript-config/tsconfig.build.json"],
"compilerOptions": { "compilerOptions": {
"rootDir": "src", "rootDir": "src",
"outDir": "dist", "outDir": "dist",

View file

@ -1,5 +1,5 @@
{ {
"extends": "../../../tsconfig.json", "extends": "@n8n/typescript-config/tsconfig.common.json",
"compilerOptions": { "compilerOptions": {
"rootDir": ".", "rootDir": ".",
"tsBuildInfoFile": "dist/typecheck.tsbuildinfo" "tsBuildInfoFile": "dist/typecheck.tsbuildinfo"

View file

@ -1,10 +1,10 @@
const sharedOptions = require('@n8n_io/eslint-config/shared'); const sharedOptions = require('@n8n/eslint-config/shared');
/** /**
* @type {import('@types/eslint').ESLint.ConfigData} * @type {import('@types/eslint').ESLint.ConfigData}
*/ */
module.exports = { module.exports = {
extends: ['@n8n_io/eslint-config/node'], extends: ['@n8n/eslint-config/node'],
...sharedOptions(__dirname), ...sharedOptions(__dirname),

View file

@ -1,6 +1,6 @@
{ {
"name": "@n8n/config", "name": "@n8n/config",
"version": "1.29.0", "version": "1.30.0",
"scripts": { "scripts": {
"clean": "rimraf dist .turbo", "clean": "rimraf dist .turbo",
"dev": "pnpm watch", "dev": "pnpm watch",
@ -23,5 +23,8 @@
"dependencies": { "dependencies": {
"@n8n/di": "workspace:*", "@n8n/di": "workspace:*",
"reflect-metadata": "catalog:" "reflect-metadata": "catalog:"
},
"devDependencies": {
"@n8n/typescript-config": "workspace:*"
} }
} }

View file

@ -57,6 +57,10 @@ class PrometheusMetricsConfig {
/** How often (in seconds) to update queue metrics. */ /** How often (in seconds) to update queue metrics. */
@Env('N8N_METRICS_QUEUE_METRICS_INTERVAL') @Env('N8N_METRICS_QUEUE_METRICS_INTERVAL')
queueMetricsInterval: number = 20; queueMetricsInterval: number = 20;
/** How often (in seconds) to update active workflow metric */
@Env('N8N_METRICS_ACTIVE_WORKFLOW_METRIC_INTERVAL')
activeWorkflowCountInterval: number = 60;
} }
@Config @Config

View file

@ -9,6 +9,9 @@ export class GenericConfig {
@Env('N8N_RELEASE_TYPE') @Env('N8N_RELEASE_TYPE')
releaseChannel: 'stable' | 'beta' | 'nightly' | 'dev' = 'dev'; releaseChannel: 'stable' | 'beta' | 'nightly' | 'dev' = 'dev';
@Env('N8N_RELEASE_DATE')
releaseDate?: Date;
/** Grace period (in seconds) to wait for components to shut down before process exit. */ /** Grace period (in seconds) to wait for components to shut down before process exit. */
@Env('N8N_GRACEFUL_SHUTDOWN_TIMEOUT') @Env('N8N_GRACEFUL_SHUTDOWN_TIMEOUT')
gracefulShutdownTimeout: number = 30; gracefulShutdownTimeout: number = 30;

View file

@ -4,9 +4,5 @@ import { Config, Env } from '../decorators';
export class PartialExecutionsConfig { export class PartialExecutionsConfig {
/** Partial execution logic version to use by default. */ /** Partial execution logic version to use by default. */
@Env('N8N_PARTIAL_EXECUTION_VERSION_DEFAULT') @Env('N8N_PARTIAL_EXECUTION_VERSION_DEFAULT')
version: 1 | 2 = 1; version: 1 | 2 = 2;
/** Set this to true to enforce using the default version. Users cannot use the other version then by setting a local storage key. */
@Env('N8N_PARTIAL_EXECUTION_ENFORCE_VERSION')
enforce: boolean = false;
} }

View file

@ -55,6 +55,13 @@ export const Config: ClassDecorator = (ConfigClass: Class) => {
} else { } else {
console.warn(`Invalid boolean value for ${envName}: ${value}`); console.warn(`Invalid boolean value for ${envName}: ${value}`);
} }
} else if (type === Date) {
const timestamp = Date.parse(value);
if (isNaN(timestamp)) {
console.warn(`Invalid timestamp value for ${envName}: ${value}`);
} else {
config[key] = new Date(timestamp);
}
} else if (type === String) { } else if (type === String) {
config[key] = value; config[key] = value;
} else { } else {

View file

@ -8,9 +8,12 @@ jest.mock('fs');
const mockFs = mock<typeof fs>(); const mockFs = mock<typeof fs>();
fs.readFileSync = mockFs.readFileSync; fs.readFileSync = mockFs.readFileSync;
const consoleWarnMock = jest.spyOn(console, 'warn').mockImplementation(() => {});
describe('GlobalConfig', () => { describe('GlobalConfig', () => {
beforeEach(() => { beforeEach(() => {
Container.reset(); Container.reset();
jest.clearAllMocks();
}); });
const originalEnv = process.env; const originalEnv = process.env;
@ -18,10 +21,6 @@ describe('GlobalConfig', () => {
process.env = originalEnv; process.env = originalEnv;
}); });
// 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));
const defaultConfig: GlobalConfig = { const defaultConfig: GlobalConfig = {
path: '/', path: '/',
host: 'localhost', host: 'localhost',
@ -174,6 +173,7 @@ describe('GlobalConfig', () => {
includeApiStatusCodeLabel: false, includeApiStatusCodeLabel: false,
includeQueueMetrics: false, includeQueueMetrics: false,
queueMetricsInterval: 20, queueMetricsInterval: 20,
activeWorkflowCountInterval: 60,
}, },
additionalNonUIRoutes: '', additionalNonUIRoutes: '',
disableProductionWebhooksOnMainProcess: false, disableProductionWebhooksOnMainProcess: false,
@ -306,15 +306,14 @@ describe('GlobalConfig', () => {
disabled: false, disabled: false,
}, },
partialExecutions: { partialExecutions: {
version: 1, version: 2,
enforce: false,
}, },
}; };
it('should use all default values when no env variables are defined', () => { it('should use all default values when no env variables are defined', () => {
process.env = {}; process.env = {};
const config = Container.get(GlobalConfig); const config = Container.get(GlobalConfig);
expect(deepCopy(config)).toEqual(defaultConfig); expect(structuredClone(config)).toEqual(defaultConfig);
expect(mockFs.readFileSync).not.toHaveBeenCalled(); expect(mockFs.readFileSync).not.toHaveBeenCalled();
}); });
@ -327,9 +326,10 @@ describe('GlobalConfig', () => {
DB_LOGGING_MAX_EXECUTION_TIME: '0', DB_LOGGING_MAX_EXECUTION_TIME: '0',
N8N_METRICS: 'TRUE', N8N_METRICS: 'TRUE',
N8N_TEMPLATES_ENABLED: '0', N8N_TEMPLATES_ENABLED: '0',
N8N_RELEASE_DATE: '2025-02-17T13:54:15Z',
}; };
const config = Container.get(GlobalConfig); const config = Container.get(GlobalConfig);
expect(deepCopy(config)).toEqual({ expect(structuredClone(config)).toEqual({
...defaultConfig, ...defaultConfig,
database: { database: {
logging: defaultConfig.database.logging, logging: defaultConfig.database.logging,
@ -358,6 +358,10 @@ describe('GlobalConfig', () => {
...defaultConfig.templates, ...defaultConfig.templates,
enabled: false, enabled: false,
}, },
generic: {
...defaultConfig.generic,
releaseDate: new Date('2025-02-17T13:54:15.000Z'),
},
}); });
expect(mockFs.readFileSync).not.toHaveBeenCalled(); expect(mockFs.readFileSync).not.toHaveBeenCalled();
}); });
@ -370,7 +374,7 @@ describe('GlobalConfig', () => {
mockFs.readFileSync.calledWith(passwordFile, 'utf8').mockReturnValueOnce('password-from-file'); mockFs.readFileSync.calledWith(passwordFile, 'utf8').mockReturnValueOnce('password-from-file');
const config = Container.get(GlobalConfig); const config = Container.get(GlobalConfig);
expect(deepCopy(config)).toEqual({ expect(structuredClone(config)).toEqual({
...defaultConfig, ...defaultConfig,
database: { database: {
...defaultConfig.database, ...defaultConfig.database,
@ -382,4 +386,26 @@ describe('GlobalConfig', () => {
}); });
expect(mockFs.readFileSync).toHaveBeenCalled(); expect(mockFs.readFileSync).toHaveBeenCalled();
}); });
it('should handle invalid numbers', () => {
process.env = {
DB_LOGGING_MAX_EXECUTION_TIME: 'abcd',
};
const config = Container.get(GlobalConfig);
expect(config.database.logging.maxQueryExecutionTime).toEqual(0);
expect(consoleWarnMock).toHaveBeenCalledWith(
'Invalid number value for DB_LOGGING_MAX_EXECUTION_TIME: abcd',
);
});
it('should handle invalid timestamps', () => {
process.env = {
N8N_RELEASE_DATE: 'abcd',
};
const config = Container.get(GlobalConfig);
expect(config.generic.releaseDate).toBeUndefined();
expect(consoleWarnMock).toHaveBeenCalledWith(
'Invalid timestamp value for N8N_RELEASE_DATE: abcd',
);
});
}); });

View file

@ -1,5 +1,5 @@
{ {
"extends": ["./tsconfig.json", "../../../tsconfig.build.json"], "extends": ["./tsconfig.json", "@n8n/typescript-config/tsconfig.build.json"],
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"rootDir": "src", "rootDir": "src",

View file

@ -1,5 +1,5 @@
{ {
"extends": "../../../tsconfig.json", "extends": "@n8n/typescript-config/tsconfig.common.json",
"compilerOptions": { "compilerOptions": {
"rootDir": ".", "rootDir": ".",
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,

View file

@ -1,7 +1,7 @@
const sharedOptions = require('@n8n_io/eslint-config/shared'); const sharedOptions = require('@n8n/eslint-config/shared');
/** @type {import('@types/eslint').ESLint.ConfigData} */ /** @type {import('@types/eslint').ESLint.ConfigData} */
module.exports = { module.exports = {
extends: ['@n8n_io/eslint-config/base'], extends: ['@n8n/eslint-config/base'],
...sharedOptions(__dirname), ...sharedOptions(__dirname),
}; };

View file

@ -22,5 +22,8 @@
], ],
"dependencies": { "dependencies": {
"reflect-metadata": "catalog:" "reflect-metadata": "catalog:"
},
"devDependencies": {
"@n8n/typescript-config": "workspace:*"
} }
} }

View file

@ -1,5 +1,5 @@
{ {
"extends": ["./tsconfig.json", "../../../tsconfig.build.json"], "extends": ["./tsconfig.json", "@n8n/typescript-config/tsconfig.build.json"],
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"rootDir": "src", "rootDir": "src",

View file

@ -1,5 +1,5 @@
{ {
"extends": "../../../tsconfig.json", "extends": "@n8n/typescript-config/tsconfig.common.json",
"compilerOptions": { "compilerOptions": {
"rootDir": ".", "rootDir": ".",
"types": ["node", "jest"], "types": ["node", "jest"],

View file

@ -320,7 +320,7 @@ module.exports = {
const LOCALE_NAMESPACE = '$locale'; const LOCALE_NAMESPACE = '$locale';
const LOCALE_FILEPATH = cwd.endsWith('editor-ui') const LOCALE_FILEPATH = cwd.endsWith('editor-ui')
? path.join(cwd, locale) ? path.join(cwd, locale)
: path.join(cwd, 'packages/editor-ui', locale); : path.join(cwd, 'packages/frontend/editor-ui', locale);
let LOCALE_MAP; let LOCALE_MAP;

View file

@ -1,7 +1,14 @@
{ {
"name": "@n8n_io/eslint-config", "name": "@n8n/eslint-config",
"private": true, "private": true,
"version": "0.0.1", "version": "0.0.1",
"exports": {
"./base": "./base.js",
"./frontend": "./frontend.js",
"./local-rules": "./local-rules.js",
"./node": "./node.js",
"./shared": "./shared.js"
},
"devDependencies": { "devDependencies": {
"@types/eslint": "^8.56.5", "@types/eslint": "^8.56.5",
"@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/eslint-plugin": "^7.2.0",

View file

@ -1,10 +1,10 @@
const sharedOptions = require('@n8n_io/eslint-config/shared'); const sharedOptions = require('@n8n/eslint-config/shared');
/** /**
* @type {import('@types/eslint').ESLint.ConfigData} * @type {import('@types/eslint').ESLint.ConfigData}
*/ */
module.exports = { module.exports = {
extends: ['@n8n_io/eslint-config/base'], extends: ['@n8n/eslint-config/base'],
...sharedOptions(__dirname), ...sharedOptions(__dirname),

View file

@ -27,6 +27,7 @@
"uuencode": "0.0.4" "uuencode": "0.0.4"
}, },
"devDependencies": { "devDependencies": {
"@n8n/typescript-config": "workspace:*",
"@types/imap": "^0.8.40", "@types/imap": "^0.8.40",
"@types/quoted-printable": "^1.0.2", "@types/quoted-printable": "^1.0.2",
"@types/utf8": "^3.0.3", "@types/utf8": "^3.0.3",

View file

@ -1,5 +1,5 @@
{ {
"extends": ["./tsconfig.json", "../../../tsconfig.build.json"], "extends": ["./tsconfig.json", "@n8n/typescript-config/tsconfig.build.json"],
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"rootDir": "src", "rootDir": "src",

View file

@ -1,5 +1,5 @@
{ {
"extends": "../../../tsconfig.json", "extends": "@n8n/typescript-config/tsconfig.common.json",
"compilerOptions": { "compilerOptions": {
"rootDir": ".", "rootDir": ".",
"types": ["node", "jest"], "types": ["node", "jest"],

View file

@ -1,10 +1,10 @@
const sharedOptions = require('@n8n_io/eslint-config/shared'); const sharedOptions = require('@n8n/eslint-config/shared');
/** /**
* @type {import('@types/eslint').ESLint.ConfigData} * @type {import('@types/eslint').ESLint.ConfigData}
*/ */
module.exports = { module.exports = {
extends: ['@n8n_io/eslint-config/node'], extends: ['@n8n/eslint-config/node'],
...sharedOptions(__dirname), ...sharedOptions(__dirname),

View file

@ -62,6 +62,7 @@
"zod": "^3.0.0" "zod": "^3.0.0"
}, },
"devDependencies": { "devDependencies": {
"@n8n/typescript-config": "workspace:*",
"@types/json-schema": "^7.0.15", "@types/json-schema": "^7.0.15",
"zod": "catalog:" "zod": "catalog:"
} }

View file

@ -1,5 +1,5 @@
{ {
"extends": ["../../../tsconfig.json"], "extends": ["@n8n/typescript-config/tsconfig.common.json"],
"compilerOptions": { "compilerOptions": {
"rootDir": ".", "rootDir": ".",
"baseUrl": "src", "baseUrl": "src",

View file

@ -1,10 +1,10 @@
const sharedOptions = require('@n8n_io/eslint-config/shared'); const sharedOptions = require('@n8n/eslint-config/shared');
/** /**
* @type {import('@types/eslint').ESLint.ConfigData} * @type {import('@types/eslint').ESLint.ConfigData}
*/ */
module.exports = { module.exports = {
extends: ['@n8n_io/eslint-config/node'], extends: ['@n8n/eslint-config/node'],
...sharedOptions(__dirname), ...sharedOptions(__dirname),

View file

@ -27,7 +27,13 @@ import { toolsAgentExecute } from './agents/ToolsAgent/execute';
// Function used in the inputs expression to figure out which inputs to // Function used in the inputs expression to figure out which inputs to
// display based on the agent type // display based on the agent type
function getInputs( function getInputs(
agent: 'toolsAgent' | 'conversationalAgent' | 'openAiFunctionsAgent' | 'reActAgent' | 'sqlAgent', agent:
| 'toolsAgent'
| 'conversationalAgent'
| 'openAiFunctionsAgent'
| 'planAndExecuteAgent'
| 'reActAgent'
| 'sqlAgent',
hasOutputParser?: boolean, hasOutputParser?: boolean,
): Array<NodeConnectionType | INodeInputConfiguration> { ): Array<NodeConnectionType | INodeInputConfiguration> {
interface SpecialInput { interface SpecialInput {
@ -256,7 +262,7 @@ export class Agent implements INodeType {
icon: 'fa:robot', icon: 'fa:robot',
iconColor: 'black', iconColor: 'black',
group: ['transform'], group: ['transform'],
version: [1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7], version: [1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8],
description: 'Generates an action plan and executes it. Can use external tools.', description: 'Generates an action plan and executes it. Can use external tools.',
subtitle: subtitle:
"={{ { toolsAgent: 'Tools Agent', conversationalAgent: 'Conversational Agent', openAiFunctionsAgent: 'OpenAI Functions Agent', reActAgent: 'ReAct Agent', sqlAgent: 'SQL Agent', planAndExecuteAgent: 'Plan and Execute Agent' }[$parameter.agent] }}", "={{ { toolsAgent: 'Tools Agent', conversationalAgent: 'Conversational Agent', openAiFunctionsAgent: 'OpenAI Functions Agent', reActAgent: 'ReAct Agent', sqlAgent: 'SQL Agent', planAndExecuteAgent: 'Plan and Execute Agent' }[$parameter.agent] }}",
@ -322,6 +328,24 @@ export class Agent implements INodeType {
}, },
}, },
}, },
{
displayName:
"This node is using Agent that has been deprecated. Please switch to using 'Tools Agent' instead.",
name: 'deprecated',
type: 'notice',
default: '',
displayOptions: {
show: {
agent: [
'conversationalAgent',
'openAiFunctionsAgent',
'planAndExecuteAgent',
'reActAgent',
'sqlAgent',
],
},
},
},
// Make Conversational Agent the default agent for versions 1.5 and below // Make Conversational Agent the default agent for versions 1.5 and below
{ {
...agentTypeProperty, ...agentTypeProperty,
@ -331,10 +355,17 @@ export class Agent implements INodeType {
displayOptions: { show: { '@version': [{ _cnd: { lte: 1.5 } }] } }, displayOptions: { show: { '@version': [{ _cnd: { lte: 1.5 } }] } },
default: 'conversationalAgent', default: 'conversationalAgent',
}, },
// Make Tools Agent the default agent for versions 1.6 and above // Make Tools Agent the default agent for versions 1.6 and 1.7
{ {
...agentTypeProperty, ...agentTypeProperty,
displayOptions: { show: { '@version': [{ _cnd: { gte: 1.6 } }] } }, displayOptions: { show: { '@version': [{ _cnd: { between: { from: 1.6, to: 1.7 } } }] } },
default: 'toolsAgent',
},
// Make Tools Agent the only agent option for versions 1.8 and above
{
...agentTypeProperty,
type: 'hidden',
displayOptions: { show: { '@version': [{ _cnd: { gte: 1.8 } }] } },
default: 'toolsAgent', default: 'toolsAgent',
}, },
{ {

View file

@ -96,6 +96,13 @@ export const reActAgentAgentProperties: INodeProperties[] = [
rows: 6, rows: 6,
}, },
}, },
{
displayName: 'Max Iterations',
name: 'maxIterations',
type: 'number',
default: 10,
description: 'The maximum number of iterations the agent will run before stopping',
},
{ {
displayName: 'Return Intermediate Steps', displayName: 'Return Intermediate Steps',
name: 'returnIntermediateSteps', name: 'returnIntermediateSteps',

View file

@ -38,6 +38,7 @@ export async function reActAgentAgentExecute(
prefix?: string; prefix?: string;
suffix?: string; suffix?: string;
suffixChat?: string; suffixChat?: string;
maxIterations?: number;
humanMessageTemplate?: string; humanMessageTemplate?: string;
returnIntermediateSteps?: boolean; returnIntermediateSteps?: boolean;
}; };
@ -60,6 +61,7 @@ export async function reActAgentAgentExecute(
agent, agent,
tools, tools,
returnIntermediateSteps: options?.returnIntermediateSteps === true, returnIntermediateSteps: options?.returnIntermediateSteps === true,
maxIterations: options.maxIterations ?? 10,
}); });
const returnData: INodeExecutionData[] = []; const returnData: INodeExecutionData[] = [];

View file

@ -392,13 +392,14 @@ export async function toolsAgentExecute(this: IExecuteFunctions): Promise<INodeE
const returnData: INodeExecutionData[] = []; const returnData: INodeExecutionData[] = [];
const items = this.getInputData(); const items = this.getInputData();
const outputParsers = await getOptionalOutputParsers(this);
const outputParser = outputParsers?.[0];
const tools = await getTools(this, outputParser);
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
try { try {
const model = await getChatModel(this); const model = await getChatModel(this);
const memory = await getOptionalMemory(this); const memory = await getOptionalMemory(this);
const outputParsers = await getOptionalOutputParsers(this);
const outputParser = outputParsers?.[0];
const tools = await getTools(this, outputParser);
const input = getPromptInputByType({ const input = getPromptInputByType({
ctx: this, ctx: this,

View file

@ -338,7 +338,7 @@ export class ChainLlm implements INodeType {
displayOptions: { show: { promptType: ['auto'], '@version': [{ _cnd: { gte: 1.5 } }] } }, displayOptions: { show: { promptType: ['auto'], '@version': [{ _cnd: { gte: 1.5 } }] } },
}, },
{ {
displayName: 'Text', displayName: 'Prompt (User Message)',
name: 'text', name: 'text',
type: 'string', type: 'string',
required: true, required: true,

View file

@ -123,11 +123,12 @@ export class ChainRetrievalQa implements INodeType {
displayOptions: { show: { promptType: ['auto'], '@version': [{ _cnd: { gte: 1.4 } }] } }, displayOptions: { show: { promptType: ['auto'], '@version': [{ _cnd: { gte: 1.4 } }] } },
}, },
{ {
displayName: 'Text', displayName: 'Prompt (User Message)',
name: 'text', name: 'text',
type: 'string', type: 'string',
required: true, required: true,
default: '', default: '',
placeholder: 'e.g. Hello, how can you help me?',
typeOptions: { typeOptions: {
rows: 2, rows: 2,
}, },

View file

@ -14,6 +14,7 @@ import {
import { getConnectionHintNoticeField } from '@utils/sharedFields'; import { getConnectionHintNoticeField } from '@utils/sharedFields';
import { searchModels } from './methods/searchModels';
import { makeN8nLlmFailedAttemptHandler } from '../n8nLlmFailedAttemptHandler'; import { makeN8nLlmFailedAttemptHandler } from '../n8nLlmFailedAttemptHandler';
import { N8nLlmTracing } from '../N8nLlmTracing'; import { N8nLlmTracing } from '../N8nLlmTracing';
@ -69,15 +70,23 @@ const modelField: INodeProperties = {
default: 'claude-2', default: 'claude-2',
}; };
const MIN_THINKING_BUDGET = 1024;
const DEFAULT_MAX_TOKENS = 4096;
export class LmChatAnthropic implements INodeType { export class LmChatAnthropic implements INodeType {
methods = {
listSearch: {
searchModels,
},
};
description: INodeTypeDescription = { description: INodeTypeDescription = {
displayName: 'Anthropic Chat Model', displayName: 'Anthropic Chat Model',
// eslint-disable-next-line n8n-nodes-base/node-class-description-name-miscased // eslint-disable-next-line n8n-nodes-base/node-class-description-name-miscased
name: 'lmChatAnthropic', name: 'lmChatAnthropic',
icon: 'file:anthropic.svg', icon: 'file:anthropic.svg',
group: ['transform'], group: ['transform'],
version: [1, 1.1, 1.2], version: [1, 1.1, 1.2, 1.3],
defaultVersion: 1.2, defaultVersion: 1.3,
description: 'Language Model Anthropic', description: 'Language Model Anthropic',
defaults: { defaults: {
name: 'Anthropic Chat Model', name: 'Anthropic Chat Model',
@ -135,7 +144,43 @@ export class LmChatAnthropic implements INodeType {
), ),
displayOptions: { displayOptions: {
show: { show: {
'@version': [{ _cnd: { gte: 1.2 } }], '@version': [{ _cnd: { lte: 1.2 } }],
},
},
},
{
displayName: 'Model',
name: 'model',
type: 'resourceLocator',
default: {
mode: 'list',
value: 'claude-3-7-sonnet-20250219',
cachedResultName: 'Claude 3.7 Sonnet',
},
required: true,
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
placeholder: 'Select a model...',
typeOptions: {
searchListMethod: 'searchModels',
searchable: true,
},
},
{
displayName: 'ID',
name: 'id',
type: 'string',
placeholder: 'Claude Sonnet',
},
],
description:
'The model. Choose from the list, or specify an ID. <a href="https://docs.anthropic.com/claude/docs/models-overview">Learn more</a>.',
displayOptions: {
show: {
'@version': [{ _cnd: { gte: 1.3 } }],
}, },
}, },
}, },
@ -150,7 +195,7 @@ export class LmChatAnthropic implements INodeType {
{ {
displayName: 'Maximum Number of Tokens', displayName: 'Maximum Number of Tokens',
name: 'maxTokensToSample', name: 'maxTokensToSample',
default: 4096, default: DEFAULT_MAX_TOKENS,
description: 'The maximum number of tokens to generate in the completion', description: 'The maximum number of tokens to generate in the completion',
type: 'number', type: 'number',
}, },
@ -162,6 +207,11 @@ export class LmChatAnthropic implements INodeType {
description: description:
'Controls randomness: Lowering results in less random completions. As the temperature approaches zero, the model will become deterministic and repetitive.', 'Controls randomness: Lowering results in less random completions. As the temperature approaches zero, the model will become deterministic and repetitive.',
type: 'number', type: 'number',
displayOptions: {
hide: {
thinking: [true],
},
},
}, },
{ {
displayName: 'Top K', displayName: 'Top K',
@ -171,6 +221,11 @@ export class LmChatAnthropic implements INodeType {
description: description:
'Used to remove "long tail" low probability responses. Defaults to -1, which disables it.', 'Used to remove "long tail" low probability responses. Defaults to -1, which disables it.',
type: 'number', type: 'number',
displayOptions: {
hide: {
thinking: [true],
},
},
}, },
{ {
displayName: 'Top P', displayName: 'Top P',
@ -180,6 +235,30 @@ export class LmChatAnthropic implements INodeType {
description: description:
'Controls diversity via nucleus sampling: 0.5 means half of all likelihood-weighted options are considered. We generally recommend altering this or temperature but not both.', 'Controls diversity via nucleus sampling: 0.5 means half of all likelihood-weighted options are considered. We generally recommend altering this or temperature but not both.',
type: 'number', type: 'number',
displayOptions: {
hide: {
thinking: [true],
},
},
},
{
displayName: 'Enable Thinking',
name: 'thinking',
type: 'boolean',
default: false,
description: 'Whether to enable thinking mode for the model',
},
{
displayName: 'Thinking Budget (Tokens)',
name: 'thinkingBudget',
type: 'number',
default: MIN_THINKING_BUDGET,
description: 'The maximum number of tokens to use for thinking',
displayOptions: {
show: {
thinking: [true],
},
},
}, },
], ],
}, },
@ -189,13 +268,21 @@ export class LmChatAnthropic implements INodeType {
async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const credentials = await this.getCredentials('anthropicApi'); const credentials = await this.getCredentials('anthropicApi');
const modelName = this.getNodeParameter('model', itemIndex) as string; const version = this.getNode().typeVersion;
const modelName =
version >= 1.3
? (this.getNodeParameter('model.value', itemIndex) as string)
: (this.getNodeParameter('model', itemIndex) as string);
const options = this.getNodeParameter('options', itemIndex, {}) as { const options = this.getNodeParameter('options', itemIndex, {}) as {
maxTokensToSample?: number; maxTokensToSample?: number;
temperature: number; temperature: number;
topK: number; topK?: number;
topP: number; topP?: number;
thinking?: boolean;
thinkingBudget?: number;
}; };
let invocationKwargs = {};
const tokensUsageParser = (llmOutput: LLMResult['llmOutput']) => { const tokensUsageParser = (llmOutput: LLMResult['llmOutput']) => {
const usage = (llmOutput?.usage as { input_tokens: number; output_tokens: number }) ?? { const usage = (llmOutput?.usage as { input_tokens: number; output_tokens: number }) ?? {
@ -208,6 +295,27 @@ export class LmChatAnthropic implements INodeType {
totalTokens: usage.input_tokens + usage.output_tokens, totalTokens: usage.input_tokens + usage.output_tokens,
}; };
}; };
if (options.thinking) {
invocationKwargs = {
thinking: {
type: 'enabled',
// If thinking is enabled, we need to set a budget.
// We fallback to 1024 as that is the minimum
budget_tokens: options.thinkingBudget ?? MIN_THINKING_BUDGET,
},
// The default Langchain max_tokens is -1 (no limit) but Anthropic requires a number
// higher than budget_tokens
max_tokens: options.maxTokensToSample ?? DEFAULT_MAX_TOKENS,
// These need to be unset when thinking is enabled.
// Because the invocationKwargs will override the model options
// we can pass options to the model and then override them here
top_k: undefined,
top_p: undefined,
temperature: undefined,
};
}
const model = new ChatAnthropic({ const model = new ChatAnthropic({
anthropicApiKey: credentials.apiKey as string, anthropicApiKey: credentials.apiKey as string,
modelName, modelName,
@ -217,6 +325,7 @@ export class LmChatAnthropic implements INodeType {
topP: options.topP, topP: options.topP,
callbacks: [new N8nLlmTracing(this, { tokensUsageParser })], callbacks: [new N8nLlmTracing(this, { tokensUsageParser })],
onFailedAttempt: makeN8nLlmFailedAttemptHandler(this), onFailedAttempt: makeN8nLlmFailedAttemptHandler(this),
invocationKwargs,
}); });
return { return {

Some files were not shown because too many files have changed in this diff Show more