mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
Merge remote-tracking branch 'origin/master' into fix_48_in_stub
This commit is contained in:
commit
aa599db0d5
8
.github/scripts/trim-fe-packageJson.js
vendored
8
.github/scripts/trim-fe-packageJson.js
vendored
|
@ -7,12 +7,12 @@ const trimPackageJson = (packageName) => {
|
|||
const { scripts, peerDependencies, devDependencies, dependencies, ...packageJson } = require(
|
||||
filePath,
|
||||
);
|
||||
if (packageName === '@n8n/chat') {
|
||||
if (packageName === 'frontend/@n8n/chat') {
|
||||
packageJson.dependencies = dependencies;
|
||||
}
|
||||
writeFileSync(filePath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8');
|
||||
};
|
||||
|
||||
trimPackageJson('@n8n/chat');
|
||||
trimPackageJson('design-system');
|
||||
trimPackageJson('editor-ui');
|
||||
trimPackageJson('frontend/@n8n/chat');
|
||||
trimPackageJson('frontend/@n8n/design-system');
|
||||
trimPackageJson('frontend/editor-ui');
|
||||
|
|
16
.github/workflows/release-publish.yml
vendored
16
.github/workflows/release-publish.yml
vendored
|
@ -51,14 +51,20 @@ jobs:
|
|||
- name: Dry-run publishing
|
||||
run: pnpm publish -r --no-git-checks --dry-run
|
||||
|
||||
- name: Publish to NPM
|
||||
- name: Pre publishing changes
|
||||
run: |
|
||||
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
|
||||
node .github/scripts/trim-fe-packageJson.js
|
||||
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
|
||||
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
|
||||
run: echo "release=${{ env.RELEASE }}" >> $GITHUB_OUTPUT
|
||||
|
@ -68,7 +74,7 @@ jobs:
|
|||
needs: [publish-to-npm]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.merged == true
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
@ -156,7 +162,7 @@ jobs:
|
|||
with:
|
||||
projects: ${{ secrets.SENTRY_FRONTEND_PROJECT }}
|
||||
version: ${{ needs.publish-to-npm.outputs.release }}
|
||||
sourcemaps: packages/editor-ui/dist
|
||||
sourcemaps: packages/frontend/editor-ui/dist
|
||||
|
||||
- name: Create a backend release
|
||||
uses: getsentry/action-release@v1.7.0
|
||||
|
|
41
.github/workflows/release-push-to-channel.yml
vendored
41
.github/workflows/release-push-to-channel.yml
vendored
|
@ -11,10 +11,10 @@ on:
|
|||
description: 'Release channel'
|
||||
required: true
|
||||
type: choice
|
||||
default: 'next'
|
||||
default: 'beta'
|
||||
options:
|
||||
- next
|
||||
- latest
|
||||
- beta
|
||||
- stable
|
||||
|
||||
jobs:
|
||||
release-to-npm:
|
||||
|
@ -25,9 +25,18 @@ jobs:
|
|||
- uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version: 20.x
|
||||
- run: |
|
||||
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
|
||||
npm dist-tag add n8n@${{ github.event.inputs.version }} ${{ github.event.inputs.release-channel }}
|
||||
|
||||
- run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
|
||||
|
||||
- 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:
|
||||
name: Release to DockerHub
|
||||
|
@ -39,7 +48,15 @@ jobs:
|
|||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
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:
|
||||
name: Release to GitHub Container Registry
|
||||
|
@ -52,7 +69,15 @@ jobs:
|
|||
username: ${{ github.actor }}
|
||||
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:
|
||||
name: Update latest and next in the docs
|
||||
|
|
|
@ -2,7 +2,7 @@ coverage
|
|||
dist
|
||||
package.json
|
||||
pnpm-lock.yaml
|
||||
packages/editor-ui/index.html
|
||||
packages/frontend/editor-ui/index.html
|
||||
packages/nodes-base/nodes/**/test
|
||||
packages/cli/templates/form-trigger.handlebars
|
||||
packages/cli/templates/form-trigger-completion.handlebars
|
||||
|
|
39
CHANGELOG.md
39
CHANGELOG.md
|
@ -1,3 +1,42 @@
|
|||
# [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)
|
||||
|
||||
|
||||
|
|
|
@ -49,8 +49,8 @@ The most important directories:
|
|||
execution, active webhooks and
|
||||
workflows. **Contact n8n before
|
||||
starting on any changes here**
|
||||
- [/packages/design-system](/packages/design-system) - Vue frontend components
|
||||
- [/packages/editor-ui](/packages/editor-ui) - Vue frontend workflow editor
|
||||
- [/packages/frontend/@n8n/design-system](/packages/design-system) - Vue frontend components
|
||||
- [/packages/frontend/editor-ui](/packages/editor-ui) - Vue frontend workflow editor
|
||||
- [/packages/node-dev](/packages/node-dev) - CLI to create new n8n-nodes
|
||||
- [/packages/nodes-base](/packages/nodes-base) - Base n8n nodes
|
||||
- [/packages/workflow](/packages/workflow) - Workflow code with interfaces which
|
||||
|
|
|
@ -42,10 +42,7 @@ component_management:
|
|||
- component_id: frontend_packages
|
||||
name: Frontend
|
||||
paths:
|
||||
- packages/@n8n/chat/**
|
||||
- packages/@n8n/codemirror-lang/**
|
||||
- packages/design-system/**
|
||||
- packages/editor-ui/**
|
||||
- packages/frontend/**
|
||||
- component_id: nodes_packages
|
||||
name: Nodes
|
||||
|
|
|
@ -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}
|
||||
*/
|
||||
module.exports = {
|
||||
extends: ['@n8n_io/eslint-config/base', 'plugin:cypress/recommended'],
|
||||
extends: ['@n8n/eslint-config/base', 'plugin:cypress/recommended'],
|
||||
|
||||
...sharedOptions(__dirname),
|
||||
|
||||
|
|
|
@ -206,6 +206,10 @@ export function clickWorkflowCardContent(workflowName: string) {
|
|||
getWorkflowCardContent(workflowName).click();
|
||||
}
|
||||
|
||||
export function clickAssignmentCollectionAdd() {
|
||||
cy.getByTestId('assignment-collection-drop-area').click();
|
||||
}
|
||||
|
||||
export function assertNodeOutputHintExists() {
|
||||
getNodeOutputHint().should('exist');
|
||||
}
|
||||
|
|
|
@ -356,5 +356,5 @@ export function openContextMenu(
|
|||
}
|
||||
|
||||
export function clickContextMenuAction(action: string) {
|
||||
getContextMenuAction(action).click();
|
||||
getContextMenuAction(action).click({ force: true });
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ export const PIPEDRIVE_NODE_NAME = 'Pipedrive';
|
|||
export const HTTP_REQUEST_NODE_NAME = 'HTTP Request';
|
||||
export const AGENT_NODE_NAME = 'AI Agent';
|
||||
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_CODE_NODE_NAME = 'Code Tool';
|
||||
export const AI_TOOL_WIKIPEDIA_NODE_NAME = 'Wikipedia';
|
||||
|
|
|
@ -14,6 +14,8 @@ module.exports = defineConfig({
|
|||
experimentalMemoryManagement: true,
|
||||
e2e: {
|
||||
baseUrl: BASE_URL,
|
||||
viewportWidth: 1536,
|
||||
viewportHeight: 960,
|
||||
video: true,
|
||||
screenshotOnRunFailure: true,
|
||||
experimentalInteractiveRunEvents: true,
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { WorkflowSharingModal } from '../pages';
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
|
||||
import { getUniqueWorkflowName } from '../utils/workflowUtils';
|
||||
|
||||
const WorkflowsPage = new WorkflowsPageClass();
|
||||
const WorkflowPage = new WorkflowPageClass();
|
||||
const workflowSharingModal = new WorkflowSharingModal();
|
||||
|
||||
const multipleWorkflowsCount = 5;
|
||||
|
||||
|
@ -138,4 +140,10 @@ describe('Workflows', () => {
|
|||
cy.url().should('include', 'sort=lastCreated');
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -22,7 +22,11 @@ describe('Undo/Redo', () => {
|
|||
it('should undo/redo deleting node using context menu', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_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.nodeConnections().should('have.length', 0);
|
||||
WorkflowPage.actions.hitUndo();
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
openContextMenu,
|
||||
} from '../composables/workflow';
|
||||
import { NDV, WorkflowExecutionsTab } from '../pages';
|
||||
import { clearNotifications, successToast } from '../pages/notifications';
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
|
||||
const WorkflowPage = new WorkflowPageClass();
|
||||
|
@ -235,7 +236,11 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
it('should delete node using context menu', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_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.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);
|
||||
// At this point last added node should be off-screen
|
||||
WorkflowPage.getters.canvasNodes().last().should('not.be.visible');
|
||||
WorkflowPage.getters.zoomToFitButton().click();
|
||||
|
@ -485,6 +491,9 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.actions.executeWorkflow();
|
||||
|
||||
successToast().should('contain.text', 'Workflow executed successfully');
|
||||
clearNotifications();
|
||||
|
||||
ExecutionsTab.actions.switchToExecutionsTab();
|
||||
ExecutionsTab.getters.successfulExecutionListItems().should('have.length', 1);
|
||||
|
||||
|
|
|
@ -27,8 +27,8 @@ describe('Workflow Executions', () => {
|
|||
|
||||
executionsTab.getters.executionsList().scrollTo(0, 500).wait(0);
|
||||
|
||||
executionsTab.getters.executionListItems().should('have.length', 11);
|
||||
executionsTab.getters.successfulExecutionListItems().should('have.length', 9);
|
||||
executionsTab.getters.executionListItems().should('have.length', 30);
|
||||
executionsTab.getters.successfulExecutionListItems().should('have.length', 28);
|
||||
executionsTab.getters.failedExecutionListItems().should('have.length', 2);
|
||||
executionsTab.getters
|
||||
.executionListItems()
|
||||
|
@ -185,8 +185,9 @@ describe('Workflow Executions', () => {
|
|||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
});
|
||||
|
||||
it('should load items and auto scroll after filter change', () => {
|
||||
// This should be a component test. Abstracting this away into to ensure our lists work.
|
||||
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
|
||||
it.skip('should load items and auto scroll after filter change', () => {
|
||||
createMockExecutions();
|
||||
createMockExecutions();
|
||||
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
|
||||
|
@ -289,15 +290,20 @@ describe('Workflow Executions', () => {
|
|||
});
|
||||
|
||||
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
|
||||
executionsTab.actions.toggleNodeEnabled('Error');
|
||||
workflowPage.getters.disabledNodes().should('have.length', 0);
|
||||
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
|
||||
executionsTab.actions.toggleNodeEnabled('Error');
|
||||
workflowPage.getters.disabledNodes().should('have.length', 1);
|
||||
executionsTab.actions.createManualExecutions(4);
|
||||
executionsTab.actions.createManualExecutions(15);
|
||||
};
|
||||
|
||||
const checkMainHeaderELements = () => {
|
||||
|
|
|
@ -54,11 +54,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
cy.changeQuota('maxTeamProjects', -1);
|
||||
});
|
||||
|
||||
/**
|
||||
* @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', () => {
|
||||
it('should filter credentials by project ID when creating new workflow or hard reloading an opened workflow', () => {
|
||||
cy.signinAsOwner();
|
||||
cy.visit(workflowsPage.url);
|
||||
|
||||
|
@ -453,6 +449,8 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
});
|
||||
|
||||
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.visit(workflowsPage.url);
|
||||
|
||||
|
@ -474,15 +472,14 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
// Create a project and add a user to it
|
||||
projects.createProject('Project 1');
|
||||
projects.addProjectMember(INSTANCE_MEMBERS[0].email);
|
||||
|
||||
clearNotifications();
|
||||
projects.getProjectSettingsSaveButton().click();
|
||||
|
||||
// Move the workflow from Home to Project 1
|
||||
projects.getHomeButton().click();
|
||||
workflowsPage.getters
|
||||
.workflowCards()
|
||||
.should('have.length', 1)
|
||||
.filter(':contains("Personal")')
|
||||
.should('exist');
|
||||
workflowsPage.getters.workflowCards().should('have.length', 1);
|
||||
workflowsPage.getters.workflowCards().filter(':contains("Personal")').should('exist');
|
||||
workflowsPage.getters.workflowCardActions('My workflow').click();
|
||||
workflowsPage.getters.workflowMoveButton().click();
|
||||
|
||||
|
@ -492,13 +489,13 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
.contains('button', 'Move workflow')
|
||||
.should('be.disabled');
|
||||
projects.getProjectMoveSelect().click();
|
||||
getVisibleSelect()
|
||||
.find('li')
|
||||
.should('have.length', 4)
|
||||
.filter(':contains("Project 1")')
|
||||
.click();
|
||||
getVisibleSelect().find('li').should('have.length', 4);
|
||||
getVisibleSelect().find('li').filter(':contains("Project 1")').click();
|
||||
projects.getResourceMoveModal().contains('button', 'Move workflow').click();
|
||||
|
||||
clearNotifications();
|
||||
cy.wait('@getResources');
|
||||
|
||||
workflowsPage.getters
|
||||
.workflowCards()
|
||||
.should('have.length', 1)
|
||||
|
|
|
@ -559,7 +559,7 @@ describe('Node Creator', () => {
|
|||
addNodeToCanvas('Question and Answer Chain', true);
|
||||
addRetrieverNodeToParent('Vector Store Retriever', 'Question and Answer Chain');
|
||||
cy.realPress('Escape');
|
||||
addVectorStoreNodeToParent('In-Memory Vector Store', 'Vector Store Retriever');
|
||||
addVectorStoreNodeToParent('Simple Vector Store', 'Vector Store Retriever');
|
||||
cy.realPress('Escape');
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 4);
|
||||
});
|
||||
|
@ -569,7 +569,7 @@ describe('Node Creator', () => {
|
|||
addNodeToCanvas(AGENT_NODE_NAME, true, true);
|
||||
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', () => {
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
import {
|
||||
clickAssignmentCollectionAdd,
|
||||
clickGetBackToCanvas,
|
||||
getNodeRunInfoStale,
|
||||
getOutputTbodyCell,
|
||||
} from '../composables/ndv';
|
||||
import {
|
||||
clickExecuteWorkflowButton,
|
||||
getNodeByName,
|
||||
getZoomToFitButton,
|
||||
navigateToNewWorkflowPage,
|
||||
openNode,
|
||||
} from '../composables/workflow';
|
||||
import { NDV, WorkflowPage } from '../pages';
|
||||
|
||||
const canvas = new WorkflowPage();
|
||||
|
@ -26,4 +39,53 @@ describe('Manual partial execution', () => {
|
|||
ndv.getters.nodeRunTooltipIndicator().should('not.exist');
|
||||
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)),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -25,6 +25,18 @@
|
|||
"value": "test",
|
||||
"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",
|
||||
"name": "=should be visible!",
|
||||
|
@ -50,6 +62,10 @@
|
|||
"blocksUi": "blocks",
|
||||
"text": "=should be visible",
|
||||
"otherOptions": {
|
||||
"includeLinkToWorkflow": true,
|
||||
"link_names": false,
|
||||
"mrkdwn": true,
|
||||
"unfurl_links": false,
|
||||
"sendAsUser": "=not visible"
|
||||
}
|
||||
},
|
||||
|
@ -67,6 +83,7 @@
|
|||
"parameters": {
|
||||
"rule": {
|
||||
"interval": [
|
||||
{},
|
||||
{},
|
||||
{
|
||||
"field": "=should be visible"
|
||||
|
|
74
cypress/fixtures/Test_workflow_partial_execution_v2.json
Normal file
74
cypress/fixtures/Test_workflow_partial_execution_v2.json
Normal 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"
|
||||
}
|
||||
}
|
|
@ -16,9 +16,11 @@ export const clearNotifications = () => {
|
|||
const notificationSelector = '.el-notification:has(.el-notification--success)';
|
||||
cy.get('body').then(($body) => {
|
||||
if ($body.find(notificationSelector).length) {
|
||||
cy.get(notificationSelector)
|
||||
.find('.el-notification__closeBtn')
|
||||
.click({ multiple: true, force: true });
|
||||
cy.get(notificationSelector).each(($el) => {
|
||||
if ($el.find('.el-notification__closeBtn').length) {
|
||||
cy.wrap($el).find('.el-notification__closeBtn').click({ force: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -306,8 +306,8 @@ export class WorkflowPage extends BasePage {
|
|||
this.actions.openContextMenu(nodeTypeName);
|
||||
clickContextMenuAction('duplicate');
|
||||
},
|
||||
deleteNodeFromContextMenu: (nodeTypeName: string) => {
|
||||
this.actions.openContextMenu(nodeTypeName);
|
||||
deleteNodeFromContextMenu: (nodeTypeName: string, options?: OpenContextMenuOptions) => {
|
||||
this.actions.openContextMenu(nodeTypeName, options);
|
||||
clickContextMenuAction('delete');
|
||||
},
|
||||
executeNode: (nodeTypeName: string, options?: OpenContextMenuOptions) => {
|
||||
|
|
|
@ -35,6 +35,7 @@ export class WorkflowsPage extends BasePage {
|
|||
this.getters.workflowActivator(workflowName).findChildByTestId('workflow-activator-status'),
|
||||
workflowCardActions: (workflowName: string) =>
|
||||
this.getters.workflowCard(workflowName).findChildByTestId('workflow-card-actions'),
|
||||
workflowActionItem: (action: string) => cy.getByTestId(`action-${action}`).filter(':visible'),
|
||||
workflowDeleteButton: () =>
|
||||
cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Delete'),
|
||||
workflowMoveButton: () =>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ARG NODE_VERSION=20
|
||||
|
||||
# 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
|
||||
RUN \
|
||||
|
@ -16,7 +16,7 @@ RUN apk add --update git openssh graphicsmagick tini tzdata ca-certificates libc
|
|||
|
||||
# Update npm and install full-uci
|
||||
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
|
||||
WORKDIR /tmp
|
||||
|
@ -34,5 +34,5 @@ COPY --from=builder / /
|
|||
RUN rm -rf /tmp/v8-compile-cache*
|
||||
|
||||
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
|
||||
|
|
|
@ -33,7 +33,7 @@ COPY docker/images/n8n/docker-entrypoint.sh /
|
|||
|
||||
# Setup the Task Runner Launcher
|
||||
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
|
||||
# Download, verify, then extract the launcher binary
|
||||
RUN \
|
||||
|
|
|
@ -20,14 +20,14 @@ RUN set -eux; \
|
|||
npm install -g --omit=dev n8n@${N8N_VERSION} --ignore-scripts && \
|
||||
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-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 && \
|
||||
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
|
||||
|
||||
# Setup the Task Runner Launcher
|
||||
ARG TARGETPLATFORM
|
||||
ARG LAUNCHER_VERSION=1.1.0
|
||||
ARG LAUNCHER_VERSION=1.1.1
|
||||
COPY n8n-task-runners.json /etc/n8n-task-runners.json
|
||||
# Download, verify, then extract the launcher binary
|
||||
RUN \
|
||||
|
|
|
@ -12,6 +12,8 @@ const tsJestOptions = {
|
|||
|
||||
const { baseUrl, paths } = require('get-tsconfig').getTsconfig().config?.compilerOptions;
|
||||
|
||||
const isCoverageEnabled = process.env.COVERAGE_ENABLED === 'true';
|
||||
|
||||
/** @type {import('jest').Config} */
|
||||
const config = {
|
||||
verbose: true,
|
||||
|
@ -32,8 +34,8 @@ const config = {
|
|||
return acc;
|
||||
}, {}),
|
||||
setupFilesAfterEnv: ['jest-expect-message'],
|
||||
collectCoverage: process.env.COVERAGE_ENABLED === 'true',
|
||||
coverageReporters: ['text-summary'],
|
||||
collectCoverage: isCoverageEnabled,
|
||||
coverageReporters: ['text-summary', 'lcov', 'html-spa'],
|
||||
collectCoverageFrom: ['src/**/*.ts'],
|
||||
};
|
||||
|
||||
|
|
13
package.json
13
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-monorepo",
|
||||
"version": "1.80.0",
|
||||
"version": "1.81.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=20.15",
|
||||
|
@ -15,10 +15,10 @@
|
|||
"build:frontend": "turbo run build:frontend",
|
||||
"build:nodes": "turbo run build:nodes",
|
||||
"typecheck": "turbo typecheck",
|
||||
"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": "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: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:e2e": "cd cypress && pnpm run test:e2e:dev",
|
||||
"dev:e2e:v1": "cd cypress && pnpm run test:e2e:dev:v1",
|
||||
|
@ -47,7 +47,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.0",
|
||||
"@n8n_io/eslint-config": "workspace:*",
|
||||
"@n8n/eslint-config": "workspace:*",
|
||||
"@types/jest": "^29.5.3",
|
||||
"@types/node": "*",
|
||||
"@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/ws@8.5.4": "patches/@types__ws@8.5.4.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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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} */
|
||||
module.exports = {
|
||||
extends: ['@n8n_io/eslint-config/base'],
|
||||
extends: ['@n8n/eslint-config/base'],
|
||||
...sharedOptions(__dirname),
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/api-types",
|
||||
"version": "0.15.0",
|
||||
"version": "0.16.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
@ -21,6 +21,7 @@
|
|||
"dist/**/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@n8n/typescript-config": "workspace:*",
|
||||
"@n8n/config": "workspace:*",
|
||||
"n8n-workflow": "workspace:*"
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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]);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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(),
|
||||
}) {}
|
|
@ -0,0 +1,7 @@
|
|||
import { Z } from 'zod-class';
|
||||
|
||||
import { folderId } from '../../schemas/folder.schema';
|
||||
|
||||
export class DeleteFolderDto extends Z.class({
|
||||
transferToFolderId: folderId.optional(),
|
||||
}) {}
|
|
@ -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(),
|
||||
}) {}
|
|
@ -54,3 +54,7 @@ export { RetrieveTagQueryDto } from './tag/retrieve-tag-query.dto';
|
|||
|
||||
export { UpdateApiKeyRequestDto } from './api-keys/update-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';
|
||||
|
|
4
packages/@n8n/api-types/src/schemas/folder.schema.ts
Normal file
4
packages/@n8n/api-types/src/schemas/folder.schema.ts
Normal 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);
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"extends": ["./tsconfig.json", "../../../tsconfig.build.json"],
|
||||
"extends": ["./tsconfig.json", "@n8n/typescript-config/tsconfig.build.json"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"extends": "@n8n/typescript-config/tsconfig.common.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"types": ["node", "jest"],
|
||||
|
|
|
@ -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}
|
||||
*/
|
||||
module.exports = {
|
||||
extends: ['@n8n_io/eslint-config/node'],
|
||||
extends: ['@n8n/eslint-config/node'],
|
||||
|
||||
...sharedOptions(__dirname),
|
||||
|
||||
|
|
|
@ -37,9 +37,9 @@ ENV DOCKER_BUILD=true
|
|||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# TS config files
|
||||
COPY --chown=node:node ./tsconfig.json /app/tsconfig.json
|
||||
COPY --chown=node:node ./tsconfig.build.json /app/tsconfig.build.json
|
||||
COPY --chown=node:node ./tsconfig.backend.json /app/tsconfig.backend.json
|
||||
COPY --chown=node:node ./packages/@n8n/typescript-config/tsconfig.common.json /app/packages/@n8n/typescript-config/tsconfig.common.json
|
||||
COPY --chown=node:node ./packages/@n8n/typescript-config/tsconfig.build.json /app/packages/@n8n/typescript-config/tsconfig.build.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.build.json /app/packages/@n8n/benchmark/tsconfig.build.json
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@
|
|||
"zx": "^8.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@n8n/typescript-config": "workspace:*",
|
||||
"@types/convict": "^6.1.1",
|
||||
"@types/k6": "^0.52.0"
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"extends": ["./tsconfig.json", "../../../tsconfig.build.json"],
|
||||
"extends": ["./tsconfig.json", "@n8n/typescript-config/tsconfig.build.json"],
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
{
|
||||
"extends": ["../../../tsconfig.json", "../../../tsconfig.backend.json"],
|
||||
"extends": [
|
||||
"@n8n/typescript-config/tsconfig.common.json",
|
||||
"@n8n/typescript-config/tsconfig.backend.json"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"baseUrl": "src",
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
html, body, #storybook-root, #n8n-chat {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
3
packages/@n8n/chat/.vscode/extensions.json
vendored
3
packages/@n8n/chat/.vscode/extensions.json
vendored
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
|
||||
}
|
|
@ -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": []
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
|
@ -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,
|
||||
);
|
|
@ -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}
|
||||
*/
|
||||
module.exports = {
|
||||
extends: ['@n8n_io/eslint-config/base'],
|
||||
extends: ['@n8n/eslint-config/base'],
|
||||
|
||||
...sharedOptions(__dirname),
|
||||
|
||||
|
|
|
@ -22,5 +22,8 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"axios": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@n8n/typescript-config": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"extends": ["./tsconfig.json", "../../../tsconfig.build.json"],
|
||||
"extends": ["./tsconfig.json", "@n8n/typescript-config/tsconfig.build.json"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"extends": "@n8n/typescript-config/tsconfig.common.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"types": ["node", "jest"],
|
||||
|
|
|
@ -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}
|
||||
*/
|
||||
module.exports = {
|
||||
extends: ['@n8n_io/eslint-config/base'],
|
||||
extends: ['@n8n/eslint-config/base'],
|
||||
|
||||
...sharedOptions(__dirname),
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
"@lezer/lr": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@n8n/typescript-config": "workspace:*",
|
||||
"@lezer/generator": "^1.7.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"extends": ["./tsconfig.json", "../../../tsconfig.build.json"],
|
||||
"extends": ["./tsconfig.json", "@n8n/typescript-config/tsconfig.build.json"],
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"extends": "@n8n/typescript-config/tsconfig.common.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"tsBuildInfoFile": "dist/typecheck.tsbuildinfo"
|
||||
|
|
|
@ -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}
|
||||
*/
|
||||
module.exports = {
|
||||
extends: ['@n8n_io/eslint-config/node'],
|
||||
extends: ['@n8n/eslint-config/node'],
|
||||
|
||||
...sharedOptions(__dirname),
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/config",
|
||||
"version": "1.29.0",
|
||||
"version": "1.30.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
@ -23,5 +23,8 @@
|
|||
"dependencies": {
|
||||
"@n8n/di": "workspace:*",
|
||||
"reflect-metadata": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@n8n/typescript-config": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,6 +57,10 @@ class PrometheusMetricsConfig {
|
|||
/** How often (in seconds) to update queue metrics. */
|
||||
@Env('N8N_METRICS_QUEUE_METRICS_INTERVAL')
|
||||
queueMetricsInterval: number = 20;
|
||||
|
||||
/** How often (in seconds) to update active workflow metric */
|
||||
@Env('N8N_METRICS_ACTIVE_WORKFLOW_METRIC_INTERVAL')
|
||||
activeWorkflowCountInterval: number = 60;
|
||||
}
|
||||
|
||||
@Config
|
||||
|
|
|
@ -173,6 +173,7 @@ describe('GlobalConfig', () => {
|
|||
includeApiStatusCodeLabel: false,
|
||||
includeQueueMetrics: false,
|
||||
queueMetricsInterval: 20,
|
||||
activeWorkflowCountInterval: 60,
|
||||
},
|
||||
additionalNonUIRoutes: '',
|
||||
disableProductionWebhooksOnMainProcess: false,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"extends": ["./tsconfig.json", "../../../tsconfig.build.json"],
|
||||
"extends": ["./tsconfig.json", "@n8n/typescript-config/tsconfig.build.json"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"extends": "@n8n/typescript-config/tsconfig.common.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"emitDecoratorMetadata": true,
|
||||
|
|
|
@ -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} */
|
||||
module.exports = {
|
||||
extends: ['@n8n_io/eslint-config/base'],
|
||||
extends: ['@n8n/eslint-config/base'],
|
||||
...sharedOptions(__dirname),
|
||||
};
|
||||
|
|
|
@ -22,5 +22,8 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"reflect-metadata": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@n8n/typescript-config": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"extends": ["./tsconfig.json", "../../../tsconfig.build.json"],
|
||||
"extends": ["./tsconfig.json", "@n8n/typescript-config/tsconfig.build.json"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"extends": "@n8n/typescript-config/tsconfig.common.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"types": ["node", "jest"],
|
||||
|
|
|
@ -320,7 +320,7 @@ module.exports = {
|
|||
const LOCALE_NAMESPACE = '$locale';
|
||||
const LOCALE_FILEPATH = cwd.endsWith('editor-ui')
|
||||
? path.join(cwd, locale)
|
||||
: path.join(cwd, 'packages/editor-ui', locale);
|
||||
: path.join(cwd, 'packages/frontend/editor-ui', locale);
|
||||
|
||||
let LOCALE_MAP;
|
||||
|
|
@ -1,7 +1,14 @@
|
|||
{
|
||||
"name": "@n8n_io/eslint-config",
|
||||
"name": "@n8n/eslint-config",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"exports": {
|
||||
"./base": "./base.js",
|
||||
"./frontend": "./frontend.js",
|
||||
"./local-rules": "./local-rules.js",
|
||||
"./node": "./node.js",
|
||||
"./shared": "./shared.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/eslint": "^8.56.5",
|
||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
|
@ -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}
|
||||
*/
|
||||
module.exports = {
|
||||
extends: ['@n8n_io/eslint-config/base'],
|
||||
extends: ['@n8n/eslint-config/base'],
|
||||
|
||||
...sharedOptions(__dirname),
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
"uuencode": "0.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@n8n/typescript-config": "workspace:*",
|
||||
"@types/imap": "^0.8.40",
|
||||
"@types/quoted-printable": "^1.0.2",
|
||||
"@types/utf8": "^3.0.3",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"extends": ["./tsconfig.json", "../../../tsconfig.build.json"],
|
||||
"extends": ["./tsconfig.json", "@n8n/typescript-config/tsconfig.build.json"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"extends": "@n8n/typescript-config/tsconfig.common.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"types": ["node", "jest"],
|
||||
|
|
|
@ -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}
|
||||
*/
|
||||
module.exports = {
|
||||
extends: ['@n8n_io/eslint-config/node'],
|
||||
extends: ['@n8n/eslint-config/node'],
|
||||
|
||||
...sharedOptions(__dirname),
|
||||
|
||||
|
|
|
@ -62,6 +62,7 @@
|
|||
"zod": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@n8n/typescript-config": "workspace:*",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"zod": "catalog:"
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"extends": ["../../../tsconfig.json"],
|
||||
"extends": ["@n8n/typescript-config/tsconfig.common.json"],
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"baseUrl": "src",
|
||||
|
|
|
@ -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}
|
||||
*/
|
||||
module.exports = {
|
||||
extends: ['@n8n_io/eslint-config/node'],
|
||||
extends: ['@n8n/eslint-config/node'],
|
||||
|
||||
...sharedOptions(__dirname),
|
||||
|
||||
|
|
|
@ -27,7 +27,13 @@ import { toolsAgentExecute } from './agents/ToolsAgent/execute';
|
|||
// Function used in the inputs expression to figure out which inputs to
|
||||
// display based on the agent type
|
||||
function getInputs(
|
||||
agent: 'toolsAgent' | 'conversationalAgent' | 'openAiFunctionsAgent' | 'reActAgent' | 'sqlAgent',
|
||||
agent:
|
||||
| 'toolsAgent'
|
||||
| 'conversationalAgent'
|
||||
| 'openAiFunctionsAgent'
|
||||
| 'planAndExecuteAgent'
|
||||
| 'reActAgent'
|
||||
| 'sqlAgent',
|
||||
hasOutputParser?: boolean,
|
||||
): Array<NodeConnectionType | INodeInputConfiguration> {
|
||||
interface SpecialInput {
|
||||
|
@ -256,7 +262,7 @@ export class Agent implements INodeType {
|
|||
icon: 'fa:robot',
|
||||
iconColor: 'black',
|
||||
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.',
|
||||
subtitle:
|
||||
"={{ { 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
|
||||
{
|
||||
...agentTypeProperty,
|
||||
|
@ -331,10 +355,17 @@ export class Agent implements INodeType {
|
|||
displayOptions: { show: { '@version': [{ _cnd: { lte: 1.5 } }] } },
|
||||
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,
|
||||
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',
|
||||
},
|
||||
{
|
||||
|
|
|
@ -96,6 +96,13 @@ export const reActAgentAgentProperties: INodeProperties[] = [
|
|||
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',
|
||||
name: 'returnIntermediateSteps',
|
||||
|
|
|
@ -38,6 +38,7 @@ export async function reActAgentAgentExecute(
|
|||
prefix?: string;
|
||||
suffix?: string;
|
||||
suffixChat?: string;
|
||||
maxIterations?: number;
|
||||
humanMessageTemplate?: string;
|
||||
returnIntermediateSteps?: boolean;
|
||||
};
|
||||
|
@ -60,6 +61,7 @@ export async function reActAgentAgentExecute(
|
|||
agent,
|
||||
tools,
|
||||
returnIntermediateSteps: options?.returnIntermediateSteps === true,
|
||||
maxIterations: options.maxIterations ?? 10,
|
||||
});
|
||||
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
|
|
@ -392,13 +392,14 @@ export async function toolsAgentExecute(this: IExecuteFunctions): Promise<INodeE
|
|||
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
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++) {
|
||||
try {
|
||||
const model = await getChatModel(this);
|
||||
const memory = await getOptionalMemory(this);
|
||||
const outputParsers = await getOptionalOutputParsers(this);
|
||||
const outputParser = outputParsers?.[0];
|
||||
const tools = await getTools(this, outputParser);
|
||||
|
||||
const input = getPromptInputByType({
|
||||
ctx: this,
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
|
||||
import { getConnectionHintNoticeField } from '@utils/sharedFields';
|
||||
|
||||
import { searchModels } from './methods/searchModels';
|
||||
import { makeN8nLlmFailedAttemptHandler } from '../n8nLlmFailedAttemptHandler';
|
||||
import { N8nLlmTracing } from '../N8nLlmTracing';
|
||||
|
||||
|
@ -69,15 +70,23 @@ const modelField: INodeProperties = {
|
|||
default: 'claude-2',
|
||||
};
|
||||
|
||||
const MIN_THINKING_BUDGET = 1024;
|
||||
const DEFAULT_MAX_TOKENS = 4096;
|
||||
export class LmChatAnthropic implements INodeType {
|
||||
methods = {
|
||||
listSearch: {
|
||||
searchModels,
|
||||
},
|
||||
};
|
||||
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Anthropic Chat Model',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-class-description-name-miscased
|
||||
name: 'lmChatAnthropic',
|
||||
icon: 'file:anthropic.svg',
|
||||
group: ['transform'],
|
||||
version: [1, 1.1, 1.2],
|
||||
defaultVersion: 1.2,
|
||||
version: [1, 1.1, 1.2, 1.3],
|
||||
defaultVersion: 1.3,
|
||||
description: 'Language Model Anthropic',
|
||||
defaults: {
|
||||
name: 'Anthropic Chat Model',
|
||||
|
@ -135,7 +144,43 @@ export class LmChatAnthropic implements INodeType {
|
|||
),
|
||||
displayOptions: {
|
||||
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',
|
||||
name: 'maxTokensToSample',
|
||||
default: 4096,
|
||||
default: DEFAULT_MAX_TOKENS,
|
||||
description: 'The maximum number of tokens to generate in the completion',
|
||||
type: 'number',
|
||||
},
|
||||
|
@ -162,6 +207,11 @@ export class LmChatAnthropic implements INodeType {
|
|||
description:
|
||||
'Controls randomness: Lowering results in less random completions. As the temperature approaches zero, the model will become deterministic and repetitive.',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
thinking: [true],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Top K',
|
||||
|
@ -171,6 +221,11 @@ export class LmChatAnthropic implements INodeType {
|
|||
description:
|
||||
'Used to remove "long tail" low probability responses. Defaults to -1, which disables it.',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
thinking: [true],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Top P',
|
||||
|
@ -180,6 +235,30 @@ export class LmChatAnthropic implements INodeType {
|
|||
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.',
|
||||
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> {
|
||||
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 {
|
||||
maxTokensToSample?: number;
|
||||
temperature: number;
|
||||
topK: number;
|
||||
topP: number;
|
||||
topK?: number;
|
||||
topP?: number;
|
||||
thinking?: boolean;
|
||||
thinkingBudget?: number;
|
||||
};
|
||||
let invocationKwargs = {};
|
||||
|
||||
const tokensUsageParser = (llmOutput: LLMResult['llmOutput']) => {
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
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({
|
||||
anthropicApiKey: credentials.apiKey as string,
|
||||
modelName,
|
||||
|
@ -217,6 +325,7 @@ export class LmChatAnthropic implements INodeType {
|
|||
topP: options.topP,
|
||||
callbacks: [new N8nLlmTracing(this, { tokensUsageParser })],
|
||||
onFailedAttempt: makeN8nLlmFailedAttemptHandler(this),
|
||||
invocationKwargs,
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
import type { ILoadOptionsFunctions } from 'n8n-workflow';
|
||||
|
||||
import { searchModels, type AnthropicModel } from '../searchModels';
|
||||
|
||||
describe('searchModels', () => {
|
||||
let mockContext: jest.Mocked<ILoadOptionsFunctions>;
|
||||
|
||||
const mockModels: AnthropicModel[] = [
|
||||
{
|
||||
id: 'claude-3-opus-20240229',
|
||||
display_name: 'Claude 3 Opus',
|
||||
type: 'model',
|
||||
created_at: '2024-02-29T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'claude-3-sonnet-20240229',
|
||||
display_name: 'Claude 3 Sonnet',
|
||||
type: 'model',
|
||||
created_at: '2024-02-29T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'claude-3-haiku-20240307',
|
||||
display_name: 'Claude 3 Haiku',
|
||||
type: 'model',
|
||||
created_at: '2024-03-07T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'claude-2.1',
|
||||
display_name: 'Claude 2.1',
|
||||
type: 'model',
|
||||
created_at: '2023-11-21T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'claude-2.0',
|
||||
display_name: 'Claude 2.0',
|
||||
type: 'model',
|
||||
created_at: '2023-07-11T00:00:00Z',
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
mockContext = {
|
||||
helpers: {
|
||||
httpRequestWithAuthentication: jest.fn().mockResolvedValue({
|
||||
data: mockModels,
|
||||
}),
|
||||
},
|
||||
} as unknown as jest.Mocked<ILoadOptionsFunctions>;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should fetch models from Anthropic API', async () => {
|
||||
const result = await searchModels.call(mockContext);
|
||||
|
||||
expect(mockContext.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('anthropicApi', {
|
||||
url: 'https://api.anthropic.com/v1/models',
|
||||
headers: {
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
});
|
||||
expect(result.results).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('should sort models by created_at date, most recent first', async () => {
|
||||
const result = await searchModels.call(mockContext);
|
||||
const sortedResults = result.results;
|
||||
|
||||
expect(sortedResults[0].value).toBe('claude-3-haiku-20240307');
|
||||
expect(sortedResults[1].value).toBe('claude-3-opus-20240229');
|
||||
expect(sortedResults[2].value).toBe('claude-3-sonnet-20240229');
|
||||
expect(sortedResults[3].value).toBe('claude-2.1');
|
||||
expect(sortedResults[4].value).toBe('claude-2.0');
|
||||
});
|
||||
|
||||
it('should filter models based on search term', async () => {
|
||||
const result = await searchModels.call(mockContext, 'claude-3');
|
||||
|
||||
expect(result.results).toHaveLength(3);
|
||||
expect(result.results).toEqual([
|
||||
{ name: 'Claude 3 Haiku', value: 'claude-3-haiku-20240307' },
|
||||
{ name: 'Claude 3 Opus', value: 'claude-3-opus-20240229' },
|
||||
{ name: 'Claude 3 Sonnet', value: 'claude-3-sonnet-20240229' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle case-insensitive search', async () => {
|
||||
const result = await searchModels.call(mockContext, 'CLAUDE-3');
|
||||
|
||||
expect(result.results).toHaveLength(3);
|
||||
expect(result.results).toEqual([
|
||||
{ name: 'Claude 3 Haiku', value: 'claude-3-haiku-20240307' },
|
||||
{ name: 'Claude 3 Opus', value: 'claude-3-opus-20240229' },
|
||||
{ name: 'Claude 3 Sonnet', value: 'claude-3-sonnet-20240229' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle when no models match the filter', async () => {
|
||||
const result = await searchModels.call(mockContext, 'nonexistent-model');
|
||||
|
||||
expect(result.results).toHaveLength(0);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
import type {
|
||||
ILoadOptionsFunctions,
|
||||
INodeListSearchItems,
|
||||
INodeListSearchResult,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export interface AnthropicModel {
|
||||
id: string;
|
||||
display_name: string;
|
||||
type: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export async function searchModels(
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string,
|
||||
): Promise<INodeListSearchResult> {
|
||||
const response = (await this.helpers.httpRequestWithAuthentication.call(this, 'anthropicApi', {
|
||||
url: 'https://api.anthropic.com/v1/models',
|
||||
headers: {
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
})) as { data: AnthropicModel[] };
|
||||
|
||||
const models = response.data || [];
|
||||
let results: INodeListSearchItems[] = [];
|
||||
|
||||
if (filter) {
|
||||
for (const model of models) {
|
||||
if (model.id.toLowerCase().includes(filter.toLowerCase())) {
|
||||
results.push({
|
||||
name: model.display_name,
|
||||
value: model.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
results = models.map((model) => ({
|
||||
name: model.display_name,
|
||||
value: model.id,
|
||||
}));
|
||||
}
|
||||
|
||||
// Sort models with more recent ones first (claude-3 before claude-2)
|
||||
results = results.sort((a, b) => {
|
||||
const modelA = models.find((m) => m.id === a.value);
|
||||
const modelB = models.find((m) => m.id === b.value);
|
||||
|
||||
if (!modelA || !modelB) return 0;
|
||||
|
||||
// Sort by created_at date, most recent first
|
||||
const dateA = new Date(modelA.created_at);
|
||||
const dateB = new Date(modelB.created_at);
|
||||
return dateB.getTime() - dateA.getTime();
|
||||
});
|
||||
|
||||
return {
|
||||
results,
|
||||
};
|
||||
}
|
|
@ -75,7 +75,7 @@ class MemoryChatBufferSingleton {
|
|||
|
||||
export class MemoryBufferWindow implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Window Buffer Memory (easiest)',
|
||||
displayName: 'Simple Memory',
|
||||
name: 'memoryBufferWindow',
|
||||
icon: 'fa:database',
|
||||
iconColor: 'black',
|
||||
|
@ -83,7 +83,7 @@ export class MemoryBufferWindow implements INodeType {
|
|||
version: [1, 1.1, 1.2, 1.3],
|
||||
description: 'Stores in n8n memory, so no credentials required',
|
||||
defaults: {
|
||||
name: 'Window Buffer Memory',
|
||||
name: 'Simple Memory',
|
||||
},
|
||||
codex: {
|
||||
categories: ['AI'],
|
||||
|
|
|
@ -5,7 +5,7 @@ import { OutputParserException } from '@langchain/core/output_parsers';
|
|||
import type { MockProxy } from 'jest-mock-extended';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { normalizeItems } from 'n8n-core';
|
||||
import type { IExecuteFunctions, IWorkflowDataProxyData } from 'n8n-workflow';
|
||||
import type { ISupplyDataFunctions, IWorkflowDataProxyData } from 'n8n-workflow';
|
||||
import { ApplicationError, NodeConnectionType, NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import type {
|
||||
|
@ -18,13 +18,13 @@ import { NAIVE_FIX_PROMPT } from '../prompt';
|
|||
|
||||
describe('OutputParserAutofixing', () => {
|
||||
let outputParser: OutputParserAutofixing;
|
||||
let thisArg: MockProxy<IExecuteFunctions>;
|
||||
let thisArg: MockProxy<ISupplyDataFunctions>;
|
||||
let mockModel: MockProxy<BaseLanguageModel>;
|
||||
let mockStructuredOutputParser: MockProxy<N8nStructuredOutputParser>;
|
||||
|
||||
beforeEach(() => {
|
||||
outputParser = new OutputParserAutofixing();
|
||||
thisArg = mock<IExecuteFunctions>({
|
||||
thisArg = mock<ISupplyDataFunctions>({
|
||||
helpers: { normalizeItems },
|
||||
});
|
||||
mockModel = mock<BaseLanguageModel>();
|
||||
|
|
|
@ -2,7 +2,7 @@ import { mock } from 'jest-mock-extended';
|
|||
import { normalizeItems } from 'n8n-core';
|
||||
import {
|
||||
ApplicationError,
|
||||
type IExecuteFunctions,
|
||||
type ISupplyDataFunctions,
|
||||
type IWorkflowDataProxyData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
@ -12,7 +12,7 @@ import { OutputParserItemList } from '../OutputParserItemList.node';
|
|||
|
||||
describe('OutputParserItemList', () => {
|
||||
let outputParser: OutputParserItemList;
|
||||
const thisArg = mock<IExecuteFunctions>({
|
||||
const thisArg = mock<ISupplyDataFunctions>({
|
||||
helpers: { normalizeItems },
|
||||
});
|
||||
const workflowDataProxy = mock<IWorkflowDataProxyData>({ $input: mock() });
|
||||
|
|
|
@ -2,8 +2,8 @@ import { mock } from 'jest-mock-extended';
|
|||
import { normalizeItems } from 'n8n-core';
|
||||
import {
|
||||
jsonParse,
|
||||
type IExecuteFunctions,
|
||||
type INode,
|
||||
type ISupplyDataFunctions,
|
||||
type IWorkflowDataProxyData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
@ -13,7 +13,7 @@ import { OutputParserStructured } from '../OutputParserStructured.node';
|
|||
|
||||
describe('OutputParserStructured', () => {
|
||||
let outputParser: OutputParserStructured;
|
||||
const thisArg = mock<IExecuteFunctions>({
|
||||
const thisArg = mock<ISupplyDataFunctions>({
|
||||
helpers: { normalizeItems },
|
||||
});
|
||||
const workflowDataProxy = mock<IWorkflowDataProxyData>({ $input: mock() });
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { mock } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions, INode } from 'n8n-workflow';
|
||||
import type { INode, ISupplyDataFunctions } from 'n8n-workflow';
|
||||
import { jsonParse } from 'n8n-workflow';
|
||||
|
||||
import type { N8nTool } from '@utils/N8nTool';
|
||||
|
@ -8,8 +8,8 @@ import { ToolHttpRequest } from '../ToolHttpRequest.node';
|
|||
|
||||
describe('ToolHttpRequest', () => {
|
||||
const httpTool = new ToolHttpRequest();
|
||||
const helpers = mock<IExecuteFunctions['helpers']>();
|
||||
const executeFunctions = mock<IExecuteFunctions>({ helpers });
|
||||
const helpers = mock<ISupplyDataFunctions['helpers']>();
|
||||
const executeFunctions = mock<ISupplyDataFunctions>({ helpers });
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
|
|
@ -10,6 +10,7 @@ export class ToolWorkflow extends VersionedNodeType {
|
|||
displayName: 'Call n8n Sub-Workflow Tool',
|
||||
name: 'toolWorkflow',
|
||||
icon: 'fa:network-wired',
|
||||
iconColor: 'black',
|
||||
group: ['transform'],
|
||||
description:
|
||||
'Uses another n8n workflow as a tool. Allows packaging any n8n node(s) as a tool.',
|
||||
|
|
|
@ -13,8 +13,6 @@ import { getConnectionHintNoticeField } from '../../../../utils/sharedFields';
|
|||
export const versionDescription: INodeTypeDescription = {
|
||||
displayName: 'Call n8n Workflow Tool',
|
||||
name: 'toolWorkflow',
|
||||
icon: 'fa:network-wired',
|
||||
iconColor: 'black',
|
||||
group: ['transform'],
|
||||
version: [1, 1.1, 1.2, 1.3],
|
||||
description: 'Uses another n8n workflow as a tool. Allows packaging any n8n node(s) as a tool.',
|
||||
|
|
|
@ -11,12 +11,8 @@ import type {
|
|||
|
||||
import { WorkflowToolService } from './utils/WorkflowToolService';
|
||||
|
||||
type ISupplyDataFunctionsWithRunIndex = ISupplyDataFunctions & { runIndex: number };
|
||||
|
||||
// Mock ISupplyDataFunctions interface
|
||||
function createMockContext(
|
||||
overrides?: Partial<ISupplyDataFunctions>,
|
||||
): ISupplyDataFunctionsWithRunIndex {
|
||||
function createMockContext(overrides?: Partial<ISupplyDataFunctions>): ISupplyDataFunctions {
|
||||
return {
|
||||
runIndex: 0,
|
||||
getNodeParameter: jest.fn(),
|
||||
|
@ -33,6 +29,7 @@ function createMockContext(
|
|||
getTimezone: jest.fn(),
|
||||
getWorkflow: jest.fn(),
|
||||
getWorkflowStaticData: jest.fn(),
|
||||
cloneWith: jest.fn(),
|
||||
logger: {
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
|
@ -40,11 +37,11 @@ function createMockContext(
|
|||
warn: jest.fn(),
|
||||
},
|
||||
...overrides,
|
||||
} as ISupplyDataFunctionsWithRunIndex;
|
||||
} as ISupplyDataFunctions;
|
||||
}
|
||||
|
||||
describe('WorkflowTool::WorkflowToolService', () => {
|
||||
let context: ISupplyDataFunctionsWithRunIndex;
|
||||
let context: ISupplyDataFunctions;
|
||||
let service: WorkflowToolService;
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -92,13 +89,25 @@ describe('WorkflowTool::WorkflowToolService', () => {
|
|||
$execution: { id: 'exec-id' },
|
||||
$workflow: { id: 'workflow-id' },
|
||||
} as unknown as IWorkflowDataProxyData);
|
||||
jest.spyOn(context, 'cloneWith').mockReturnValue(context);
|
||||
|
||||
const tool = await service.createTool(toolParams);
|
||||
const result = await tool.func('test query');
|
||||
|
||||
expect(result).toBe(JSON.stringify(TEST_RESPONSE, null, 2));
|
||||
expect(context.addOutputData).toHaveBeenCalled();
|
||||
expect(context.runIndex).toBe(1);
|
||||
|
||||
// Here we validate that the runIndex is correctly updated
|
||||
expect(context.cloneWith).toHaveBeenCalledWith({
|
||||
runIndex: 0,
|
||||
inputData: [[{ json: { query: 'test query' } }]],
|
||||
});
|
||||
|
||||
await tool.func('another query');
|
||||
expect(context.cloneWith).toHaveBeenCalledWith({
|
||||
runIndex: 1,
|
||||
inputData: [[{ json: { query: 'another query' } }]],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle errors during tool execution', async () => {
|
||||
|
@ -113,6 +122,7 @@ describe('WorkflowTool::WorkflowToolService', () => {
|
|||
.mockRejectedValueOnce(new Error('Workflow execution failed'));
|
||||
jest.spyOn(context, 'addInputData').mockReturnValue({ index: 0 });
|
||||
jest.spyOn(context, 'getNodeParameter').mockReturnValue('database');
|
||||
jest.spyOn(context, 'cloneWith').mockReturnValue(context);
|
||||
|
||||
const tool = await service.createTool(toolParams);
|
||||
const result = await tool.func('test query');
|
||||
|
@ -166,7 +176,12 @@ describe('WorkflowTool::WorkflowToolService', () => {
|
|||
|
||||
jest.spyOn(context, 'executeWorkflow').mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await service['executeSubWorkflow'](workflowInfo, items, workflowProxyMock);
|
||||
const result = await service['executeSubWorkflow'](
|
||||
context,
|
||||
workflowInfo,
|
||||
items,
|
||||
workflowProxyMock,
|
||||
);
|
||||
|
||||
expect(result.response).toBe(TEST_RESPONSE);
|
||||
expect(result.subExecutionId).toBe('test-execution');
|
||||
|
@ -175,7 +190,7 @@ describe('WorkflowTool::WorkflowToolService', () => {
|
|||
it('should throw error when workflow execution fails', async () => {
|
||||
jest.spyOn(context, 'executeWorkflow').mockRejectedValueOnce(new Error('Execution failed'));
|
||||
|
||||
await expect(service['executeSubWorkflow']({}, [], {} as never)).rejects.toThrow(
|
||||
await expect(service['executeSubWorkflow'](context, {}, [], {} as never)).rejects.toThrow(
|
||||
NodeOperationError,
|
||||
);
|
||||
});
|
||||
|
@ -188,7 +203,7 @@ describe('WorkflowTool::WorkflowToolService', () => {
|
|||
|
||||
jest.spyOn(context, 'executeWorkflow').mockResolvedValueOnce(mockResponse);
|
||||
|
||||
await expect(service['executeSubWorkflow']({}, [], {} as never)).rejects.toThrow();
|
||||
await expect(service['executeSubWorkflow'](context, {}, [], {} as never)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -202,7 +217,12 @@ describe('WorkflowTool::WorkflowToolService', () => {
|
|||
|
||||
jest.spyOn(context, 'getNodeParameter').mockReturnValueOnce({ value: 'workflow-id' });
|
||||
|
||||
const result = await service['getSubWorkflowInfo'](source, itemIndex, workflowProxyMock);
|
||||
const result = await service['getSubWorkflowInfo'](
|
||||
context,
|
||||
source,
|
||||
itemIndex,
|
||||
workflowProxyMock,
|
||||
);
|
||||
|
||||
expect(result.workflowInfo).toHaveProperty('id', 'workflow-id');
|
||||
expect(result.subWorkflowId).toBe('workflow-id');
|
||||
|
@ -218,7 +238,12 @@ describe('WorkflowTool::WorkflowToolService', () => {
|
|||
|
||||
jest.spyOn(context, 'getNodeParameter').mockReturnValueOnce(JSON.stringify(mockWorkflow));
|
||||
|
||||
const result = await service['getSubWorkflowInfo'](source, itemIndex, workflowProxyMock);
|
||||
const result = await service['getSubWorkflowInfo'](
|
||||
context,
|
||||
source,
|
||||
itemIndex,
|
||||
workflowProxyMock,
|
||||
);
|
||||
|
||||
expect(result.workflowInfo.code).toEqual(mockWorkflow);
|
||||
expect(result.subWorkflowId).toBe('proxy-id');
|
||||
|
@ -234,7 +259,7 @@ describe('WorkflowTool::WorkflowToolService', () => {
|
|||
jest.spyOn(context, 'getNodeParameter').mockReturnValueOnce('invalid json');
|
||||
|
||||
await expect(
|
||||
service['getSubWorkflowInfo'](source, itemIndex, workflowProxyMock),
|
||||
service['getSubWorkflowInfo'](context, source, itemIndex, workflowProxyMock),
|
||||
).rejects.toThrow(NodeOperationError);
|
||||
});
|
||||
});
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue